背景
JavaScript在设计之初, 只是为了满足浏览器页面的简单交互, 十分简单且不完善.
后随着技术发展, 浏览器的能力也越来越强, Web来到了2.0时代, 基于Ajax
技术的前端功能逻辑越来越复杂.
这个时候JS的设计弊端就出现了, 因为没有模块化, 如何组织管理越来越多的代码, 变成了一个棘手的问题.
为此, 早期的前端开发者, 探索实践出了一些解决方案, 来更好的组织代码, 比如经典的 namespace
模式.
即把代码封装成方法, 然后统一储存在一个全局对象上.
// a.js
!(function() {
var myModule = {};
myModule.foo = function () {
console.log("foo")
};
window.myModule = myModule;
})();
// b.js
!(function() {
myModule.bar = function () {
console.log("bar");
}
})();
这样只要我们按照顺序加载 a.js
, b.js
那么就能得到一个完整的模块myModule以供使用.
:::info
上面的两个js文件中, 都使用了
!(function () { // your code })();
来包围主体代码, 这样主要是为了避免变量污染全局空间.
而这种模式, 也被称为IIFE(Immediately Invoked Function Expression).
更多的信息可以参考: IIFE—MDN
:::
然而这些”骚操作”, 都是从代码设计上来解决模块化的需求, 使用中也有种种约束和弊端. 比如不同的文件之间, 必须要通过全局变量来传递方法或者数据.
为此, JS社区里面的开发者们便一起集思广益, 探讨如何去设计一款真正意义上的模块化.
CommonJS 与 AMD, CMD
CommonJS
是由CommonJS
(最开始叫ServerJS
)社区提出的一种JS模块化方案, 在推出Modules/1.0
规范后. 经NodeJS采用并迅速发扬光大, CommonJS
的核心理念是:
- 一个文件就是一个模块, 每个模块都是一个独立的作用域, 意味着文件内的定义变量不能被其他模块读取, 除非或者由本模块输出, 或者定义为
global
的属性- 输出模块变量的方法是把所有需要输出的方法变量等, 都存放在一个对象上, 然后通过
module.exports
来输出.- 加载模块使用
require
,直接从文件读取其中module.exports
的对象, 然后使用其提供的方法或者属性.
由于CommonJS
设计之初是运行在服务器端, 服务器上执行JS的好处是可以直接同步的读取文件获得内容.
因此当社区决定将CommonJS
推广给Web端时, 发现由于浏览器执行JS前需要先加载, 因此不能像NodeJS
那样同步的加载模块直接使用.
为了解决加载的问题, 社区里面的人又展开了激烈的讨论, 其中有一个流派主张异步先加载模块, 加载完成的回调里面再使用, 于是就有了AMD(Asynchronous Module Definition)
规范, 其代表产物就是 requireJS
AMD
也是使用require
来加载模块, 区别在于异步使用, 即:
require(["aModule", "bModule"], function (moduleA, moduleB) {
// 使用moduleA 或者 moduleB
});
同时, 需要用define
来定义一个模块, 当需要依赖其他模块是, 需要指定依赖关系
define(["depModule"], function (depModule) {
return {
foo: function () {
depModule.xxx();
}
}
});
但是这样写, 终究是过于恶心了, 因此 AMD
规范也允许像 CommonJS
一样直接require
, 这个时候 define
的写法是这样的
define(function (require, exports) {
const depModule = require("depModule");
exports.foo = function () {
depModule.xxx();
}
});
虽然看着和CommonJS
一样, 但是只是换了种写法, 本质还是先加载解析模块再使用, 所有的模块都会提前加载解析.
:::info
我们的VanCharts
图表大概是在15年左右写的, 就是使用 requireJS
来组织模块化.
参考:
:::
AMD
解决了异步加载模块的问题, 而且还支持多个模块并行加载, 可以说是比较适合用来做浏览器端的模块化方案.
但是它的缺点也很明显, 即模块加载解析提前Early Executing
, 这样会有什么问题, 参考下面的代码
define(["aModule", function (moduleA) {
if (false) {
moduleA.xxx();
}
}];
这段代码里面, 其实moduleA
并没有用, 但是 AMD
下, 加载 aModule.js
后却还是解析了模块代码拿到了moduleA
.
对于一些有洁癖的程序员, 这种做法是不能接受的, 于是大家又开始给出其他的实现, 这其中做的比较好的就是 SeaJS
.在保证JS异步加载的同时又做到按需解析执行, 后来就演变成了CMD规范(Common Module Definition)
:::info
SeaJS的作者, 即玉伯. 现在的蚂蚁金服技术体验部的负责人, 也是antv的带头人之一.
语雀地址: https://www.yuque.com/yubo
:::
当然, SeaJS
也是有缺点的, 因为要做到同步解析, 那么最开始就需要把所有的模块对应的JS文件加载下来, 而怎么找到这些模块的JS, 就需要提前解析脚本手动收集依赖.
因此, SeaJS
对比requireJS
多了一个文件解析收集依赖的过程.
而且, 由于是同步解析模块, 所以只能串行操作, 不能像 requireJS
提前并行解析模块, 所以在运行速度上会略微慢些.
UMD模块
上面说了CommonJS
, AMD
与CMD
, 因实现有所不同, 所写的代码不能相互直接使用, 因此需要一个通用的规范来兼容他们, 即UMD
规范(Universal Module Definition
).
UMD的实现很简单, 即根据运行环境判断, 需要按照什么规范来暴露模块对象.
如果你经常使用一些打包好的第三方JS库, 那么你可能很熟悉下面的代码.
(function(root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
console.log('是commonjs模块规范,nodejs环境')
var depModule = require('./umd-module-depended')
module.exports = factory(depModule);
} else if (typeof define === 'function' && define.amd) {
console.log('是AMD模块规范,如require.js')
define(['depModule'], factory)
} else if (typeof define === 'function' && define.cmd) {
console.log('是CMD模块规范,如sea.js')
define(function(require, exports, module) {
var depModule = require('depModule')
module.exports = factory(depModule)
})
} else {
console.log('没有模块环境,直接挂载在全局对象上')
root.umdModule = factory(root.depModule);
}
}(this, function(depModule) {
console.log('我调用了依赖模块', depModule)
// ...省略了一些代码,去代码仓库看吧
return {
name: '我自己是一个umd模块'
}
}));
我们如果要写一些通用的工具或者框架, 最好也使用UMD的模块化规范, 这样能覆盖更多的使用场景.
好在现在的构建工具, 已经支持自动打包成umd
的形式了.
:::info
UMD的概念是在 CMD之前提出来的, 上面的代码是兼容CMD之后的UMD实现
:::
ESModule
上文说到的CommonJS 与 AMD, CMD.
都是在JS语言不够成熟时, 社区给的解决方案, 而随着时代的进步, JS的语言也在更新迭代.
在2015年, JS语言的规范, 即ECMAScript 6发布, 从语言层面实现了模块化的支持, 即ESModule.
:::info
对ES6不熟悉, 或者对于ECMAScript不熟悉的同学, 可以去看看阮一峰老师写的ES6教程.
网道: ES6教程
:::
总结
我们今天从前端模块化发展, 介绍了CommonJS
, AMD
与CMD
规范, 再到UMD
, ESModule
.
可见JS是一门很有活力的语言, 虽然前期有很多问题, 但是经过社区和众多开发者的努力, 逐渐被完善.
我们今天学习最新的语法, 用着最趁手的工具, 更应该饮水思源, 向那些为语言发展做出贡献的前辈们致以敬意,
Salute~.
参考资料:
SeaJS-issue: 前端模块化开发那点历史
segmentfault-前端模块化详解
CNBlogs: 前端模块化—-彻底搞懂AMD, CMD, ESM和CommonJS