学习ES6和Webpack过程中,研究了一下CommonJS,AMD, CMD, ES6这些模块系统到底有什么区别。
先提结论:
- 相同点是,所有的文件模块系统,都采用单例模式。
- 模块加载方面:CJS采用同步阻塞机制,而AMD采用异步回调机制,CMD可通过调用不同的
require
方法,采用不同的机制。 - 模块执行方面:CJS,AMD都采用预先执行,在引用模块的同时执行模块代码;CMD则采用延迟执行,只有当模块中的变量或方法真正被执行时, 模块代码才被执行。
- 个人觉得服务器端使用CJS和CMD都可以,但比较偏爱CJS简洁的语法。浏览器端从性能方面考虑还是比较推荐使用AMD,尽量避免在代码运行过程中去做资源请求。
- 特别推荐使用webpack模块打包工具配上ES6的模块文件系统语法,在本地进行模块编译后上线。
正文:
JS本来是没有模块的语言,随着页面交互越来越复杂,为了避免全局变量泛滥以及方便多人合作开发等,我们开始运用对象,模拟类,闭包等来实现模块的效果,例如:
- 用对象将方法和变量封装起来。
var module1 = {
_count : 0,
m1 : function (){
//...
},
m2 : function (){
//...
}
};
- 用闭包封装方法,可得到外部无法读取的私有变量
var module1 = (function(){
var _count = 0;
var m1 = function(){
//...
};
var m2 = function(){
//...
};
return {
m1 : m1,
m2 : m2
};
})();
- 沙箱模式,声明模块依赖
Sandbox.modules = {};
Sandbox.modules.dom = function(box) {
box.getElement = function() {};
box.getStyle = function() {console.log('getStyle')};
box.foo = 'bar';
};
new Sandbox(['dom', 'ajax'], function(box) { //声明依赖
box.getElement();
})
思路:1. 将全局属性方法的初始化函数放在Sandbox构造函数的属性modules中;2. 在构造函数内部获取所需的属性,赋值给this; 3. 运行callback(this)。具体实现代码可在这篇文章里搜‘沙箱模式’查看。
虽然模块化编程能较好的帮助我们处理单个文件内的代码结构。但随着单页面应用的出现,单个js文件已经无法满足复杂交互的要求了。
而单纯依赖html<script>
引用的多文件间没有依赖关系,需要根据依赖严格注意在页面中的引用顺序,即使这样,还是很容易出现错误。因此,协调不同文件之间依赖关系模块文件系统就出现了。
CommonJS
CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。Node.js 就是CommonJS规范的最常见的实现。规范其中也定义了适合服务器端使用的同步阻塞文件模块系统:
语法:
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
//a.js 定义模块
module.exports = 'I am from a.js!';
//main.js 引用模块
const a = require('./a.js');
console.log(a); //输出'I am from a.js!'
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令:
var exports = module.exports;
这让我们可以很方便的在对外输出模块接口时,向exports对象添加方法。最终输出的是exports对象。
//a.js 定义模块
exports.doStuff = function() {
console.log('doing stuff...');
}
//main.js 引用模块
const a = require('./a.js');
a.doStuff(); //输出'doing stuff...'
这就解释了如果希望输出的是非对象类型,不能直接赋值给
exports
,而是要赋值给module.export
。这是因为exports
实际上是指向module.export
的,修改它的值只会修改它的指针指向别的对象或数据,而不能修改module.export
的值。
特征:
- 同步阻塞:CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。这是由于服务器端的文件是存在于本地的,加载时间比较短。但在浏览器端需要较长时间向服务器端请求文件的情况下,这种文件模块系统并不合适。
- 单例模式: 虽然CommonJS允许在同一个文件对同一个模块多次引用,但获取到的是同一个对象(类,函数)。可以看看下面这个例子:
//counter.js 定义一个计数器函数,有一个内部变量i
var i = 0
function count() {
return ++i;
}
module.exports = count;
//main.js引用counter.js
var counter1 = require('./counter')
var counter2 = require('./counter')
var counter3 = require('./counter')
console.log(counter1())
console.log(counter2())
console.log(counter3())
运行结果:三次引用counter.js都执行一次,分别输出1,2,3;说明三次引用的是同一个函数,操作的是同一个变量。
AMD
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。相对于CommonJS的同步阻塞模块系统,更适合用于浏览器端。最常用的实现是require.js。
它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
语法:
//name.js 定义模块
define([], function(){
return 'daisy';
});
//age.js 定义模块
define([], function(){
return 23;
});
//main.js 引用模块
require(['./name', './age'], function(name, age) {
console.log(`Hi! I am ${name}, I am ${age} years old.`); //Hi! I am daisy, I am 23 years old.
});
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
- 第一个参数[module],是一个数组,里面的成员就是要加载的模块;
- 第二个参数callback,则是加载成功之后的回调函数。回调函数的参数依次是按照数组顺序依赖的模块内容。
同样的,在define()语句定义模块时,第一个参数数组也可以用于引用其他模块,在回调函数的参数重输出。
特征:
- 异步加载: 模块代码采用回调函数的形式执行,页面不需要停下等待加载完成,因此是浏览器端常用的模块文件系统。
- 与CommonJS一样,引用时采用单例模式。
//counter.js
define([], function(){
let i = 0;
return function() {
return ++i;
}
});
//main.js
require(['./counter'], function(counter) {
console.log(counter()); //1
console.log(counter()); //2
console.log(counter()); //3
});
require(['./counter'], function(counter) {
console.log(counter()); //4
console.log(counter()); //5
console.log(counter()); //6
});
CMD
CMD是个人比较不熟悉的,但感觉上更像是AMD和CJS的结合延伸。CMD 推崇 as lazy as possible,sea.js是CMD常用的实现。
语法:
define({ "foo": "bar" });
define('I am a template. My name is {{name}}.');
define(function() {
// 模块代码
}) ;
//也可以接受依赖
define('hello', ['jquery'], function(require, exports, module) {
// 模块代码
});
//引用
define(function(require, exports) {
// 同步获取模块 a 的接口
const a = require('./a');
// 调用模块 a 的方法
a.doSomething();
// 异步获取模块b的接口
require.async('./b', function(b) {
b.doSomething();
});
// 异步加载多个模块,在加载完成时,执行回调
require.async(['./c', './d'], function(c, d) {
c.doSomething();
d.doSomething();
});
});
特征:
- 与AMD预先加载模块不同的是,CMD提倡就近加载:在需要用到该模块的地方才调用
require()
或require.async()
获取模块接口,尽量避免预先加载不需要的模块。 - 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同。
对于提前执行和延迟执行的理解:AMD在最初加载模块文件时,立刻执行模块中的方法,并将返回值赋值给回调函数的参数;而CMD在调用
const a = require('./a.js')
后,解析路径后只是将a.js文件中的代码通过<scpript>
插入到页面中,并给予唯一的id。只有当真正调用a时,才会去执行对应id的那段js代码。所以叫延迟执行。
- 同时支持同步异步两种模块加载方式。
ES6
千呼万唤ES6终于推出了模块文件系统。新一代的标准的设计理念是兼容现有的 CommonJS 和 AMD 模块。但目前因为还没有浏览器支持这个功能,我们能够看到的只是模块到处和引用的语法。个人感觉依赖ES6变量结构语法,这套语法用起来会比之前所有文件系统的语法更加顺手。
语法
一个 ES6 的模块是一个包含了 JS 代码的文件。ES6 里没有所谓的 module 关键字。一个模块看起来就和一个普通的脚本文件一样,除了以下两个区别:
- ES6 的模块自动开启严格模式,即使你没有写 'use strict'。
- 你可以在模块中使用 import 和 export。
//直接在希望导出的 function、class、var、let 或 const 前添加export关键字
export function detectCats(canvas, options) {
var kittydar = new Kittydar(options);
return kittydar.detectCats(canvas);
}
export class Kittydar {
... several methods doing image processing ...
}
// This helper function isn't exported.
function resizeCanvas() {
...
}
//直接导出对象集合:
export {detectCats, Kittydar};
// no `export` keyword required here
function detectCats(canvas, options) { ... }
class Kittydar { ... }
在另外一个文件中,我们可以导入这个模块并且使用 detectCats() 函数:
//引用整个模块导出对象
import kitty from "kittydar.js";
kitty.detectCats();
//直接引用模块中的某个方法或变量
import {detectCats} from "kittydar.js";
function go() {
var canvas = document.getElementById("catpix");
var cats = detectCats(canvas);
drawRectangles(canvas, cats);
}
//同时引用模块中的多个方法或变量
import {detectCats, Kittydar} from "kittydar.js";
特征:
由于在浏览器端仍没有具体的实现,很难去判断它的特性。Jon Coppeard 正在火狐浏览器上实现 ES6 的模块。之后包括 JavaScript Loader 规范在内的工作已经在进行中。HTML 中类似 <script type=module>这样的东西之后也会和大家见面。
总结:
- 相同点是,所有的文件模块系统,都采用单例模式。
- 模块加载方面:CJS采用同步阻塞机制,而AMD采用异步回调机制,CMD可通过调用不同的
require
方法,采用不同的机制。 - 模块执行方面:CJS,AMD都采用预先执行,在引用模块的同时执行模块代码;CMD则采用延迟执行,只有当模块中的变量或方法真正被执行时, 模块代码才被执行。
- 个人觉得服务器端使用CJS和CMD都可以,但比较偏爱CJS简洁的语法。浏览器端从性能方面考虑还是比较推荐使用AMD,尽量避免在代码运行过程中去做资源请求。
- 特别推荐使用webpack模块打包工具配上ES6的模块文件系统语法,在本地进行模块编译后上线。
参考:
CommonJS规范
阮一峰AMD
AMD和CMD的区别from玉伯
关于AMD 是提前执行,CMD 是延迟执行
ES6文件系统模块