将代码拆分成独立的块,然后再将这些块连接起来可以通过模块模式来实现。这种模式背后的思想很简单:把逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。
模块系统本质上是键/值实体,其中每个模块都有个可用于引用它的标识符。这个标识符在模拟模块的系统中可能是字符串,在原生实现的模块系统中可能是模块文件的实际路径
每个模块都会与某一个唯一的标识符关联,该标识符可用于检索模块。这个标识符通常是 JavaScript文件的路径,但在某些模块系统中,这个标识符也可以是在模块本身内部声明的命名空间路径字符串。
相互依赖的模块必须指定一个模块作为入口,这也是代码执行的起点。
if(loadCondition) {
require('./moduleA');
}
在这个模块中,是否加载 moduleA 是运行时确定的。
动态依赖可以支持更复杂的依赖关系,但代价是增加了对模块进行静态分析的难度。
分析工具会检查代码结构并在不实际执行代码的情况下推断其行为。
加载器回执行深度优先的依赖加载:
在ES6原生支持模块之前,使用模块的JavaScript代码本质上是希望使用默认没有的语言特性。因此,必须按照符合某种规范的模块语法来编写代码,另外还需要单独的模块工具把这些模块语法与JavaScript运行时连接起来。这里的模块语法和连接方式有不同的表现形式,通常需要在浏览器中额外加载库或者在构建时完成预处理。
CommonJS规范概述了同步声明依赖的模块定义。这个规范主要用于在服务器端实现模块化代码组织,但也可用于定义在浏览器中使用的模块依赖。CommonJS模块语法不能直接在浏览器中运行
注意 一般认为,Node.js的模块系统使用了CommonJS规范,实际上并不完全正确。Node.js使用了轻微修改版本的CommonJS,因为Node.js主要在服务器环境下使用,所以不需要考虑网络延迟问题。考虑到一致性,本节使用Node.js风格的模块定义语法。
CommonJS模块定义需要使用require()指定依赖,而使用exports对象定义自己的公共API。
var moduleB = require('./moduleB');
module.exports = {
stuff: moduleB.doStuff();
}
无论一个模块在 require() 中被引用多少次,模块永远是单例
console.log('moduleA');
var a1 = require('./moduleA');
var a2 = require('./moduleA');
console.log(a1===a2); // true
模块第一次加载后会被缓存,后续加载会取得缓存的模块。模块的加载顺序由依赖图决定。
在 CommonJS 中,模块加载时模块系统执行的同步操作。因此 require()可以像下面这样以编程方式嵌入在模块中:
console.log('moduleA');
if (loadCondition) {
require('./moduleA');
}
如果moduleA已经在前面某个地方加载过了,这个条件 require()就意味着只暴露moduleA这个命名空间而已。
CommonJS以服务端为目标环境,能够一次性把所有模块都加载到内存,而异步模块定义(AMD Asynchronous Module Definition)的模块定义系统则以浏览器为目标执行环境,这需要考虑网络延迟的问题。AMD的一般策略是让模块声明自己的依赖,而运行在浏览器中的模块系统会按需获取依赖,并在依赖加载完成后立即执行依赖它们的模块。
AMD模块实现的核心是用函数包装模块定义。这样可以防止声明全局变量,并允许加载器库控制何时加载模块。包装函数也便于模块代码的移植,因为包装函数内部的所有模块代码使用的都是原生JavaScript结构。包装模块的函数是全局 define 的参数,它是由AMD加载器库的实现定义的
AMD模块可以使用字符串标识符指定自己的依赖,而AMD支持可选地为模块指定字符串标识符。
// ID 为‘moduleA’的模块定义。moduleA依赖moduleB,
// moduleB会异步加载
define('moduleA', ['moduleB'], function(moduleB) {
return {
stuff: moduleB.duStuff();
}
})
AMD也支持require和exports对象,通过他们的可以在AMD模块工厂函数内部定义CommonJS风格的模块。这样可以像请求模块一样请求它们,但AMD加载器会将它们识别为原生AMD结构,而不是模块定义
define('moduleA', ['require'], function(require){
if(condition) {
var moduleB = require('moduleB');
}
})
为了统一CommonJS和AMD生态系统,通用模块定义(UMD,Universal Module Definition)规范应运而生。UMD可用于创建这两个系统都可以使用的模块代码。本质上,UMD定义的模块会在启动时检测要使用哪个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式中。
下面是只包含一个依赖的UMD模块定义的示例:
(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 {};
}));
随着ECMAScript 6模块规范得到越来越广泛的支持,上述展示的模式终会走向没落
ES6模块最大的一个改进就是引入了模块规范。这个规范全方位简化了之前出现的模块加载器,原生浏览器支持意味着加载器以及其他预处理都不再必要。从很多方面看,ES6模块系统是集AMD和CommonJS之大成者。
ECMAScript 6模块是作为一整块JavaScript代码而存在的。带有type="module"属性的
完全支持 ECMAScript 6模块的浏览器可以从顶级模块加载整个依赖图,且是异步完成的。浏览器会解析入口模块,确定依赖,并发送对依赖模块的请求。这些文件通过网络返回之后,浏览器就会解析它们的内容,确定它们的依赖,如果这些二级依赖还没有加载,则会发送更多请求。这个异步递归加载过程会持续到整个应用程序的依赖图都解析完成。解析完依赖图,应用程序就可以正式加载模块了。
这个过程与AMD风格的模块加载非常相似。模块文件按需加载,且后续模块的请求会因为这个依赖模块的网络延迟而同步延迟。
ECMAScript 6模块借用了CommonJS和AMD的很多优秀特性。如:
ES6模块支持两种导出:命名导出和默认导出。
const foo = 'foo';
export { foo };
export const foo = 'foo';
export { foo };
const foo = 'foo';
命名导出就好像模块是被导出值的容器。
1. 行内命名导出
export const foo = 'foo';
const foo = 'foo';
export { foo };
2. 导出时可以提供别名,别名必须在export子句的大括号语法中指定。
const foo = 'foo';
export { foo as myFoo };
3. 导出声明分组
const foo = 'foo';
const bar = 'bar';
const baz = 'baz';
export { foo, bar as myBar, baz};
默认导出就好像模块与被导出的值是一回事。默认导出使用 default 关键字将一个值声明为默认导出,每个模块只能有一个默认导出
export default foo
export default function() {}
export default function foo() {}
export default function*() {}
export default class {}
模块可以通过使用import关键字使用其他模块导出的值。必须出现在模块的顶级
const foo = 'foo', bar = 'bar', baz = 'baz'
export {foo, bar, baz}
import * as Foo form './foo.js'
console.log(Foo.foo); // foo
import { foo, bar, baz as myBaz } from './foo.js';
import { default as foo } from './foo.js';
import foo from './foo.js'
模块导入的值可以直接通过管道转移到导出。此时,也可以将默认导出转换为命名导出,或者相反。如果想把一个模块的所有命名导出集中在一块,可以像下面这样在bar.js中使用*导出:
export * from './foo.js'
这样,foo.js中的所有命名导出都会出现在导入bar.js的模块中。如果foo.js有默认导出,则该语法会忽略它。