模塊是每門語言構建復雜系統(tǒng)的必備特性彤侍,JavaScript自然也不例外英支。JavaScript當前流行的模塊化標準有CommonJS改化、AMD掩蛤、CMD、ES6等等陈肛,本文對這些標準做了簡單梳理揍鸟,努力做到應用時不懵逼,不亂用句旱。
模塊
現(xiàn)如今幾乎每門語言都有自己的模塊化解決方案阳藻,這是隨著軟件工程越來越復雜的必然產(chǎn)物晰奖。貼幾個流行語言的模塊化介紹大家感受下:
所有語言的模塊化解決方案都是為了實現(xiàn)將復雜的程序拆分成獨立的幾個模塊,每個模塊寫明自己的依賴腥泥、輸出自己的能力畅涂。模塊化讓復雜代碼變得容易維護、方便復用道川。
概覽
JavaScript標準眾多午衰,縷清這幾個標準的發(fā)展史有助于大家選擇采用哪種方案來寫代碼。
- CommonJS應該是最早在民間自發(fā)產(chǎn)生的服務端模塊化標準冒萄,一開始是叫ServerJS臊岸,后來改名了。
- 服務端JS有了模塊化標準之后尊流,瀏覽器JS表示我也必須有帅戒,于是基于CommonJS標準產(chǎn)生了AMD,和CommonJS相比最大的不同就是依賴的異步加載崖技。
- CMD是類似AMD的對于瀏覽器JS模塊化標準逻住,源自Sea.js。
- ES6則是集大成者迎献,其統(tǒng)一了同步和異步的模塊化標準瞎访,試圖讓JS模塊化標準從分裂走向統(tǒng)一,并取得了不小的成績吁恍。
標準定制一般都是和實現(xiàn)相輔相成的扒秸,那么JS這些有名的模塊化標準主要都有哪些實現(xiàn)呢?
CommonJS | AMD | CMD | ES6 |
---|---|---|---|
Node.js/RingoJS | RequireJS/curl.js | SeaJS | ES6 |
每個標準都在JS世界的不同領域中得到廣泛的應用冀瓦,對這些標準進行初步的了解是有必要的伴奥。
CommonJS
為了方便,直接使用Node.js的模塊化實現(xiàn)來說明CommonJS標準翼闽。下面給出按照CommonJS標準寫的demo拾徙,隨后其他標準的demo也會實現(xiàn)一樣的功能。
// math.js
const { PI } = Math;
exports.area = (r) => PI * r ^ 2;
exports.circumference = (r) => 2 * PI * r;
console.log(module);
// main.js
var area = require('./math').area;
var result = area(3);
console.log(result);
CommonJS模塊定義了三個變量感局,module
尼啡,exports
和require
。
module
通過console.log(module)
蓝厌,我們可以打印出module
的結構如下:
Module {
id: '.', // 模塊Id玄叠,一般都是文件的絕對路徑
exports: { area: [Function], circumference: [Function] }, // 模塊對外輸出的變量
parent: null, // 調用該模塊的模塊古徒,如果直接執(zhí)行就是null
filename: '/path/to/demo/math.js', // 帶絕對路徑的文件名
loaded: false, // 模塊是否加載完成
children: [], // 模塊的依賴
paths: // 模塊依賴的搜索路徑
[ '/path/to/demo/node_modules',
'/path/to/node_modules',
'/path/node_modules',
'/node_modules' ] }
exports
在module
對象中是有字段exports
的拓提,exports
實際上就是module.exports
。
var exports = module.exports;
因此導出變量有兩種方式:
exports.area = (r) => PI * r ^ 2;
exports.circumference = (r) => 2 * PI * r;
// 或者也可以如下
module.exports.area = (r) => PI * r ^ 2;
module.exports.circumference = (r) => 2 * PI * r;
因為exports
是module.exports
的引用隧膘,在導出的時候我們就要格外小心了代态。
exports.area = (r) => PI * r ^ 2;
module.exports = (r) => 2 * PI * r; // 將module.exports對象覆蓋寺惫,area這個變量就不會被導出。
exports = (r) => 2 * PI * r; // exports就不再是module.exports的引用了蹦疑,會導致后面的circumference導出無效西雀。
exports.circumference = (r) => 2 * PI * r;
require
require
的參數(shù)是模塊id,require
實現(xiàn)的功能就是根據(jù)模塊id去找到對應的依賴模塊歉摧。模塊id的變數(shù)主要在兩個方面艇肴,一個是后綴名,一個是路徑叁温。
首先來說后綴名再悼,一般默認是js的,所以我們在依賴的以后一般不需要添加后綴名膝但。而且找不到的話冲九,Node.js還會嘗試添加.json
,.node
后綴去查找跟束。
var area = require('./math').area;
// 和上面是一樣的
var area = require('./math.js').area;
再來說路徑莺奸,絕對路徑和相對路徑就不多說,比較好理解冀宴。
var area = require('/math').area; // 在指定的絕對路徑查找模塊
var area = require('./math').area; // 在相對與當前目錄的路徑查找模塊
還有如果不是以"."灭贷、".."或者"/"開頭的話,那就會先去核心模塊路徑找略贮,找不到再按照module.paths
指定的路徑找氧腰。
var area = require('math').area;
AMD
同樣的,本節(jié)采用RequireJS
來說明AMD標準刨肃。先上一個例子古拴。
// math.js
define('app/math', function () {
const { PI } = Math;
return {
area: function (r) {
return PI * r ^2;math.js
},
circumference: function (r) {
return 2 * PI * r;
}
};
});
// main1.js
define(['app/math', 'print'], function (math, print) {
print(math.area(3));
});
// main2.js
define(function (require) {
var math = require('./math');
var print = require('print');
print(math.area(3));
});
define
AMD使用define
這個api來定義一個模塊,其語法比較簡單真友。
define(id?, dependencies?, factory);
模塊id和依賴都是可選參數(shù)黄痪,只有構造函數(shù)是必須的。
id
AMD的模塊id和CommonJS
的module
對象中的id作用是一樣的盔然,用來唯一的指定模塊桅打,一般是模塊的絕對路徑。雖然define函數(shù)將這個id暴露給使用者愈案,但一般也是不填的挺尾,一些優(yōu)化工具會自動生成絕對路徑作為id參數(shù)傳給define
函數(shù)。id的定義也和CommonJS類似站绪,相對路徑遭铺、絕對路徑、js后綴可以省略等等。詳細的可以查看AMD模塊id的格式魂挂。
dependencies
在factory
函數(shù)中使用到的依賴需要先在這里指明甫题,比如示例代碼,需要指明app/math
和print
涂召,然后將他們作為factory
的參數(shù)傳給函數(shù)體使用坠非。AMD協(xié)議保證在factory
函數(shù)執(zhí)行之前,能將所有的依賴都準備好果正。
除了指明依賴之外炎码,dependencies
還有一種寫法。這種寫法是為了方便復用按照CommonJS
規(guī)范寫的模塊秋泳,足見AMD規(guī)范的良苦用心辅肾。
define(function(require, exports, module) {
var a = require("a");
exports.foo = function () {
return a.bar();
};
});
在RequireJS
中依賴的查找路徑是通過配置文件來指定的baseUrl
、paths
轮锥、bundles
等矫钓,這一點和Node.js是完全不一樣的。
AMD這個標準有個比較明顯的缺陷就是所有的依賴都必須要先執(zhí)行舍杜,這個從其接口的設計上就能看出來新娜。如果依賴比較多的話,這個事情就比較坑爹了既绩。
factory
這個參數(shù)名字比較有意思概龄,叫工廠函數(shù),當某塊被依賴的時候饲握,這個工廠函數(shù)就會被執(zhí)行私杜,而且即便被依賴多次,也只會執(zhí)行一次救欧。在factory
中需要導出變量的時候衰粹,直接return就可以了,當然也可以使用CommonJS規(guī)范的exports笆怠。
相比較而言铝耻,AMD標準還是比較復雜的。
CMD
CMD雖然沒有CommonJS
和AMD
出名蹬刷,但是SeaJS
在國內還是比較出名瓢捉,這里也捎帶提及CMD規(guī)范,不多說办成,來demo代碼先泡态。
// math.js
define(function(require, exports, module) {
const { PI } = Math;
exports.area = function (r) {
return PI * r ^2;math.js
},
exports.circumference = function (r) {
return 2 * PI * r;
}
});
// main1.js
define(function(require, exports, module) {
var area = require('./math').area;
var print = require('print');
print(area(3));
});
上面的示例和AMD的示例雖然比較像,但是實際上CMD的規(guī)范和AMD還是不太一樣的迂卢,有自己的一些特色某弦。
define
模塊定義和雖然和AMD一樣用的是define
函數(shù)桐汤,但是只支持factory
一個參數(shù)。
define(factory);
factory
和AMD也是類似的刀崖,可以是函數(shù)惊科,也可以是一個object拍摇。
require && require.async
CMD除了有同步的require接口亮钦,還有異步接口require.async,這樣就解決了我們之前提到的AMD需要先把所有依賴都加載好才能執(zhí)行factory
的弊端充活。
define(function(require) {
// 同步接口示例
var a = require('./a');
a.doSomething();
// 異步接口示例
require.async('./b', function(b) {
b.doSomething();
});
})
exports
這個就比較類似CommonJS的exports
了蜂莉,是用來輸出API或者對象的。
module
這個也比較類似CommonJS的module對象混卵,不過相比于Node.js的module對象要簡單的多映穗,只包括
module.uri // 模塊完整解析出來的uri
module.dependencies // 所有的依賴
module.exports // 導出的能力
從上面的簡單描述可以看出,CMD想同時解決AMD和CommonJS能解決的問題幕随,基于AMD和CommonJS的設計做了簡化優(yōu)化蚁滋,同時設計了異步require
的接口等。關于CMD的大量細節(jié)可以查看SeaJS官網(wǎng)赘淮。
ES6
一直以來JavaScript語言本身是沒有內置的模塊系統(tǒng)辕录,ES6終結了這個局面。雖然ES6的普及還需要好多年梢卸,但ES6完全兼容ES5的所有特性走诞。ES6的寫法可以通過轉換工具轉成ES5來執(zhí)行,是時候好好學習ES6了蛤高。
讓我們來看看用ES6實現(xiàn)上面的示例是什么樣的蚣旱?
// math.js
const { PI } = Math;
export function area(r) {
return PI * r ^ 2;
}
export function circumference(r) {
return 2 * PI * r;
}
// main.js
import { area, circumference } from './math';
console.log(area(3));
export
ES6的模塊是嚴格要求一個模塊一個文件,一個文件一個模塊的戴陡。每個模塊可以只導出一個變量塞绿,也可以導出多個變量。
一個模塊導出多次使用命名的導出(named exports)恤批。
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
一個模塊只導出一次使用默認導出(default exports)位隶,非常方便。
export default 'abc';
export default foo();
export default /^xyz$/;
export default 5 * 7;
export default { no: false, yes: true };
export default function () {}
import
ES6的import
和之前標準的require
是比較不一樣的开皿,被導出變量是原有變量的只讀視圖涧黄。這意味著雖然變量被導出了,但是它還是和內部變量保持關聯(lián)赋荆,被導出變量的變化笋妥,會導致內部變量也跟著變化。也許這正是ES6重新取了import
這個名字而沒有使用require
的原因窄潭。這一點和require
是完全不一樣的春宣,require
變量導出之后就生成了一個新的變量,和原始的內部變量就脫離關系了。有個demo能比較好的說明這個問題月帝。
//------ lib.js ------
export let counter = 3;
export function incCounter() {
counter++;
}
//------ main.js ------
import { counter, incCounter } from './lib';
// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4
模塊是ES6語言的一項重大特性躏惋,里面的細節(jié)比較多,詳細描述怕是篇幅太長了嚷辅,需要詳細了解ES6模塊語法的同學請移步ES Modules簿姨。
總結
本文簡單描述了CommonJS、AMD簸搞、CMD以及ES6的模塊標準扁位,仔細研究各個標準的細節(jié)可以一窺JavaScript模塊化標準的發(fā)展歷程。JavaScript語言早期作為網(wǎng)站的一種腳本語言趁俊,不需要模塊化這種特性域仇,但隨著node.js的出現(xiàn),js的工程越來越復雜寺擂,模塊化也越來越重要暇务。CommonJS、AMD和CMD是在語言不支持的情況下發(fā)展出來的第三方模塊化解決方案怔软,ES6正是基于這些解決方案提出了語言內置的模塊標準垦细,希望ES6能盡快的推廣起來,這樣JSer就能輕松許多啦爽雄。
參考文獻
- exploringjs modules
- JavaScript Modules
- Asynchronous Module Definition
- CommonJS規(guī)范
- Modules/1.1
- CommonJS
- SeaJs
- CMD規(guī)范
- nodejs modules
- 前端模塊化開發(fā)那點歷史
- Writing Modular JavaScript With AMD, CommonJS & ES Harmony
