背景
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
