JS模塊標準怎麼這麼多?
模塊是每門語言構建複雜係統的必備特性,JavaScript自然也不例外。JavaScript當前流行的模塊化標準有CommonJS、AMD、CMD、ES6等等,本文對這些標準做了簡單梳理,努力做到應用時不懵逼,不亂用。
模塊
現如今幾乎每門語言都有自己的模塊化解決方案,這是隨著軟件工程越來越複雜的必然產物。貼幾個流行語言的模塊化介紹大家感受下:
所有語言的模塊化解決方案都是為了實現將複雜的程序拆分成獨立的幾個模塊,每個模塊寫明自己的依賴、輸出自己的能力。模塊化讓複雜代碼變得容易維護、方便複用。
概覽
JavaScript標準眾多,縷清這幾個標準的發展史有助於大家選擇采用哪種方案來寫代碼。
- CommonJS應該是最早在民間自發產生的服務端模塊化標準,一開始是叫ServerJS,後來改名了。
- 服務端JS有了模塊化標準之後,瀏覽器JS表示我也必須有,於是基於CommonJS標準產生了AMD,和CommonJS相比最大的不同就是依賴的異步加載。
- CMD是類似AMD的對於瀏覽器JS模塊化標準,源自Sea.js。
- ES6則是集大成者,其統一了同步和異步的模塊化標準,試圖讓JS模塊化標準從分裂走向統一,並取得了不小的成績。
標準定製一般都是和實現相輔相成的,那麼JS這些有名的模塊化標準主要都有哪些實現呢?
CommonJS | AMD | CMD | ES6 |
---|---|---|---|
Node.js/RingoJS | RequireJS/curl.js | SeaJS | ES6 |
每個標準都在JS世界的不同領域中得到廣泛的應用,對這些標準進行初步的了解是有必要的。
CommonJS
為了方便,直接使用Node.js的模塊化實現來說明CommonJS標準。下麵給出按照CommonJS標準寫的demo,隨後其他標準的demo也會實現一樣的功能。
// 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, // 調用該模塊的模塊,如果直接執行就是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
的參數是模塊id,require
實現的功能就是根據模塊id去找到對應的依賴模塊。模塊id的變數主要在兩個方麵,一個是後綴名,一個是路徑。
首先來說後綴名,一般默認是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
同樣的,本節采用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和依賴都是可選參數,隻有構造函數是必須的。
id
AMD的模塊id和CommonJS
的module
對象中的id作用是一樣的,用來唯一的指定模塊,一般是模塊的絕對路徑。雖然define函數將這個id暴露給使用者,但一般也是不填的,一些優化工具會自動生成絕對路徑作為id參數傳給define
函數。id的定義也和CommonJS類似,相對路徑、絕對路徑、js後綴可以省略等等。詳細的可以查看AMD模塊id的格式。
dependencies
在factory
函數中使用到的依賴需要先在這裏指明,比如示例代碼,需要指明app/math
和print
,然後將他們作為factory
的參數傳給函數體使用。AMD協議保證在factory
函數執行之前,能將所有的依賴都準備好。
除了指明依賴之外,dependencies
還有一種寫法。這種寫法是為了方便複用按照CommonJS
規範寫的模塊,足見AMD規範的良苦用心。
define(function(require, exports, module) {
var a = require("a");
exports.foo = function () {
return a.bar();
};
});
在RequireJS
中依賴的查找路徑是通過配置文件來指定的baseUrl
、paths
、bundles
等,這一點和Node.js是完全不一樣的。
AMD這個標準有個比較明顯的缺陷就是所有的依賴都必須要先執行,這個從其接口的設計上就能看出來。如果依賴比較多的話,這個事情就比較坑爹了。
factory
這個參數名字比較有意思,叫工廠函數,當某塊被依賴的時候,這個工廠函數就會被執行,而且即便被依賴多次,也隻會執行一次。在factory
中需要導出變量的時候,直接return就可以了,當然也可以使用CommonJS規範的exports。
相比較而言,AMD標準還是比較複雜的。
CMD
CMD雖然沒有CommonJS
和AMD
出名,但是SeaJS
在國內還是比較出名,這裏也捎帶提及CMD規範,不多說,來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的規範和AMD還是不太一樣的,有自己的一些特色。
define
模塊定義和雖然和AMD一樣用的是define
函數,但是隻支持factory
一個參數。
define(factory);
factory
和AMD也是類似的,可以是函數,也可以是一個object。
require && require.async
CMD除了有同步的require接口,還有異步接口require.async,這樣就解決了我們之前提到的AMD需要先把所有依賴都加載好才能執行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的設計做了簡化優化,同時設計了異步require
的接口等。關於CMD的大量細節可以查看SeaJS官網。
ES6
一直以來JavaScript語言本身是沒有內置的模塊係統,ES6終結了這個局麵。雖然ES6的普及還需要好多年,但ES6完全兼容ES5的所有特性。ES6的寫法可以通過轉換工具轉成ES5來執行,是時候好好學習ES6了。
讓我們來看看用ES6實現上麵的示例是什麼樣的?
// 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
是比較不一樣的,被導出變量是原有變量的隻讀視圖。這意味著雖然變量被導出了,但是它還是和內部變量保持關聯,被導出變量的變化,會導致內部變量也跟著變化。也許這正是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語言的一項重大特性,裏麵的細節比較多,詳細描述怕是篇幅太長了,需要詳細了解ES6模塊語法的同學請移步ES Modules。
總結
本文簡單描述了CommonJS、AMD、CMD以及ES6的模塊標準,仔細研究各個標準的細節可以一窺JavaScript模塊化標準的發展曆程。JavaScript語言早期作為網站的一種腳本語言,不需要模塊化這種特性,但隨著node.js的出現,js的工程越來越複雜,模塊化也越來越重要。CommonJS、AMD和CMD是在語言不支持的情況下發展出來的第三方模塊化解決方案,ES6正是基於這些解決方案提出了語言內置的模塊標準,希望ES6能盡快的推廣起來,這樣JSer就能輕鬆許多啦。
參考文獻
- exploringjs modules
- JavaScript Modules
- Asynchronous Module Definition
- CommonJS規範
- Modules/1.1
- CommonJS
- SeaJs
- CMD規範
- nodejs modules
- 前端模塊化開發那點曆史
- Writing Modular JavaScript With AMD, CommonJS & ES Harmony
最後更新:2017-08-25 15:03:42