隨著 JavaScript 日新月異的發(fā)展,超過了它產(chǎn)生時候的自我定位,由于沒有模塊管理的概念,在做大型項目或文件組織的時候,就會異常糾結(jié),而且后續(xù)也很難維護,長此以往,模塊化是必然趨勢~
模塊化的主要特征是:
- 可復(fù)用
- 封裝了變量和函數(shù),和全局的 namaspace 不接觸,松耦合
- 只暴露可用的 public 方法,其它私有方法全部隱藏
目前比較流行的 JS 模塊化規(guī)范有 CommonJS、AMD、CMD、UMD 以及 ES6 的模塊化。
一、CommonJS
Node.js 是 CommonJS 規(guī)范的主要實踐者,它有四個重要的環(huán)境變量為模塊化的實現(xiàn)提供支持:module、exports、require、global。實際使用時,通過 module.exports 導(dǎo)出對外的變量或接口,通過 require 導(dǎo)入其它模塊的輸出到當(dāng)前的模塊的作用域中。
模塊定義
// 定義模塊 math.js
var basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = { // 在這里寫需要向外暴露的變量或函數(shù)
basicNum: basicNum,
add: add
};
模塊引用
// 引入自定義的模塊,參數(shù)需要包含路徑,可省略后綴.js
var math = require('./math);
math.add(3, 5);
// 引入核心模塊,參數(shù)直接寫模塊名,不需要包含路徑
var http = require('http');
http.createServer(...).listen(8080);
module.exports v.s. exports
很多時候,我們會看到在一個模塊中有兩種方式來輸出變量:
方式一:對 module.exports 賦值
// hello.js
function sayHello() {
console.log('Hello');
}
function sayGoodbye() {
console.log('Goodbye');
}
module.exports = {
sayHello: sayHello,
sayGoodbye: sayGoodbye
};
方式二:直接使用 exports
// hello.js
function sayHello() {
console.log('Hello');
}
function sayGoodbye() {
console.log('Goodbye');
}
exports.sayHello = sayHello;
exports.sayGoodbye = sayGoodbye;
但是,不可以直接對 exports 賦值。
// 代碼可以執(zhí)行,但是并沒有輸出任何變量
exports = {
sayHello: sayHello,
sayGoodbye: sayGoodbye
};
原因是什么呢?我們來分析一下 Node.js 的加載機制。
首先,Node.js 會把待加載的文件 hello.js 放入一個包裝函數(shù) load() 中執(zhí)行。在執(zhí)行 load() 函數(shù)前,Node.js 準(zhǔn)備好了 module 變量:
var module = {
id: 'hello',
exports: {}
};
load() 函數(shù)最終返回 module.exports:
var load = function(module) {
// hello.js 文件的內(nèi)容
...
// load 函數(shù)返回
return module.exports;
};
var exports = load(module);
也就是說,exports 實際上是 module.exports 的引用,或者理解為 exports 是一個指針,指向 module.exports ,所以在使用 exports 的時候,只能是 exports.sayHello = function() {...} 這樣的方式,而不能使用 exports = { sayHello: function() {}},這種方式相當(dāng)于重新定義了 exports,module.exports 仍然是空對象 {},所以給 exports 賦值是無效的。
優(yōu)點:解決了依賴、全局變量污染的問題。
缺點:CommonJS 用同步的方式加載模塊。在服務(wù)端,模塊文件都存在本地磁盤,讀取非常快,所以這樣做不會有問題。但是在瀏覽器端,限于網(wǎng)絡(luò)原因,更合理的方案是使用異步加載。
二、AMD
AMD( Asynchronous Module Definition ) 是 Require.js 在推廣過程中對模塊定義的規(guī)范化產(chǎn)出。
AMD 規(guī)范采用異步方式加載模塊,所有依賴這個模塊的語句都定義在一個回調(diào)函數(shù)中,等到加載完成后,這個回調(diào)函數(shù)才會執(zhí)行。
實現(xiàn) AMD 規(guī)范的模塊化通過 define() 方法將代碼定義為模塊,通過 require() 方法實現(xiàn)模塊的加載。
這里以 require.js 為例,首先將 require.js 引入到頁面中:
<script src="js/require.js" data-main="js/main"></script>
定義模塊
(1)獨立模塊
即 不需要依賴任何其他模塊
// math.js
define(function() {
var basicNum = 0;
var add = function(a, b) {
return a + b;
};
return {
basicNum: basicNum,
add: add
};
});
(2)非獨立模塊
即 需要依賴其他模塊
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(3, 5);
$('#sum').html(sum);
});
require.js 還提供了一個 API: require.config() ,可以用來配置項目中用到的基礎(chǔ)模塊。
// 通過 config() 指定各模塊路徑和引用名
require.config({
baseUrl: 'js/lib',
paths: {
'jquery': 'jquery.min', // 實際路徑為 js/lib/jquery.min.js
'underscore': 'underscore.min'
}
});
// 引入模塊
require(['jquery', 'underscore'], function($, _) {
...
});
優(yōu)點:適合在瀏覽器環(huán)境中異步加載模塊,可以并行加載多個模塊。
缺點:不能按需加載,開發(fā)成本大。
三、CMD
CMD( Common Module Definition ) 是 Sea.js 在推廣過程中對模塊定義的規(guī)范化產(chǎn)出。
AMD 推崇依賴前置、提前執(zhí)行,CMD 推崇依賴就近、延遲執(zhí)行。
// AMD 寫法
define(['a', 'b', 'c', 'd', 'e'], function(a, b, c, d, e) {
// 等于在最前面聲明并初始化了所有依賴的模塊
a.doSomething();
if (false) {
// 即使沒有用到某個模塊 b,但 b 還是提前執(zhí)行了
b.doSomething();
}
});
// CMD 寫法
define(function(require, exports, module) {
var a = require('./a); // 在需要時聲明
a.doSomething();
if (false) {
var b = require('./b);
b.doSomething();
}
});
四、UMD
UMD ( Universal Module Definition ),希望提供一個前后端跨平臺的解決方案(支持 AMD 與 CommonJS 模塊方式)。
UMD 的實現(xiàn)原理:
- 先判斷是否支持 Node.js 模塊格式( exports 是否存在 ),存在則使用 Node.js 模塊格式。
- 再判斷是否支持 AMD(define 是否存在),存在則使用 AMD 方式加載模塊。
- 前兩個都不存在,則將模塊公開到全局( window 或 global )。
下面是一個示例:
eventUtil.js
(function(root, factory) {
if (typef exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
root.eventUtil = factory();
}
})(this, function() {
// module
return {
addEvent: function(el, type, handle) {
// ...
},
removeEvent: function(el, type, handle) {
// ...
}
};
});
五、ES6 Module
在 ES6 中, 我們可以通過 import 引入模塊,通過 export 導(dǎo)出模塊,功能比前幾個方案更強大,也是我們推薦使用的,但是由于瀏覽器對 ES6 的支持程度不同,目前都是使用 babel 或 traceur 把 ES6 代碼轉(zhuǎn)化為 ES5 代碼,然后再在瀏覽器環(huán)境中執(zhí)行。
// 定義模塊 math.js
var basicNum = 0;
var add = function(a, b) {
return a + b;
};
export { basicNum, add };
// 引用模塊
import { basicNum, add } from './math';
function test(element) {
element.textContent = add(basicNum, 99);
}
test();
導(dǎo)出模塊時還可以用 export default ,為模塊指定默認(rèn)輸出,對應(yīng)的 import 語句不需要使用大括號。
// 輸出模塊
export default {
basicNum,
add
}
// 引入模塊
import math from './math';
注:一個模塊只能有一個 export default。
六、CommonJS 與 ES6 模塊化的差異
1. CommonJS 支持動態(tài)導(dǎo)入,也就是 require(${path}/xx.js) ,ES6 目前還不支持,但是已有提案。
2. CommonJS 是同步導(dǎo)入,ES6是異步導(dǎo)入。
- CommonJS 因為用于服務(wù)端,文件都在本地,同步導(dǎo)入即使卡住主線程影響也不大。
- ES6 因為用于瀏覽器,需要下載文件,如果也采用同步導(dǎo)入會對渲染有很大影響。
3. CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
- CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內(nèi)部的變化就影響不到這個值;另一方面,如果導(dǎo)出的值變了,導(dǎo)入的值也不會變,所以如果想更新值,必須重新導(dǎo)入一次。
- ES6 采用實時綁定的方式,導(dǎo)入和導(dǎo)出的值都指向同一個內(nèi)存地址,所以導(dǎo)入的值會跟隨導(dǎo)出的值變化。
4. CommonJS 模塊是運行時加載,ES6 模塊是編譯時加載。
- CommonJS 模塊就是一個對象,在導(dǎo)入時先加載整個模塊,生成一個對象( 這個對象只有在腳本運行完才會生成 ),然后再從這個對象上讀取方法,這種加載稱為“運行時加載”。
- ES6 模塊不是對象,它的對外接口只是一種靜態(tài)定義,在代碼運行之前( 即編譯時 )的靜態(tài)解析階段就完成了模塊加載,比 CommonJS 模塊的加載方式更高效。