第26章 模块
# 理解模块模式
模块的思想:把逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。
# 模块标识符
模块系统本质上是键/值实体,其中每个模块都有个可用于引用它的标识符。
标识符在模拟模块的系统中可能是字符串或模块文件的实际路径。
系统中的任何模块都应该能够无歧义地引用其他模块。
注:似乎指的就是命名空间。
# 模块依赖
模块系统运行时会检视依赖,进而保证这些外部模块能够被加载并在本地模块运行时初始化所有依赖。
# 模块加载
当一个外部模块被指定为依赖时,本地模块期望在执行它时,依赖已准备好并已初始化。
- 加载模块涉及执行其中的代码,但必须是在所有依赖都加载并执行之后。
- 如果浏览器没有收到依赖模块的代码,则必须发送请求并等待网络返回。
- 收到模块代码之后,浏览器必须确定刚收到的模块是否也有依赖。
- 然后递归地评估并加载所有依赖,直到所有依赖模块都加载完成。
- 只有整个依赖图都加载完成,才可以执行入口模块。
# 入口
相互依赖的模块必须指定一个模块作为入口(entry point),这也是代码执行的起点。因为js是从上到下依次执行并且是单线程的。
入口模块 A 必须在应用程序的其他部分加载后才能执行。
# 异步依赖
js可以异步执行,所以js可以按需加载。即让 JavaScript 通知模块系统在必要时加载新模块,并在模块加载完成后提供回调。好处就是提高性能,因为页面只需要同步加载一个文件。
# 动态依赖
有些模块系统则允许开发者在程序结构中动态添加依赖。
if (loadCondition) {
require('./moduleA');
}
2
3
# 静态分析
分析工具会检查代码结构并在不实际执行代码的情况下推断其行为。对静态分析友好的模块系统可以让模块打包系统更容易将代码处理为较少的文件。
# 循环依赖
包括 CommonJS、AMD 和ES6 在内的所有模块系统都支持循环依赖。
# 凑合的模块系统
ES6 之前的模块有时候会使用函数作用域和立即调用函数表达式(IIFE,Immediately Invoked Function Expression)将模块定义封装在匿名闭包中。
为了暴露公共 API,模块 IIFE 会返回一个对象,其属性就是模块命名空间中的公共成员:
var Foo = (function() {
return {
bar: 'baz',
baz: function() {
console.log(this.bar);
}
};
})();
console.log(Foo.bar); // 'baz'
Foo.baz(); // 'baz'
2
3
4
5
6
7
8
9
10
为了让模块正确使用外部的值,可以将它们作为参数传给 IIFE:
var globalBar = 'baz';
var Foo = (function(bar) {
return {
bar: bar,
baz: function() {
console.log(bar);
}
};
})(globalBar);
console.log(Foo.bar); // 'baz'
Foo.baz(); // 'baz'
2
3
4
5
6
7
8
9
10
11
必须手动管理依赖和排序。要添加异步加载和循环依赖非常困难。最后,对这样的系统进行静态分析也是个问题。
# ES6之前的模块加载器
# CommonJS
CommonJS 规范概述了同步声明依赖的模块定义。CommonJS 模块语法不能在浏览器中直接运行。
CommonJS 模块定义需要使用 require()指定依赖,而使用 exports 对象定义自己的公共 API。
var moduleB = require('./moduleB');
module.exports = {
stuff: moduleB.doStuff();
};
2
3
4
该模块引入了模块moduleB,并导出模块对象,拥有stuff属性。注意模块不一定要赋值到变量,可以直接引入。
无论一个模块在 require()中被引用多少次,模块永远是单例。
console.log('moduleA');
var a1 = require('./moduleA');
var a2 = require('./moduleA');
console.log(a1 === a2); // true
2
3
4
模块加载是模块系统执行的同步操作。
console.log('moduleA');
if (loadCondition) {
require('./moduleA');
}
2
3
4
moduleA 只会在 loadCondition 求值为 true 时才会加载。
module.exports 对象非常灵活,有多种使用方式。如果只想导出一个实体,可以直接给 module.
exports 赋值:
module.exports = 'foo';
这样,整个模块就导出一个字符串,可以像下面这样使用:
var moduleA = require('./moduleB');
console.log(moduleB); // 'foo'
2
也可以导出多个值。
module.exports = {
a: 'A',
b: 'B'
};
// 或
module.exports.a = 'A';
module.exports.b = 'B';
2
3
4
5
6
7
# 异步模块定义(AMD)
CommonJS 以服务器端为目标环境,能够一次性把所有模块都加载到内存,
而异步模块定义(AMD,Asynchronous Module Definition)的模块定义系统则以浏览器为目标执行环境。让模块声明自己的依赖,而运行在浏览器中的模块系统会按需获取依赖,并在依赖加载完成后立即执行依赖它们的模块。
// ID 为'moduleA'的模块定义。moduleA 依赖 moduleB,
// moduleB 会异步加载
define('moduleA', ['moduleB'], function(moduleB) {
return {
stuff: moduleB.doStuff();
};
});
2
3
4
5
6
7
# 通用模块定义(UMD)
为了统一 CommonJS 和 AMD 生态系统,通用模块定义(UMD,Universal Module Definition)规范应运而生。UMD 可用于创建这两个系统都可以使用的模块代码。
UMD 定义的模块会在启动时检测要使用哪个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式(IIFE)中。
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD。注册为匿名模块
define(['moduleB'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node。不支持严格 CommonJS
// 但可以在 Node 这样支持 module.exports 的
// 类 CommonJS 环境下使用
module.exports = factory(require(' moduleB '));
} else {
// 浏览器全局上下文(root 是 window)
root.returnExports = factory(root. moduleB);
}
}(this, function (moduleB) {
// 以某种方式使用 moduleB
// 将返回值作为模块的导出
// 这个例子返回了一个对象
// 但是模块也可以返回函数作为导出值
return {};
}));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
不应该期望手写这个包装函数,它应该由构建工具自动生成。
随着 ECMAScript 6 模块规范得到越来越广泛的支持,本节展示的模式最终会走向没落。
# 使用 ES6 模块
# 模块标签及定义
带有 type="module"属性的