一、 为什么需要模块化

以前没有模块化时,我们可能会按如下方式划分模块:
通过 <script> 标签引入各个文件,把每个文件看成是一个模块,每个模块的接口通常是暴露在全局作用域下的,也就是定义在 window 对象中。

<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="module3.js"></script>

如果通过这种方式做模块化,当项目变得越来越大时,很容易造成全局变量冲突,项目也会变得越来越难以管理。

因此我们需要找到一种合适的方式来帮助我们模块化,模块化能给我们带来哪些好处呢?具体来说,主要包含以下几个方面:

  1. 减少全局变量污染
  2. 控制依赖
  3. 增强代码的可维护性
  4. 增加代码的复用性
  5. 分治思想的实践


二、模块化标准现状

1. CommonJS

规范

  1. 模块的标识应遵循一定的书写规则。
  2. 定义全局函数​​require(dependency)​​,通过传入模块标识来引入其他依赖模块,执行的结果即为别的模块暴漏出来的 API。
  3. 如果被 require 函数引入的模块中也包含外部依赖,则依次加载这些依赖。
  4. 如果引入模块失败,那么 require 函数应该抛出一个异常。
  5. 模块通过变量 exports 来向外暴露 API,exports 只能是一个 Object 对象,暴露的 API 须作为该对象的属性。

使用方式

导出模块:

// math.js
var num = 0;
function add(a, b) {
return a + b;
}
module.exports = {
num: num,
add: add
}

加载模块:

// 引入自定义的模块时,参数包含路径,可省略.js
// 引入核心模块时,不需要带路径,如var http = require("http");
var math = require('./math');
math.add(1, 2) //3

优点

  1. 简单易用。
  2. 解决了模块依赖的问题。
  3. 减少了全局变量污染。

缺点

  1. 无法在浏览器端使用。
  2. 无法非阻塞的并行加载多个模块。

2. AMD(Async Module Definition)

代表作 RequireJS。

规范

  1. 模块的标识遵循 CommonJS Module Identifiers。
  2. 定义全局函数​​define(id, dependencies, factory)​​,用于定义模块。dependencies 为依赖的模块数组,在 factory 中需传入形参与之一一对应。
  3. 如果 dependencies 的值中有 require、exports 或 module,则与 CommonJS 中的实现保持一致。
  4. 如果 dependencies 省略不写,则默认为 ['require', 'exports', 'module'],factory 中也会默认传入三者。
  5. 如果 factory 为函数,模块可以通过以下三种方式对外暴漏 API:return 任意类型;​​exports.XModule = XModule、module.exports = XModule​​。
  6. 如果 factory 为对象,则该对象即为模块的导出值。

使用方式

定义模块

  • 独立模块
define({
method1: function() {},
method2: function() {},
});

// 函数的返回值就是输出的模块
define(function () {
return {
method1: function() {},
method2: function() {},
};
});
  • 有依赖的模块

​define(['module1', 'module2'], function(m1, m2) {
...
});
// module1模块和module2模块指的是,当前目录下的module1.js文件和module2.js文件,等同于写成['./module1', './module2']

需要注意的是,回调函数必须返回一个对象,这个对象就是你定义的模块。

调用模块

require(['foo', 'bar'], function ( foo, bar ) {
foo.doSomething();
});

优点

  1. 可以用于浏览器。
  2. 异步加载模块。
  3. 可以并行加载多个模块。

缺点

  1. 提高了开发成本。
  2. 不能按需加载,而是提前加载所有的依赖。

* RequireJS 从 2.0 开始,也改成了可以延迟执行。

3. CMD (Common Module Definition)

CMD 是 ​​sea.js​​ 在推广过程中对模块定义的规范化产出,属于 CommonJS 的一种规范。

使用方式

定义模块:

define(function (require, exports, module) {
var add = function (a, b) {
return a + b;
}
exports.add = add;
})

使用模块:

seajs.use(['math.js'], function (math) {
var sum = math.add(1, 2);
});

优点

  1. 实现了浏览器端的模块化加载。
  2. 可以按需加载。
  3. 依赖就近,延迟执行。

缺点

  1. 依赖 SPM 打包,模块加载逻辑偏重。

4. UMD (Universal Module Definition)

UMD 是一种 JavaScript 通用模块定义规范,让你的模块能在 JavaScript 所有运行环境中发挥作用。

规定如下:

  1. 优先判断是否存在 exports 方法,如果存在,则采用 CommonJS 方式加载模块;
  2. 其次判断是否存在 define 方法,如果存在,则采用 AMD 方式加载模块;
  3. 最后判断 global 对象上是否定义了所需依赖,如果存在,则直接使用;反之,则抛出异常。

5. ES Module

使用方式

导出模块:

// 导出
exportfunction hello() { };
exportdefault {
// ...
};

引入模块:

import { readFile } from'fs';
import React from'react';

优点

  1. 语法层面的支持,使用简单。

缺点

  1. 浏览器还没有完全兼容,必须通过工具转换成标准的 ES5 后才能正常运行。

浏览器的支持情况

【Web技术】1134- 写给前端新手看的一些模块化知识_加载


三、模块化的演变历史

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 这样的构建工具。


【Web技术】1134- 写给前端新手看的一些模块化知识_模块化_02