一、 为什么需要模块化
以前没有模块化时,我们可能会按如下方式划分模块:
通过 <script> 标签引入各个文件,把每个文件看成是一个模块,每个模块的接口通常是暴露在全局作用域下的,也就是定义在 window 对象中。
如果通过这种方式做模块化,当项目变得越来越大时,很容易造成全局变量冲突,项目也会变得越来越难以管理。
因此我们需要找到一种合适的方式来帮助我们模块化,模块化能给我们带来哪些好处呢?具体来说,主要包含以下几个方面:
- 减少全局变量污染
- 控制依赖
- 增强代码的可维护性
- 增加代码的复用性
- 分治思想的实践
二、模块化标准现状
1. CommonJS
规范
- 模块的标识应遵循一定的书写规则。
- 定义全局函数
require(dependency)
,通过传入模块标识来引入其他依赖模块,执行的结果即为别的模块暴漏出来的 API。 - 如果被 require 函数引入的模块中也包含外部依赖,则依次加载这些依赖。
- 如果引入模块失败,那么 require 函数应该抛出一个异常。
- 模块通过变量 exports 来向外暴露 API,exports 只能是一个 Object 对象,暴露的 API 须作为该对象的属性。
使用方式
导出模块:
加载模块:
优点
- 简单易用。
- 解决了模块依赖的问题。
- 减少了全局变量污染。
缺点
- 无法在浏览器端使用。
- 无法非阻塞的并行加载多个模块。
2. AMD(Async Module Definition)
代表作 RequireJS。
规范
- 模块的标识遵循 CommonJS Module Identifiers。
- 定义全局函数
define(id, dependencies, factory)
,用于定义模块。dependencies 为依赖的模块数组,在 factory 中需传入形参与之一一对应。 - 如果 dependencies 的值中有 require、exports 或 module,则与 CommonJS 中的实现保持一致。
- 如果 dependencies 省略不写,则默认为 ['require', 'exports', 'module'],factory 中也会默认传入三者。
- 如果 factory 为函数,模块可以通过以下三种方式对外暴漏 API:return 任意类型;
exports.XModule = XModule、module.exports = XModule
。 - 如果 factory 为对象,则该对象即为模块的导出值。
使用方式
定义模块:
- 独立模块
- 有依赖的模块
define(['module1', 'module2'], function(m1, m2) {
...
});
// module1模块和module2模块指的是,当前目录下的module1.js文件和module2.js文件,等同于写成['./module1', './module2']
需要注意的是,回调函数必须返回一个对象,这个对象就是你定义的模块。
调用模块:
优点
- 可以用于浏览器。
- 异步加载模块。
- 可以并行加载多个模块。
缺点
- 提高了开发成本。
- 不能按需加载,而是提前加载所有的依赖。
* RequireJS 从 2.0 开始,也改成了可以延迟执行。
3. CMD (Common Module Definition)
CMD 是 sea.js
在推广过程中对模块定义的规范化产出,属于 CommonJS 的一种规范。
使用方式
定义模块:
使用模块:
优点
- 实现了浏览器端的模块化加载。
- 可以按需加载。
- 依赖就近,延迟执行。
缺点
- 依赖 SPM 打包,模块加载逻辑偏重。
4. UMD (Universal Module Definition)
UMD 是一种 JavaScript 通用模块定义规范,让你的模块能在 JavaScript 所有运行环境中发挥作用。
规定如下:
- 优先判断是否存在 exports 方法,如果存在,则采用 CommonJS 方式加载模块;
- 其次判断是否存在 define 方法,如果存在,则采用 AMD 方式加载模块;
- 最后判断 global 对象上是否定义了所需依赖,如果存在,则直接使用;反之,则抛出异常。
5. ES Module
使用方式
导出模块:
引入模块:
优点
- 语法层面的支持,使用简单。
缺点
- 浏览器还没有完全兼容,必须通过工具转换成标准的 ES5 后才能正常运行。
浏览器的支持情况
三、模块化的演变历史
2009 年的时候,Mozilla 的工程师 Kevin Dangoor 与同事们一起制定了一套 JsaveScript 模块化标准,并取名为 ServerJS。ServerJS 最早是用于服务端的,目的是为了在自动化测试的工作中提供模块化导入的功能。之后 ServerJS 更名为了 CommonJS。
同年 9 月,Ryan Dahl 创造了 Node.js,而一开始 Node.js 还没有包管理工具,随后采用 CommonJS 规范的 npm (即 node package manager) 诞生了。随着 Node.js 的快速发展,CommonJS 规范也渐渐进入广大前端开发者的视野。至此,JavaScript 第一个模块化规范,也正式登入历史舞台。
随着 npm 的流行,广大前端开发者也希望引入这种模块化的方案到日常的开发工作中。但是 CommonJS 只能应用于服务端,因此势必需要重新制定规范标准。而此时,关于如何制定新的标准,主要有三大流派:
保守派
CommonJS 已经在服务端成功应用了,那么在浏览器加载模块前,我们先通过工具将模块转换成浏览器能够执行的代码就可以了。该想法与如今的 babel 等工具思路是相似的,通过将高版本的代码转换为低版本的代码,目的都是为了兼容。而 browserify 就是这一观点下的产物。
激进派
浏览器与服务端存在很大的差异,我们应该根据浏览器的特点,放弃 require 的方式而是使用回调的方式引入模块。将同步加载模块更改为异步加载模块。
中间派
CommonJS 中的 require 等规范还是有可取之处,在尽可能与现有的 CommonJS 规范保持一致的前提下,我们也可以引入一些好的特性,比如 exports 可以导出更多类型而不是局限于 Object。
各自进展
激进派
激进派的 James Burke 在 2009 年开发出了 RequireJS 模块加载器。2011 年,在 RequireJS 社区的基础上,诞生了 AMD(Async Module Definition)社区。AMD 是第一个支持浏览器端的 Javascript 模块化解决方案,RequireJS 迅速被广大开发者熟知和采用。
中间派
中间派的故事比较曲折。CommonJS 的主要贡献者之一 Wes Garland 给出了一个名为 BravoJS 的实现。Wes Garland 本人是学院派,理论功底十分强,但写出的作品却不很实用。另一位实战派大佬提出了 Modules/Wrappings 的方案,并给出了一个名为 FlyScript 的实现。而两位大佬对具体的实现发生了一些争论,最后以 FlyScript 的 GitHub 仓库被删除而结束这段恩怨。
2011 年 4 月,阿里巴巴的前端大佬玉伯,因为给 RequireJS 提出的建议被不断拒绝后,在参考了 AMD 和 CommonJS 的方案后,便自己写了一个模块加载器 Sea.JS,同时提出了 CMD 规范。CMD 规范的主要内容与 AMD 相似,但是保留了 CommonJS 中延迟加载和就近声明的特性。
UMD
2014 年 9 月,美籍华裔 Homa Wong 提交了 UMD 第一个版本的代码。UMD 即 Universal Module Definition 的缩写,它本质上并不是一个真正的模块化方案,而是将 CommonJS 和 AMD 相结合。
ES Module
2016 年 5 月,经过了两年的讨论,ECMAScript 6.0 终于正式通过决议,成为了国际标准。在这一标准中,首次引入了 import 和 export 两个 JavaScript 关键字,并提供了被称为 ES Module 的模块化方案。2017 年 9 月, Chrome 的 61.0 版本首次在浏览器端支持了 ES Module。目前已经有很多浏览器原生支持了 ES Module。而伴随着 ES Module 的兴起,也出现了像 Vite 这样的构建工具。