虽然对模块化这些规范很熟悉,因为频繁遇到所以还是重新梳理一遍,有疑问的地方做个实践论证。重点研究分析EsModule和cjs,cmd、amd、umd则会只研究基本概念和常用方案。

ES Module(重点)

  • 旨在成为浏览器和服务器通用的模块化解决方案,模块功能主要由两个命令构成:exportimport
  • import命令会被 JavaScript 引擎静态分析。当遇到模块加载命令import,会生成一个只读引用(ES6的模块不是对象而是值的引用),等运行时,再根据这个只读引用去被加载的模块取值。所以编译时候就引入了模块代码,而非代码运行时引入,所以无法实现执行条件加载或者字符串拼接路径之类的,这种加载称为编译时加载
  • ESM的模块输出的是值的引用而非拷贝,所以内部改变变量会改变export出的值。

与CommonJs的引入关系

ES Module import也可以引入Commonjs的模块,模块得使用Commonjs规范编写,后缀名为.cjs或者模块自身的package.json中设置

1
"type": "commonJs”

在Node环境中支持情况

Node版本 >=13.2.0 直接支持

Node版本 <13.2.0 如12的版本添加--experimental-module ,低于12版本的则不支持

Node环境中的加载支持都需要用.mjs扩展后缀或者在package.json中设置

1
"type": "module"

在浏览器环境支持情况

  • 安全策略更严格,非同域脚本的加载受 CORS 策略限制
  • 服务器端提供 ES Module 资源时,必须返回有效的属于 JavaScript 类型的 Content-Type 头如text/javascript

目前主流浏览器都已经对于ES Module支持。 给 script 标签添加 type=module 属性,就可以让浏览器以 ES Module 的方式加载脚本。

1
2
3
4
<script type=module src='x' />
<script type=module>
// do something
</script>

针对非内联script的deferasync,无论在什么位置都不会阻塞HTML的解析,它们的区别是async是下载完立刻执行不会按照脚本在页面上的顺序,defer是按脚本的顺序执行。内联script的deferasync不生效。

module的script下载和执行同样不阻塞HTML解析器,无论内联还是外联,模块之间是按照顺序执行的。给 script 标签显式指定 async defer 行为属性。但添加defer并没有意义。

1
2
3
4
<script type=module src='x' async ></script>
<script type=module async>
// do something
</script>

远程 script 根据 URL 作为判断唯一性的 Key,决定进行一次还是多次执行。URL 是同一路径下的模块多次加载只会进行一次执行,div#counterdata-count属性值是1

1
2
3
4
// a.js
const el = document.getElementById('counter');
const countNum = parseInt(el.dataset.count.trim() || 0, 10);
el.dataset.count = countNum++;
1
2
3
<script type=module src='./a.js' async ></script>
<script type=module src='./a.js' async ></script>
<script type=module src='./a.js' async ></script>

ES Module循环引用的问题

ES6模块不会缓存运行结果,而是动态地去被加载的模块取值,以及变量总是绑定其所在的模块。ES6根本不会关心是否发生了”循环加载”,只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

注意!! ES Module循环引用一定要有跳出机制,不然就会堆栈溢出,像下面这样。

