Ch26 模块
26.1 理解模块
模块化的意思就是将代码拆分成独立的块,然后再把这些块连接起来。
26.1.1 模块标识符
模块系统本质上是键/值实体,每个模块都有个可用于引用它的标识符。这个标识符可能是模块文件的实际路径。
将模块标识符解析为实际模块的过程要根据模块系统对标识符的实现。原生浏览器模块标识符必须提供实际JavaScript文件的路径。除了文件路径,nodejs还会搜索node_modules某路,用标识符去匹配包含index.js的目录。
26.1.2 模块依赖
本地模块向系统模块声明一组外部模块,这些模块再运行程序时是必须的,模块系统检视这些依赖,进而保证这些外部模块能够被加载并在本地模块运行时初始化所有依赖。
26.1.3 模块加载
在浏览器中,加载模块涉及几个步骤。
- 加载模块涉及执行其中的代码,但必须是在所有依赖都加载并执行之后。
- 如果浏览器没有收到依赖模块的代码,则必须发送请求并等待网络返回。
- 收到模块代码之后,浏览器必须确定刚收到的模块是否也有依赖。
- 然后递归地评估并加载所有依赖,直到所有依赖模块都加载完成。
- 只有整个依赖图都加载完成,才可以执行入口模块。
26.1.4 入口
相互依赖的模块必须指定一个模块作为入口(entry point),这也是代码执行的起点。入口模块也可能依 赖其他模块,其他模块同样可能有自己的依赖。
26.1.5 异步加载
因为 JavaScript 可以异步执行,所以如果能按需加载就好了。换句话说,可以让 JavaScript 通知模块系统在必要时加载新模块,并在模块加载完成后提供回调。
// 在模块 A 里面
load('moduleB').then(function(moduleB) {
moduleB.doStuff();
});
26.1.6 动态加载
有些模块系统要求开发者在模块开始列出所有依赖,而有些模块系统则允许开发者在程序结构中动态添加依赖。动态添加的依赖有别于模块开头列出的常规依赖,这些依赖必须在模块执行前加载完毕。
if (loadCondition) {
require('./moduleA');
}
动态依赖可以支持更复杂的依赖关系,但代价是增加了对模块进行静态分析的难度。
26.1.8 循环依赖
要构建一个没有循环依赖的 JavaScript 应用程序几乎是不可能的,因此包括 CommonJS、AMD 和 ES6 在内的所有模块系统都支持循环依赖。
在包含循环依赖的应用程序中,模块加载顺序可能会出人意料。不过,只要恰当地封装模块,使它们没有副作用,加载顺序就应该不会影响应用程序的运行。
26.2 凑合的模块系统
使用函数作用域和立即调用函数表达式将模块定义在匿名闭包中。
// 扩展 Foo 以增加新方法
var Foo = (function(FooModule) {
FooModule.baz = function() {
console.log(FooModule.bar);
}
return FooModule;
})(Foo || {});
// 扩展 Foo 以增加新数据
var Foo = (function(FooModule) {
FooModule.bar = 'baz';
return FooModule;
})(Foo || {});
console.log(Foo.bar); // 'baz'
Foo.baz(); // 'baz'
26.3 ES6之前的模块加载器
26.3.1 CommonJs
CommonJS 规范概述了同步声明依赖的模块定义。这个规范主要用于在服务器端实现模块化代码组织,但也可用于定义在浏览器中使用的模块依赖。CommonJS 模块语法不能在浏览器中直接运行。
CommonJS 模块定义需要使用 require()指定依赖,而使用exports对象定义自己的公共 API。如下例:
var moduleB = require('./moduleB');
module.exports = {
stuff: moduleB.doStuff();
}
无论一个模块在 require()中被引用多少次,模块永远是单例。在下面的例子中,moduleA 只会 被打印一次。这是因为无论请求多少次,moduleA 只会被加载一次。
var a1 = require('./moduleA');
var a2 = require('./moduleA');
console.log(a1 === a2); // true
在 CommonJS 中,模块加载是模块系统执行的同步操作。因此 require()可以像下面这样以编程 方式嵌入在模块中:
if (loadCondition) {
require('./moduleA');
}
这里,moduleA 只会在 loadCondition 求值为 true 时才会加载。这个加载是同步的,因此 if()块之后的任何代码都会在加载 moduleA 之后执行。
CommonJS 依赖几个全局属性如 require 和 module.exports。如果想在浏览器中使用 CommonJS 模块,就需要与其非原生的模块语法之间构筑“桥梁”。
26.3.2 AMD异步模块定义
CommonJS 以服务器端为目标环境,能够一次性把所有模块都加载到内存,而异步模块定义(AMD, Asynchronous Module Definition)的模块定义系统则以浏览器为目标执行环境,这需要考虑网络延迟的问题。
AMD 的一般策略是让模块声明自己的依赖,而运行在浏览器中的模块系统会按需获取依赖,并在依赖加载完成后立即执行依赖它们的模块。
26.3.3 UMD通用模块定义
为了统一 CommonJS 和 AMD 生态系统,通用模块定义(UMD,Universal Module Definition)规范应运而生。
UMD 可用于创建这两个系统都可以使用的模块代码。本质上,UMD 定义的模块会在启动时检测要使用哪个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式中。
26.3.4 模块加载器没落
随着 ECMAScript 6 模块规范得到越来越广泛的支持,本节展示的模式最终会走向没落。RIP...
26.4 ES6模块
从很多方面看,ES6 模块系统是集 AMD 和 CommonJS 之大成者。
26.4.1 模块标签以及定义
ECMAScript 6 模块是作为一整块 JavaScript 代码而存在的。
<script type="module" src="path/to/myModule.js"></script>
上面这个type为module的script标签会告诉浏览器相关代码应该作为模块执行,而不是作为传统的脚本执行。所有模块都会像<script defer>加载的脚本一样按顺序执行。解析到标签后会立即下载模块文件,但执行会延迟到文档解析完成。
和CommonJS相同,同一个模块无论在一个页面中被加载多少次,也不管它是如何加载的,实际上都只会加载一次。
26.4.2 模块加载
ECMAScript 6 模块的独特之处在于,既可以通过浏览器原生加载,也可以与第三方加载器和构建工具一起加载。有些浏览器还没有原生支持 ES6 模块,因此可能还需要第三方工具。事实上,很多时候使用第三方工具可能会更方便。
26.4.4 模块导出
ES6 模块的公共导出系统与 CommonJS 非常相似。控制模块的哪些部分对外部可见的是 export 关 键字。ES6 模块支持两种导出:
- 命名导出
- 默认导出:default关键字,每个模块只能有一个默认导出
export关键字必须在模块顶级,不能嵌套在某个块中:
// allowed
export ....
// not allowed
if(condition){
export ...
}
导出时也可以提供别名,别名必须在 export 子句的大括号语法中指定。
const foo = 'foo';
export { foo as myFoo };
26.4.5 模块导入
模块可以通过使用 import 关键字使用其他模块导出的值。与 export 类似,import 必须出现在 模块的顶级:
// 允许
import ...
// 不允许
if (condition) {
import ...
}
导入对模块而言是只读的。直接修改导出的值是不可能的,但可以修改导出对象的属性。同样,也不能给导出的集合添加或删除导出的属性。要修改导出的值,必须使用有内部变量和属性访问权限的导出方法。