模块化方案
立即执行函数
早期常用的手段。解决命名冲突
、污染全局作用域
的问题
let a = {};
(function(globalVariable){
globalVariable.test = 1
// 声明各种变量、函数都不会污染全局作用域
})(a)
console.log(a.test) // 1
CommonJS
- commonjs是Node中的模块规范,通过
require
以及export
进行导入和导出。module.export
属于commonjs2
// math.js
module.exports = {
add: (x,y) => {
return x + y;
}
}
// main.js
const math = require('./CommonJS/math')
console.log(math.add(1, 1))
cjs
是动态加载,可以直接require
一个变量,如:
require(`./${a}`);
require
命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成一个对象
,下次require
时,直接在缓存中取。
id
是模块名,exports
是该模块导出的接口,loaded
表示模块是否加载完毕。
{
id: '...',
exports: { ... },
loaded: true,
...
}
commonjs
默认只能在node环境下使用,不能在浏览器中直接使用。根本原因是缺少
module
、exports
、require
、global
四个模块。只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。另外,由于
CommonJS
是同步加载
模块,这对于服务器端不是一个问题,因为所有的模块都放在本地硬盘。但是,对于浏览器而言,它需要从服务器加载模块,涉及到网速,代理等原因,一旦等待时间过长,浏览器处于”假死”状态。所以在浏览器端,不适合于CommonJS规范。
于是,在浏览器端又出现了一个规范—AMD
AMD
AMD是Asynchronous Module Definition
的缩写,采用异步方式加载模块,模块的加载不影响它后面语句的运行。
RequireJS
是一个非常小巧的JavaScript模块载入框架,是AMD规范最好的实现者之一。在
RequireJS
内部,会使用head.appendChild()
将每一个模块依赖加载为一个script标签。RequireJS
等待所有的依赖加载完毕,计算出模块定义函数正确调用顺序,然后依次调用它们。
模块功能主要的几个命令:define
、require
、return
和define.amd
。
define
是全局函数,用来定义模块,define(id?, dependencies?, factory)
。
require
命令用于输入其他模块提供的功能
return
命令用于规范模块的对外接口
define.amd
属性是一个对象,此属性的存在来表明函数遵循AMD规范。
我们借助require.js
来使用AMD
规范:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<span>body</span>
<!-- AMD -->
<!-- require.js在加载的时候会检查data-main属性,当requireJS自身加载执行后,就会再次异步加载data-main属性指向的main.js。 -->
<script src="./AMD/require.js" data-main="main.js"></script>
</body>
</html>
// main.js
// AMD
require(['./AMD/math'], function (math) {
console.log(math.zero)
alert(math.add(1, 1));
})
// math.js
// 定义math.js模块,依赖zero模块
define(['./zero'], function (zero) {
var add = function (x, y) {
return x + y;
};
return {
add: add,
zero: zero
};
});
// zero.js
define(function () {
return 0;
})
CMD
CMD(Common Module Definition - 通用模块定义)
规范主要是Sea.js
推广中形成的,一个文件就是一个模块,可以像Node.js一般书写模块代码。主要在浏览器中运行,当然也可以在Node.js中运行。
与AMD
类似,不同点在于:
- AMD 推崇依赖前置、提前执行
- CMD推崇依赖就近、延迟执行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<span>body</span>
<!-- CMD -->
<script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>
<script>
seajs.use('./main.js')
</script>
</body>
</html>
// main.js
define(function (require, exports, module) {
var a = require('./CMD/a'); //在需要时申明
console.log(a.getHello());
var b = require('./CMD/b'); //在需要时申明
console.log(b.getHello());
});
// a.js
define(function (require, exports, module) {
console.log('a.js');
exports.getHello = function () {
return 'a';
}
});
// b.js
define(function (require, exports, module) {
console.log('b.js');
exports.getHello = function () {
return 'b';
}
});
UMD
一种兼容cjs
和amd
的模块,既可以在 node
环境中被 require
引用,也可以在浏览器中直接用 CDN 被 script.src
引入。
- 判断
define
是否函数,并且是否存在define.amd
,来判断是否为AMD规范 - 判断
module
是否为一个对象,并且是否存在module.exports
来判断是否为CommonJS
规范 - 如果以上两种都没有,设定为原始的代码规范。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- UMD -->
<script src="./UMD/math.js"></script>
<script>
console.log(math.add(1,1));
</script>
</body>
</html>
(function (global, factory) {
if (typeof define === "function" && define.amd) {
// AMD
define(["xxx"], factory);
} else if (typeof exports === "object" && typeof module !== 'undefined') {
// CommonJS
module.exports = factory(require("xxx"));
} else {
// 全局变量
global.math = factory(global["xxx"]);
}
})(this, function () {
return {
add: (x, y) => {
return x + y;
}
}
});
esm(ES Module)
esm
是tc39
对于ESMAScript
的模块化规范,正因是语言层规范,因此在 Node 及 浏览器中均会支持。
tc39
是一个维护和发展 JavaScript的组织。ESMAScript
是JavaScript
的标准,JavaScript
是基于ESMAScript
的语言。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<span>body</span>
<!-- ES Module -->
<script src="./main.js" type="module"></script>
</body>
</html>
// main.js
import { add } from './ESModule/math.js'
console.log(add(1, 1));
// math.js
export const add = (x, y) => x + y;
模块的导入导出,通过
import
和export
来确定。 可以和Commonjs模块混合使用。esm
是静态导入,所以在编译期间进行Tree Shaking
,减少js
体积。同时,tc39也为动态加载模块定义了API:
import(module)
,该方法返回的是一个Promise
对象,可以支持按需加载,大大提高了模块引用的灵活性。import
命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。export
命令会有变量声明提前的效果。
import 优先执行:
// a.js
console.log('a.js')
import { age } from './b.js';
// b.js
export let age = 1;
console.log('b.js 先执行');
// 执行结果:
// b.js 先执行
// a.js
export 变量声明提升:
// a.js
import { foo } from './b.js';
console.log('a.js,b的foo:',foo);
export const bar = 1;
export const bar2 = () => {
console.log('bar2');
}
export function bar3() {
console.log('bar3');
}
// b.js
export let foo = 1;
import * as a from './a.js';
console.log(a);
// node环境下输出结果
[Module: null prototype] {
bar: <uninitialized>,
bar2: <uninitialized>,
bar3: [Function: bar3]
}
a.js,b的foo: 1
esm是未来的趋势。目前一些CDN厂商、前端构建工具都有对cjs
模块向esm
模块转化的趋势,如skypack
,vite
等。
其他
ESModule和CommonJS的对比
- ES modules 输出的是值的引用,而 CommonJS 输出的是值的拷贝
- ES modules 模块编译时加载,而 CommonJS 模块总是在运行时加载
TreeShaking
- 没有使用额外的模块系统,直接定位import来替换export的模块
- 去掉了未被使用的代码
ES Modules 之所以能 Tree-shaking 主要为以下四个原因(摘自尤雨溪在知乎的回答):
import
只能作为模块顶层的语句出现,不能出现在 function 里面或是 if 里面。import
的模块名只能是字符串常量。- 不管
import
的语句出现的位置在哪里,在模块初始化的时候所有的import
都必须已经导入完成。 import binding
是immutable
的,类似 const。比如说你不能 import { a } from ‘./a’ 然后给 a 赋值。
副作用
// effect.js
console.log(unused());
export function unused() {
console.log(1);
}
// index.js
import {unused} from './effect';
console.log(42);
此例子中 console.log(unused());
就是副作用。在 index.js
中并不需要这一句 console.log
。而 rollup
并不知道这个全局的函数去除是否安全。因此在打包地时候你可以显示地指定treeshake.moduleSideEffects
为 false,可以显示地告诉 rollup
外部依赖项没有其他副作用。
不指定的情况下的打包输出。 npx rollup index.js --file bundle.js
console.log(unused());
function unused() {
console.log(1);
}
console.log(42);
指定没有副作用下的打包输出。npx rollup index.js --file bundle-no-effect.js --no-treeshake.moduleSideEffects
console.log(42);
当然以上只是副作用的一种,详情其他几种看查看 treeshake.moduleSideEffects
参考链接