历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby
的 require
、Python
的 import
,甚至就连 CSS
都有 @import
,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
Immediately Invoked Function Expression
在早期,JavaScript 程序本来很小,它们大多被用来执行独立的脚本任务,在 web 页面需要的地方提供一定交互,所以一般不需要多大的脚本。过了几年,我们现在有了运行大量 JavaScript 脚本的复杂程序,还有一些被用在其他环境。于是,一系列问题逐渐暴露出来:
- 全局变量灾难
- 函数命名冲突
- 复杂依赖关系
最初的模块定义是通过 IIFE
,将模块的代码包裹在一个函数中,这样模块的变量就无法暴露在全局中。曾经风靡的封装风格就是大名鼎鼎的 jQuery
,这种风格虽然灵活了些,但并未解决根本问题,所需依赖还是得外部提前提供,还是增加了全局变量。
- 如何安全的包装一个模块的代码,不污染模块外的任何代码?
- 如何唯一标识一个模块?
- 如何优雅的把模块的 API 暴露出去,不增加全局变量?
- 如何方便的使用所依赖的模块?
近年来,有必要开始考虑提供一种将 JavaScript 程序拆分为可按需导入的单独模块的机制。在 ES2015 之前,社区制定了一些模块加载方案,最主要的有 CommonJS
和 AMD
两种。前者用于服务器,后者用于浏览器。ES2015 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS
和 AMD
等规范,成为浏览器和服务器通用的模块解决方案。
CommonJS Modules/1.x
CommonJS
是一个标准化 Web 浏览器之外的 JavaScript 模块生态系统的项目。CommonJS
关于模块如何工作的规范如今广泛用于 Node.js 的服务器端 JavaScript。它也用于浏览器端 JavaScript,但该代码必须使用转译器打包,因为浏览器不支持 CommonJS
。CommonJS
可以通过使用 require()
函数和 module.exports
来识别。
js
// 导出模块
exports.fn = function() {}
module.exports = {
// The module code goes here
}
// 导入模块
const fn = require('./module')
模块标识符
- 模块标识符是由正斜杠分隔的"术语"字符串。
- 术语必须是驼峰命名法标识符、
.
或..
。 - 模块标识符可能没有像
.js
这样的文件扩展名。 - 模块标识符可以是"相对的"或"顶级的"。如果第一项是
.
或者..
则模块标识符是"相对的"。 - 顶级标识符是从概念模块名称空间根解析出来的。
- 相对标识符是相对于编写和调用
require
的模块的标识符来解析的。
模块上下文
-
在模块中,有一个自由变量
require
,即一个函数。require
函数接受模块标识符。require
返回外部模块导出的 API。- 如果存在依赖循环,外部模块可能在其传递依赖之一需要时尚未完成执行;在这种情况下,
require
返回的对象必须至少包含外部模块在调用require
导致当前模块执行之前准备好的导出。 - 如果无法返回请求的模块,
require
必须抛出错误。 require
函数可以具有main
属性。require
函数可以具有paths
属性,即到顶级模块目录的路径的优先级路径字符串数组,从高到低。
-
在模块中,有一个名为
exports
的自由变量,它是模块在执行时可以添加其 API 的对象- 模块必须使用
exports
对象作为唯一的导出方式。
- 模块必须使用
-
在一个模块中,必须有一个自由变量
module
,即一个Object
。- "模块"对象必须具有
id
属性,该属性是模块的顶级id
。id
属性必须使得require(module.id)
将返回module.id
源自的导出对象。 - "模块"对象可能有一个
uri
字符串,它是创建模块的资源的完全限定 URI。沙箱中不得存在uri
属性。
- "模块"对象必须具有
Asynchronous Module Definition
异步模块定义 (AMD
) API 指定了一种定义模块的机制,以便可以异步加载模块及其依赖项。这特别适合于同步加载模块会带来性能、可用性、调试和跨域访问问题的浏览器环境。AMD
的实施具有以下优势:
- 网站性能改进:
AMD
实现仅在需要时才加载较小的 JavaScript 文件。 - 更少的页面错误:
AMD
实现允许开发人员定义在执行模块之前必须加载的依赖项,因此模块不会尝试使用尚不可用的外部代码。
js
define('alpha', [
'require',
'exports',
'beta'
], function(require, exports, beta) {
// The module code goes here
})
API规范
该规范定义了单个函数"define",可用作自由变量或全局变量。函数的签名: define(id?, dependencies?, factory)
。
- 参数
id
是一个字符串文字。它指定正在定义的模块的id
。此参数是可选的,如果不存在,则模块id
应默认为加载程序为给定响应脚本请求的模块的id
。如果存在,模块id
必须是"顶级"或绝对id
(不允许相对id
)。
模块 id
可用于标识正在定义的模块,它们也可用于依赖项数组参数中。 AMD
中的模块 id
是 CommonJS
模块标识符中允许的超集。引用该页面:
- 模块标识符是由正斜杠分隔的"术语"字符串。
- 术语必须是驼峰命名法标识符、
.
或..
。 - 模块标识符可能没有像
.js
这样的文件扩展名。 - 模块标识符可以是"相对的"或"顶级的"。如果第一项是
.
或者..
,则模块标识符是"相对的"。 - 顶级标识符是从概念模块名称空间根解析出来的。
- 相对标识符是相对于编写和调用
require
的模块的标识符来解析的。
- 参数
dependency
是模块id
的数组文字,这些模块id
是正在定义的模块所需的依赖项。必须在执行模块工厂函数之前解析依赖项,并且解析后的值应作为参数传递给工厂函数,参数位置对应于依赖项数组中的索引。
依赖项 id
可以是相对 id
,并且应该相对于正在定义的模块进行解析。换句话说,相对 id
是相对于模块的 id
解析的,而不是相对于用于查找模块 id
的路径。
该规范定义了三个具有不同解析的特殊依赖项名称。如果 require
、exports
或 module
的值出现在依赖项列表中,则该参数应解析为 CommonJS
模块规范定义的相应自由变量。
依赖项参数是可选的。如果省略,则应默认为 ["require", "exports", "module"]
。但是,如果工厂函数的数量(长度属性)小于 3
,则加载程序可能会选择仅使用与函数的数量或长度对应的参数数量来调用工厂。
- 参数
factory
是一个应该执行以实例化模块或对象的函数。如果工厂是一个函数,它应该只执行一次。如果工厂参数是一个对象,则应将该对象指定为模块的导出值。如果工厂函数返回一个值(对象、函数或任何强制为true
的值),则应将该值指定为模块的导出值。
简化的 CommonJS 包装
如果省略依赖项参数,模块加载器可以选择以 require
语句的形式(字面意思是 require("module-id")
的形式)扫描工厂函数以查找依赖项。第一个参数必须按字面意思命名为 require
才能起作用。
在某些情况下,由于代码大小限制或函数缺乏 toString
支持(众所周知,Opera Mobile
缺乏对函数的 toString
支持),模块加载器可能会选择不扫描依赖项。
如果存在依赖项参数,则模块加载器不应扫描工厂函数内的依赖项。
define.amd 属性
为了清楚地表明全局定义函数(根据脚本 src
浏览器加载的需要)符合 ·,任何全局定义函数都应该有一个名为 amd
的属性,其值为一个对象。这有助于避免与可能定义不符合 AMD API
的 Define()
函数的任何其他现有 JavaScript 代码发生冲突。
此时,define.amd
对象内部的属性尚未指定。想要了解实现支持的基本 API 之外的其他功能的实现者可以使用它。
具有对象值的 define.amd
属性的存在表明与此 API 一致。如果有另一个版本的 API,它可能会定义另一个属性,例如 define.amd2
,以指示符合该版本的 API 的实现。
AMD
提供了一些 CommonJS
互操作性。它允许在代码中使用类似的 exports
和 require()
接口,尽管它自己的 define()
接口更加基础和首选。AMD
规范由 Dojo Toolkit
、RequireJS
和其他库实现。
Common Module Definition
通用模块规范(CMD
)解决了如何编写模块以便在基于浏览器的环境中进行互操作。言下之意,该规范定义了模块系统必须提供的最少功能,以支持可互操作的模块。
- 模块是单例的。
- 不应在模块范围内引入新的自由变量。
- 执行必须是惰性的。
js
define(function(require, exports, module) {
// The module code goes here
})
模块定义
模块是用 define
关键字定义的,它是一个函数:define(factory);
。
define
函数接受一个参数,即模块工厂。factory
可以是函数或其他有效值。- 如果
factory
是一个函数,则该函数的前三个参数(如果指定)必须按顺序为require
、exports
和module
。 - 如果
factory
不是函数,则模块的导出将设置为该对象。
模块上下文
在模块中,有三个自由变量: require
、 exports
和 module
。
require
函数require
是一个函数require
接受模块标识符。require
返回外部模块导出的 API。- 如果请求的模块无法返回,
require
应该返回null
。
require.async
是一个函数require.async
接受模块标识符列表和可选的回调函数。- 回调函数接收模块导出作为函数参数,其列出顺序与第一个参数中的顺序相同。
- 如果无法返回请求的模块,则回调应相应接收
null
。
exports
对象- 在模块中,有一个名为
exports
的自由变量,它是模块在执行时可以添加其 API 的对象。
- 在模块中,有一个名为
module
对象module.uri
:模块的完整解析uri
。module.dependencies
:模块所需的模块标识符列表。module.exports
:模块导出的 API。它与exports
对象相同。
模块标识符
- 模块标识符是并且必须是文字字符串。
- 模块标识符可能没有像
.js
这样的文件扩展名。 - 模块标识符应该是短划线连接的字符串。
- 模块标识符可以是相对路径。
Universal Module Definition
通用模块规范 (UMD
) 定义了 API 的设计和实现,这些模块能够在任何地方工作,无论是在客户端、服务器还是其他地方。UMD
模式通常尝试提供与当今最流行的脚本加载器的兼容性。在许多情况下,它使用 AMD
作为基础,并添加特殊外壳来处理 CommonJS
兼容性。
js
;(function (global, factory) {
if (typeof define === 'function' && define.amd) {
define(['module'], function (module) {
return (root.returnExportsGlobal = factory(module))
})
} else if (typeof exports === 'object' && module.exports) {
module.exports = factory(require('module'))
} else {
root.returnExportsGlobal = factory(root.module)
}
})(typeof self !== 'undefined' ? self : this, function (module) {
// The module code goes here
})
ECMAScript 2015 Modules
ES2015 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CJS
、 AMD
等规范,成为浏览器和服务器通用的模块解决方案。ESM
的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
js
// 导出模块
export function fn () {}
// 导出默认模块
export default {}
// 导入模块
import module, { fn } from 'module'
// 动态导入模块
import('module').then((module) => {
// Do something with the module.
})
.mjs 与 .js
V8 引擎推荐 .mjs
的做法:
- 比较清晰,这可以指出哪些文件是模块,哪些是常规的 JavaScript。
- 这能保证你的模块可以被运行时环境和构建工具识别,比如 Node.js 和 Babel。
注意:
- 一些工具不支持
.mjs
,比如 TypeScript。 <script type="module">
属性用于指示引入的模块。
CJS VS ESM
CJS
使用require('module')
语法导入其他模块,使用module.exports = stuff
;ESM
使用import { module } from 'module'
语法进行导入,使用export stuff
语法进行导出CJS
文件可以使用.cjs
扩展名告诉 Node 它们位于CJS
中;ESM
文件可以使用.mjs
扩展名告诉 Node 它们位于ESM
中CJS
导入是同步的;ESM
导入是异步的(这也允许顶级await
)CJS
在 Node 中工作,但在浏览器中不起作用;ESM
受所有现代浏览器和最新版本的 Node 支持,但在低于12
的 Node 版本中根本不起作用- 大量核心 JavaScript 生态系统工具是在 Node 中开发的,而 Node 最近才开始支持
ESM
,因此现有 Node 项目的很大一部分都是在CJS
中