1
2
3
4
5
6
7
8
9
10
11
RangeError: Maximum call stack size exceeded
at afn (file:///Users/aszero/sourcecode/lab/modules/a.mjs:3:20)
at bfn (file:///Users/aszero/sourcecode/lab/modules/b.mjs:4:3)
at afn (file:///Users/aszero/sourcecode/lab/modules/a.mjs:4:3)
at bfn (file:///Users/aszero/sourcecode/lab/modules/b.mjs:4:3)
at afn (file:///Users/aszero/sourcecode/lab/modules/a.mjs:4:3)
at bfn (file:///Users/aszero/sourcecode/lab/modules/b.mjs:4:3)
at afn (file:///Users/aszero/sourcecode/lab/modules/a.mjs:4:3)
at bfn (file:///Users/aszero/sourcecode/lab/modules/b.mjs:4:3)
at afn (file:///Users/aszero/sourcecode/lab/modules/a.mjs:4:3)
at bfn (file:///Users/aszero/sourcecode/lab/modules/b.mjs:4:3)

举个循环应用的例子

1
2
3
4
5
6
7
8
// a.mjs
import { bfn } from './b.mjs'
export function afn () {
console.log('afn执行,开始调用bfn')
bfn()
console.log('afn执行完毕')
}
afn()
1
2
3
4
5
6
7
8
9
10
11
// b.mjs
import { afn } from './a.mjs'
export function bfn () {
const randomNum = Math.random()
console.log(randomNum > 0.5 ? 'bfn进入条件' : 'bfn返回空')
if (randomNum > 0.5) { // 随机跳出机制
console.log('bfn执行,开始调用afn')
afn()
console.log('bfn执行完毕')
}
}

上面的随机数就是循环引用的跳出机制

执行

1
$ node a.mjs

打印的结果有很多种可能性,因为是根据随机数的条件随机退出的,我们取一组数据看一下运行流程

1
2
3
4
5
6
7
8
afn执行,开始调用bfn // a
bfn进入条件 // a - b
bfn执行,开始调用afn // a - b
afn执行,开始调用bfn // a - b - a
bfn返回空 // a - b - a - b 退出机制返回空
afn执行完毕 // a - b - a
bfn执行完毕 // a - b
afn执行完毕 // a

上面可以看出ES module执行的时候是忽略import加载,模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者保证真正取值时能够取到值,只要引用是存在的,代码就能执行。

上面的字母表示了执行的流程,分析如下,首先执行afn (链路a),调用bfn,进入bfn(链路a-b)如果满足条件进入循环引用调用afn,此时回到afn(链路a-b-a)再调用bfn,进入bfn(链路a-b-a-b)不满足进入循环引用的条件退出循环,返回空,那么上一层afn(链路a-b-a)继续执行完毕,再上一层引用这个afn的bfn(链路a-b)也执行完毕,同时引用这个bfnafn(链路a)也执行完毕。

CommonJs (重点)

  • 同步加载机制,常见于NodeJs。在Node环境使用moduleexportsrequirefilenamedirname提供模块化支持。
  • CommonJS 的模块输出的是值的浅拷贝,内部改变变量不会改变exports出的值。模块就是对象(即module.exports属性),等到运行时才把模块挂载在exports之上,加载模块其实就是查找对象属性,这种加载称为运行时加载。node中模块导入require是一个内置的函数,因此只有在运行后我们才可以得知模块导出内容,无法做静态分析。
  • 同步加载,代码在本地,加载时间基本等于硬盘读取时间。浏览器环境不支持CommonJS。
1
2
3
(function(exports, require, module, __filename, __dirname) {
// code
});

exports vs module.exports

  • module.exports 默认值为{}
  • exports 是 module.exports 的引用
  • exports 默认指向 module.exports 的内存空间
  • require() 返回的是 module.exports 而不是 exports
  • 若对 exports 重新赋值,则断开了 exports 对 module.exports 的指向

commonJS循环引用的问题

CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。CommonJS的做法是,一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。 下面我做了个图方便理解,图中序号是执行顺序。

例如 a.js引用b.js,b.js中也引用了a.js,那么a.js代码在执行到引用b.js的时候require(b.js)(图中流程2节点),停止执行下面的代码去加载b.js并执行,在b.js中执行到又引用a.js的时候require(a.js)(图中流程4节点),这时候a.js已经执行了多少输出b.js就引入多少(图中流程1节点输出),b.js保持继续执行直到执行完毕(图中流程5节点),这时候就回到a.js(图中流程6节点)加载执行b.js中断执行的地方继续执行,这时候也可以用b.js的输出值(图中流程5节点输出)。

AMD

  • 异步加载机制,模块的加载不影响后面语句的执行。依赖部分会在执行完成之后的回调中定义。
  • AMD举例require.js实现AMD规范的模块化:用require.config()指定引用路径等,用define()定义模块,用require()加载模块。
  • AMD/CMD是CommonJS在浏览器端的解决方案。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 网页中引入require.js及main.js
<script src="js/require.js" data-main="js/main"></script>

// config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 引用模块
require(["jquery","underscore"],function($,_){
// some code here
});
// 定义的模块本身如果也需要依赖
define(['underscore'],function(_){
// some code here
})

CMD

CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。CMD举例sea.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
a.doSomething();
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.doSomething()
}
});

/** CMD写法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var add = function(a,b){
return a+b;
}
exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
var sum = math.add(1+2);
});

UMD

是AMD和CommonJS的糅合,跨平台的解决方案。

AMD 模块以浏览器第一的原则发展,异步加载模块。 CommonJS 模块以服务器第一原则发展,选择同步加载。它的模块无需包装(unwrapped modules)。 这迫使人们又想出另一个更通用的模式 UMD(Universal Module Definition),实现跨平台的解决方案。UMD 先判断支持 Node.js 的模块(exports)是否存在,存在则使用 Node.js 模块模式。再判断是否支持 AMDdefine 是否存在),存在则使用 AMD 方式加载模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function (window, factory) {
if (typeof exports === 'object') {

module.exports = factory();
} else if (typeof define === 'function' && define.amd) {

define(factory);
} else {

window.eventUtil = factory();
}
})(this, function () {
//module ...
});

原创内容,欢迎交流转载请注明出处