参考文献
前端模块化:CommonJS,AMD,CMD,ES6
ES6 模块与 CommonJS 模块的差异
前端模块化,AMD与CMD的区别,包括了模块的发展历程
一、引言
模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统。
首先站在框架设计者的角度,思考一个模块加载器要做什么事:
- 分析模块代码依赖的文件
- 下载文件
- JS加载文件
二、CommonJs
所以虽然JavaScript在web端发展这么多年,第一个流行的模块化规范却由服务器端的JavaScript应用带来,CommonJS规范是由NodeJS发扬光大,这标志着JavaScript模块化编程正式登上舞台。 Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports
定义当前模块对外输出的接口(不推荐直接用exports),用require
加载模块。
// 定义模块math.js
var basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
add: add,
basicNum: basicNum
}
// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);
// 引用核心模块时,不需要带路径
var http = require('http');
http.createService(...).listen(3000);
内部原理:
导出的是运行时对象——module.exports
- 在 node 中,每个模块内部都有一个自己的module对象
- 该 module 对象中,有一个成员叫:exports对象,默认是一个空对象
- 也就是说如果你需要对外导出成员,只需要把导出的成员挂载到 module.exports 中
- 我们发现,每次导出接口成员的时候都通过 module.exports.XXX = XXX 的方式很麻烦
- 所以 node 为了简化操作,专门提供了一个变量:exports = module.exports
var module = {
exports: {
foo: 'bar',
add: function (x, y) {
return x + y
}
}
}
// 也就是说在模块中还有这么一句代码
// var exports = module.exports
module.exports.foo = bar
module.exports.add = function (x, y) {
return x + y
}
console.log(exports === module.exports) // true
// 当一个模块需要导出单个成员的时候,直接给 exports 赋值是不管用的
exports = 'hello'
谁来 require 我,谁就得到 module.exports
默认在代码的最后有一句:
return module.exports
一定要记住,最后return 的是module.exports,不是exports
所以你给 exports 重新赋值不管用(引用类型赋值原理)
// 如果你实在分不清 exports 和module.exports,你可以选择忘记 exports而只使用 module.exports 也没问题,因为 exports 只是一个快捷方式
// module.exports.xxx = xxx
// module.exports = {}
module的属性如下:
{
id: '<repl>', // 模块标识符
exports: {}, // 模块导出的内容
parent: undefined, // 最先引用该模块的模块
filename: null, // 模块文件名
loaded: false, // 该模块是否已经加载完毕
children: [], // 该模块引用的模块
paths: // 该模块的搜索路径
[
'/Users/ces/repl/node_modules',
'/Users/ces/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/ces/.node_modules',
'/Users/ces/.node_libraries',
'/usr/local/lib/node'
]
}
尴尬的浏览器
CommonJs
中,由于require
是同步
的。模块系统需要同步读取模块文件内容,并编译执行以得到模块接口。这在服务器端实现很简单,也很自然,然而, 想在浏览器端实现问题却很多,但脚本标签天生异步加载(不是解析,解析默认是同步的),因此传统CommonJS模块在浏览器环境中无法正常加载。
script 标签的加载是异步的吗?
三、AMD和require.js
AMD 即Asynchronous Module Definition,即异步模块定义
。它是一个在浏览器端
模块化开发的规范,由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎RequireJS
.实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出
requireJS主要解决两个问题:
- 多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
- js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长
语法:
用require.config()
指定引用路径等,用define()
定义模块,用require()
加载模块。
// 定义math.js模块
define(function () {
var basicNum = 0;
var add = function (x, y) {
return x + y;
};
return {
add: add,
basicNum :basicNum
};
});
// 定义一个依赖underscore.js的模块
define(['underscore'],function(_){
var classify = function(list){
_.countBy(list,function(num){
return num > 30 ? 'old' : 'young';
})
};
return {
classify :classify
};
})
// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
var sum = math.add(10,20);
$("#sum").html(sum);
});
四、CMD和sea.js
CMD 即Common Module Definition通用模块定义,此规范其实是在sea.js推广过程中产生的。CMD有个浏览器的实现SeaJS
,SeaJS要解决的问题和requireJS一样,CMD它与AMD很类似,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同
CMD和AMD区别:
- AMD推崇依赖前置,在定义模块的时候就要声明并初始化其依赖的
所有模块
, 即便没用到某个模块 ,但还是提前执行了 - CMD推崇就近依赖,只有在用到某个模块的时候再去require 声明并执行
/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
a.doSomething();
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.doSomething()
}
});
/** CMD写法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
五、ES6 Module
其模块功能主要由两个命令构成:export
和import
。ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
特点:
- ES6 Module是静态的,也就是说它是在编译阶段运行,和var以及function一样具有提升效果(这个特点使得它支持tree shaking)
- 自动采用严格模式(顶层的this返回undefined)
- ES6 Module支持使用
export {<变量>}
导出具名的接口,或者export default
导出匿名的接口 - 目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 export 和 import,会经过
Babel
转换为 CommonJS 规范
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。
/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
ele.textContent = math.add(99 + math.basicNum);
}
六、 ES6 模块与 CommonJS 模块的差异
1、 CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
- 通过
export {<变量>}
导出的,它导出的是一个变量的引用。属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。 但是通过export default
导出的,在导入的时候的值相当于只是导入一个值。
2、 CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
CommonJs
运行在服务器上,被设计为运行时加载,即代码执行到那一行才回去加载模,CommonJS的模块是对象,输入时必须查找对象属性,只有运行时才能得到这个对象,不能在编译时做到静态化ES6 Module
ES6模块不是对象,而是通过export
命令显示指定输出代码
,是静态的输出一个接口,在编译的阶段就加载模块,且和var以及function一样具有提升效果(这个特点使得它支持tree shaking)
3、因此CommonJs require
可以做动态加载(比如可以写在判断里),ES6 Module 静态语法import
语句必须位于顶层作用域中
- 由于
import
命令没有办法代替require
的动态加载功能,因此引入了import()
函数。完成动态加载。import()
返回一个Promise
对象,可以.then()
import()
函数适用场合:按需加载、条件加载(if...else
)
4、CommonJS 加载的是整个模块,即将所有的接口全部加载进来,ES6 可以单独加载其中的某个接口(方法)
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。
5、ES6 模块中默认采用严格模式,因此顶层的 this 指向 undefined,而CommonJS 模块的顶层 this 指向当前模块
6、CommonJs加载具有缓存,ES6模块不会缓存运行结果
区别:(另外一种)
- ES6模块在编译时,就能确定模块的依赖关系,以及输入和输出的变量。
CommonJS 模块,运行时加载。 - 因此require 可以做动态加载,import 语句做不到,import 语句必须位于顶层作用域中。
- ES6 模块自动采用严格模式,无论模块头部是否写了 “use strict”;
- ES6 模块中顶层的 this 指向 undefined,CommonJS 模块的顶层 this 指向当前模块。
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
下面看两个经典的例子对比一下:
CommonJs模块化
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
ES6模块化
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
从上面我们看出,CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。而ES6 模块是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。
另外CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。