背景

JavaScript在设计之初, 只是为了满足浏览器页面的简单交互, 十分简单且不完善.
后随着技术发展, 浏览器的能力也越来越强, Web来到了2.0时代, 基于Ajax技术的前端功能逻辑越来越复杂.
这个时候JS的设计弊端就出现了, 因为没有模块化, 如何组织管理越来越多的代码, 变成了一个棘手的问题.
为此, 早期的前端开发者, 探索实践出了一些解决方案, 来更好的组织代码, 比如经典的 namespace模式.

即把代码封装成方法, 然后统一储存在一个全局对象上.

  1. // a.js
  2. !(function() {
  3. var myModule = {};
  4. myModule.foo = function () {
  5. console.log("foo")
  6. };
  7. window.myModule = myModule;
  8. })();
  9. // b.js
  10. !(function() {
  11. myModule.bar = function () {
  12. console.log("bar");
  13. }
  14. })();

这样只要我们按照顺序加载 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的核心理念是:

  1. 一个文件就是一个模块, 每个模块都是一个独立的作用域, 意味着文件内的定义变量不能被其他模块读取, 除非或者由本模块输出, 或者定义为global的属性
  2. 输出模块变量的方法是把所有需要输出的方法变量等, 都存放在一个对象上, 然后通过module.exports来输出.
  3. 加载模块使用 require,直接从文件读取其中 module.exports的对象, 然后使用其提供的方法或者属性.

由于CommonJS设计之初是运行在服务器端, 服务器上执行JS的好处是可以直接同步的读取文件获得内容.
因此当社区决定将CommonJS推广给Web端时, 发现由于浏览器执行JS前需要先加载, 因此不能像NodeJS那样同步的加载模块直接使用.

为了解决加载的问题, 社区里面的人又展开了激烈的讨论, 其中有一个流派主张异步先加载模块, 加载完成的回调里面再使用, 于是就有了AMD(Asynchronous Module Definition)规范, 其代表产物就是 requireJS

AMD也是使用require来加载模块, 区别在于异步使用, 即:

  1. require(["aModule", "bModule"], function (moduleA, moduleB) {
  2. // 使用moduleA 或者 moduleB
  3. });

同时, 需要用define来定义一个模块, 当需要依赖其他模块是, 需要指定依赖关系

  1. define(["depModule"], function (depModule) {
  2. return {
  3. foo: function () {
  4. depModule.xxx();
  5. }
  6. }
  7. });

但是这样写, 终究是过于恶心了, 因此 AMD规范也允许像 CommonJS一样直接require, 这个时候 define的写法是这样的

  1. define(function (require, exports) {
  2. const depModule = require("depModule");
  3. exports.foo = function () {
  4. depModule.xxx();
  5. }
  6. });

虽然看着和CommonJS一样, 但是只是换了种写法, 本质还是先加载解析模块再使用, 所有的模块都会提前加载解析. :::info 我们的VanCharts图表大概是在15年左右写的, 就是使用 requireJS来组织模块化.
参考: :::

AMD解决了异步加载模块的问题, 而且还支持多个模块并行加载, 可以说是比较适合用来做浏览器端的模块化方案.
但是它的缺点也很明显, 即模块加载解析提前Early Executing, 这样会有什么问题, 参考下面的代码

  1. define(["aModule", function (moduleA) {
  2. if (false) {
  3. moduleA.xxx();
  4. }
  5. }];

这段代码里面, 其实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, AMDCMD, 因实现有所不同, 所写的代码不能相互直接使用, 因此需要一个通用的规范来兼容他们, 即UMD规范(Universal Module Definition).

UMD的实现很简单, 即根据运行环境判断, 需要按照什么规范来暴露模块对象.
如果你经常使用一些打包好的第三方JS库, 那么你可能很熟悉下面的代码.

  1. (function(root, factory) {
  2. if (typeof module === 'object' && typeof module.exports === 'object') {
  3. console.log('是commonjs模块规范,nodejs环境')
  4. var depModule = require('./umd-module-depended')
  5. module.exports = factory(depModule);
  6. } else if (typeof define === 'function' && define.amd) {
  7. console.log('是AMD模块规范,如require.js')
  8. define(['depModule'], factory)
  9. } else if (typeof define === 'function' && define.cmd) {
  10. console.log('是CMD模块规范,如sea.js')
  11. define(function(require, exports, module) {
  12. var depModule = require('depModule')
  13. module.exports = factory(depModule)
  14. })
  15. } else {
  16. console.log('没有模块环境,直接挂载在全局对象上')
  17. root.umdModule = factory(root.depModule);
  18. }
  19. }(this, function(depModule) {
  20. console.log('我调用了依赖模块', depModule)
  21. // ...省略了一些代码,去代码仓库看吧
  22. return {
  23. name: '我自己是一个umd模块'
  24. }
  25. }));

我们如果要写一些通用的工具或者框架, 最好也使用UMD的模块化规范, 这样能覆盖更多的使用场景.
好在现在的构建工具, 已经支持自动打包成umd的形式了. :::info UMD的概念是在 CMD之前提出来的, 上面的代码是兼容CMD之后的UMD实现 :::

ESModule

上文说到的CommonJS 与 AMD, CMD.
都是在JS语言不够成熟时, 社区给的解决方案, 而随着时代的进步, JS的语言也在更新迭代.
在2015年, JS语言的规范, 即ECMAScript 6发布, 从语言层面实现了模块化的支持, 即ESModule. :::info 对ES6不熟悉, 或者对于ECMAScript不熟悉的同学, 可以去看看阮一峰老师写的ES6教程.
网道: ES6教程 :::

总结

我们今天从前端模块化发展, 介绍了CommonJS, AMDCMD规范, 再到UMD, ESModule.
可见JS是一门很有活力的语言, 虽然前期有很多问题, 但是经过社区和众多开发者的努力, 逐渐被完善.
我们今天学习最新的语法, 用着最趁手的工具, 更应该饮水思源, 向那些为语言发展做出贡献的前辈们致以敬意,
Salute~.

参考资料:

SeaJS-issue: 前端模块化开发那点历史
segmentfault-前端模块化详解
CNBlogs: 前端模块化—-彻底搞懂AMD, CMD, ESM和CommonJS

简书: 前端模块化(CommonJS, AMD和CMD)