大家好,我是若川。最近组织了源码共读活动,感兴趣的可以点此加我微信ruochuan12 进群参与,每周大家一起学习200行左右的源码,共同进步。已进行三个月了,很多小伙伴表示收获颇丰。
一、 为什么需要模块化
以前没有模块化时,我们可能会按如下方式划分模块:
通过 <script> 标签引入各个文件,把每个文件看成是一个模块,每个模块的接口通常是暴露在全局作用域下的,也就是定义在 window 对象中。
<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="module3.js"></script>
如果通过这种方式做模块化,当项目变得越来越大时,很容易造成全局变量冲突,项目也会变得越来越难以管理。
因此我们需要找到一种合适的方式来帮助我们模块化,模块化能给我们带来哪些好处呢?具体来说,主要包含以下几个方面:
减少全局变量污染
控制依赖
增强代码的可维护性
增加代码的复用性
分治思想的实践
二、模块化标准现状
1. CommonJS
规范
模块的标识应遵循一定的书写规则。
定义全局函数
require(dependency)
,通过传入模块标识来引入其他依赖模块,执行的结果即为别的模块暴漏出来的 API。如果被 require 函数引入的模块中也包含外部依赖,则依次加载这些依赖。
如果引入模块失败,那么 require 函数应该抛出一个异常。
模块通过变量 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
优点
简单易用。
解决了模块依赖的问题。
减少了全局变量污染。
缺点
无法在浏览器端使用。
无法非阻塞的并行加载多个模块。
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({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();
});
优点
可以用于浏览器。
异步加载模块。
可以并行加载多个模块。
缺点
提高了开发成本。
不能按需加载,而是提前加载所有的依赖。
* 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);
});
优点
实现了浏览器端的模块化加载。
可以按需加载。
依赖就近,延迟执行。
缺点
依赖 SPM 打包,模块加载逻辑偏重。
4. UMD (Universal Module Definition)
UMD 是一种 JavaScript 通用模块定义规范,让你的模块能在 JavaScript 所有运行环境中发挥作用。
规定如下:
优先判断是否存在 exports 方法,如果存在,则采用 CommonJS 方式加载模块;
其次判断是否存在 define 方法,如果存在,则采用 AMD 方式加载模块;
最后判断 global 对象上是否定义了所需依赖,如果存在,则直接使用;反之,则抛出异常。
5. ES Module
使用方式
导出模块:
// 导出
exportfunction hello() { };
exportdefault {// ...
};
引入模块:
import { readFile } from'fs';
import React from'react';
优点
语法层面的支持,使用简单。
缺点
浏览器还没有完全兼容,必须通过工具转换成标准的 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 这样的构建工具。
最近组建了一个湖南人的前端交流群,如果你是湖南人可以加我微信 ruochuan12 私信 湖南 拉你进群。
推荐阅读
1个月,200+人,一起读了4周源码
我历时3年才写了10余篇源码文章,但收获了100w+阅读
老姚浅谈:怎么学JavaScript?
我在阿里招前端,该怎么帮你(可进面试群)
················· 若川简介 ·················
你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》10余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动,帮助1000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。
识别上方二维码加我微信、拉你进源码共读群
今日话题
略。分享、收藏、点赞、在看我的文章就是对我最大的支持~