前端模块化进化史
概述
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。譬如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 、AMD 和 CMD 两种。CommonJS 用于服务器,AMD 和 CMD 用于浏览器,对应的实践分别为requireJs和seaJs。
CommonJS
规范
CommonJS也可以说是NodeJS的模块化规范,他是随着nodejs的出现而被制定的,
Modules/1.0规范包含以下内容:
1. 模块的标识应遵循的规则(书写规范)
2. 定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为别的模块暴漏出来的API
3. 如果被require函数引入的模块中也包含依赖,那么依次加载这些依赖
4. 如果引入模块失败,那么require函数应该报一个异常
5. 模块通过变量exports来向往暴漏API,exports只能是一个对象,暴漏的API须作为此对象的属性。
符合CommonJS规范的模块应该是这样:
var react=require(./react.js);//引入模块
react.render();//使用模块
module.exports.x = x;//对外输出
其中:
【module】CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
【module.exports】module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
【exports】为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
var exports = module.exports;
我们可以在export对象下挂载属性和方法:
exports.area = function (r) { return Math.PI * r * r; }; exports.x = "hello world"
注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。
exports = function(){}//不要这样做!!!!
同样对module.exports赋值的话,挂载在exports对象下的方法也无法输出了。保险简单起见都用module.exports即可!!
利弊
nodeJS主要是运行在服务端的,因此CommonJS的规范主要是针对服务器端环境,并不完全适用于浏览器环境,主要原因是:CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作。像Node.js主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,所以加载起来比较快,不用考虑异步加载的方式。但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMD和CMD 解决方案。
AMD
AMD(Asynchronous Module Definition)即“异步的模块定义”,它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
规范
模块定义 defined方法
AMD就只有一个接口:define(id?,dependencies?,factory);
define("modA", ['package/lib'], function(lib){
function foo(){
lib.log('hello world!');
}
//dependencies参数加载依赖,['package/lib']
//factory函数的形参调用依赖,function(lib){}
return {
foo: foo //通过return对外输出
};
});
AMD规范也允许输出的模块兼容CommonJS规范,这时define方法需要写成下面这样:
define(function (require, exports, module){
//加载依赖模块
var someModule = require("someModule");
var anotherModule = require("anotherModule");
//调用模块
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
//对外输出
exports.asplode = function (){
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};
});
主模块入口(require加载)
require(dependencies?,callback?)
- dependencies:所依赖的模块(可选):该模块名称可以是模块的路径(不要加’.js’),也可以是require.config中配置的模块别名,但对于有主模块(就是定义了模块ID),paths中的别名必须和模块ID相同。
- callback 回调函数:参数名可以自定义。一般和依赖模块名中写入的相同。
例如:
require(['pkg/index/index_common_homepage'], function(index_common_homepage) {
console.log('pkg index_common_homepage called')
});
利弊
AMD规范会将所有的依赖模块预先下载,预先下载没什么争议,由于浏览器的环境特点,被依赖的模块肯定要预先下载的。问题在于,模块也被预先解析和执行了。如果一个模块依赖了十个其他模块,那么在本模块的代码执行之前,要先把其他十个模块的代码都执行一遍,无论该模块是否用到。这个性能消耗是不容忽视的。而CMD规范就很好的避开了这个问题。
CMD
CMD(Common Module Definition)也称“通用的模块定义”,和CommonJS保持了更好的兼容性。
规范定义
模块定义 defined方法
define(function(require, exports, module){
//code
})
三个形参,不可更改,与commonJS是对应的
define(function(require, exports, module){
//加载依赖模块
var mod = require("./mod.js");
//调用模块
var nums=mod.add();
//对外输出,两种方式
//方式1:
return {
nums:nums
}
//方式2:
module.exports.nums=nums;
})
主模块入口(sea.use加载)
seajs.use("main",function(ex){
console.log(ex.num);
});
利弊
- CMD可以做到按需加载,定义一个模块的时候不需要立即制定依赖模块,在需要的时候require就可以了,比较方便;
- CMD定义模块时无需罗列依赖数组,在factory函数中需传入形参require,exports,module,然后它会调用factory函数的toString方法,对函数的内容进行正则匹配,通过匹配到的require语句来分析依赖,这样就真正实现了commonJS风格的代码。
AMD VS CMD
AMD 推崇依赖前置, 代码在一旦运行到此处,能立即知晓依赖。而无需遍历整个函数体找到它的依赖,因此性能有所提升,缺点就是开发者必须显式得指明依赖——这会使得开发工作量变大,比如:当依赖项有n个时候 那么写起来比较烦 且容易出错。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。
执行顺序上:CMD是延迟执行的,而AMD是提前执行的。
api设计角度:AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
UMD
在AMD 与CommonJs 广泛发展的同时,为了同时兼容两种模块化方式,于是出现了Universal Module Definition,虽然他的定义方式及其丑陋,但是他兼容了AMD 与CommonJs,同时还兼容原始的“全局”方式。
规范定义
模块定义
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory(require('jquery'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.jQuery);
}
}(this, function ($) {
// methods
function myFunc(){};
// exposed public method
return myFunc;
}));
采用的是IIFE写法,将需要运行的函数放在第二位, 在 IIFE执行之后当作参数传递进, 所以他倒置代码的运行顺序。
ESM
即ECMAScript2015 Module. 也就是ES6中的模块化。
ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
规范定义
ES6 中新增了两个命令 import 和 export
- import 命令用于输入其他模块提供的功能
- export 命令用于规定模块的对外接口
ES6 中的模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
//加载依赖模块
import {deviceInfo, cookie,} from "../lib/util.js";
//调用模块,对外输出
export var cookie_spm = cookie.get('spm') || "";
//对外输出
export function(){
//code..
}
利弊
ES6 的模块是编译时加载,效率要比 CommonJS 模块的加载方式高。