在JavaScript模塊一文中介紹了如何組織代碼實現(xiàn)模塊化落午。模塊化能隱藏私有的屬性和方法,只暴露出公共接口漓穿。這樣別人就不需要從頭開始造輪子坷衍,直接用你的模塊中定義的功能就行了。而且保證了命名空間苫亦,不會出現(xiàn)命名沖突毛肋。
但如果沒有一套規(guī)范做參照,每個人都隨自己的喜好定義模塊屋剑,使用別人的模塊就會出現(xiàn)障礙润匙。本篇就介紹一下通用的定義JS模塊的規(guī)范:CommonJS和AMD
CommonJS
Nodejs的模塊系統(tǒng)就采用CommonJS模式。CommonJS標準規(guī)定唉匾,一個單獨的文件就是一個模塊孕讳,模塊內(nèi)將需要對外暴露的變量放到exports對象里,可以是任意對象巍膘,函數(shù)厂财,數(shù)組等,未放到exports對象里的都是私有的峡懈。用require方法加載模塊璃饱,即讀取模塊文件獲得exports對象。
例如定義個hi.js:
var str = 'Hi';
function sayHi(name) {
console.log(str + ', ' + name + '!');
}
module.exports = sayHi;
在hi模塊中定義了sayHi函數(shù)肪康,用exports將它暴露出去荚恶。未被暴露出去的變量str是無法被外部訪問的。其他模塊要用這個函數(shù)的話磷支,需要先require這個hi模塊:
var Hi = require('./hi');
Hi('Jack'); // Hi, Jack!
第一行里的變量Hi谒撼,其實就是hi.js里用module.exports = sayHi;
輸出的sayHi函數(shù)。
注意上面用require加載時寫的是相對路徑雾狈,讓Nodejs去指定路徑下加載模塊嗤栓。如果省略相對路徑,默認就會在node_modules文件夾下找hi模塊箍邮,那很可能因為找不到而報錯。如果你加載的是Node內(nèi)置模塊叨叙,或npm下載安裝后的模塊锭弊,可以省略相對路徑,例如:
var net = require('net');
var http = require('http');
在JavaScript模塊一文中介紹過JS模塊的寫法擂错,這里出現(xiàn)的module味滞,exports,require是JS的新語法嗎?不是新語法剑鞍,只是CommonJS的語法糖昨凡。Node會將上述hi.js編譯成:
// 準備module對象:
var module = {
id: 'hi',
exports: {}
};
var load = function (module) {
function sayHi(name) {
console.log('Hi, ' + name + '!');
}
module.exports = sayHi;
return module.exports;
};
var exported = load(module);
// 保存module:
save(module, exported);
上來先定義一個module對象,屬性id是該模塊名即文件名蚁署,屬性exports就是最終需要暴露出來的對象便脊。因此我們在hi.js代碼里明明沒有var聲明過module對象,但module.exports = sayHi;居然不會報錯光戈。原因就是module是Node在加載js文件前已經(jīng)事先替我們準備好了哪痰。最后Node會用自定義函數(shù)save將這個module對象保存起來。
Node保存了所有的module對象后久妆,當我們用require()獲取module時晌杰,Node會根據(jù)module.id找到對應(yīng)的module,并返回module. exports筷弦,這樣就實現(xiàn)了模塊的輸出肋演。
CommonJS是同步的,意味著你想調(diào)用模塊里的方法烂琴,必須先用require加載模塊爹殊。這對服務(wù)器端的Nodejs來說不是問題,因為模塊的JS文件都在本地硬盤上监右,CPU的讀取時間非潮呙穑快,同步不是問題健盒。
但在客戶端瀏覽器用CommonJS加載模塊將取決于網(wǎng)速绒瘦,如果采用同步,網(wǎng)絡(luò)情緒不穩(wěn)定時扣癣,頁面可能卡住惰帽。因此針對客戶端出現(xiàn)了AMD異步模塊定義。
AMD
AMD(Asynchronous Module Definition)異步加載模塊父虑。AMD標準規(guī)定该酗,用define來定義模塊,用require來加載模塊:
define(id, [depends], factory);
require([module], callback);
先看define定義模塊的示例:
define(['module1', 'module2'], function (module1, module2) {
……
return { … };
});
第一個參數(shù)id是你的模塊名士嚎,上例省略呜魄。事實上這個id沒什么用,就是給開發(fā)者看的莱衩。
第二個參數(shù)[depends]是該模塊所依賴的其他模塊爵嗅,上例中該模塊依賴另兩個模塊module1和module2。如果你定義的模塊不依賴其他任何模塊笨蚁,該參數(shù)可以省略睹晒。
第三個參數(shù)factory趟庄,生產(chǎn)出(即return出)一個對象供外部使用(我看到有的資料里將該參數(shù)命名為callback,感覺callback語義方面有點曖昧伪很,命名成factory更貼切)戚啥。
定義好模塊后,再看用require加載模塊的示例:
require(['yourModule1', 'yourModule2'], function (yourModule1, yourModule2) {
……
});
可以和CommonJS的require方法對比一下锉试,AMD的require多了一個參數(shù)callback猫十。
第一個參數(shù)是需要加載的模塊名,可以是一個數(shù)組键痛,意思是加載多個模塊炫彩。加載模塊時,如果該模塊的define里有[depends]參數(shù)絮短,就會先加載[depends]里指定的依賴模塊江兢。加載完[depends]里的依賴模塊后,運行define里的factory方法得到該模塊的實例丁频。然后將該實例依次傳入第二參數(shù)callback作為參數(shù)杉允。
第二個參數(shù)callback回調(diào)函數(shù),參數(shù)是根據(jù)第一個參數(shù)席里,依次加載模塊后得到的實例對象叔磷。等第一個參數(shù)指定的模塊全部加載完后,會執(zhí)行該callback奖磁。
require加載過程分幾步:
第一步改基,將依賴列表的模塊名轉(zhuǎn)換為URL,常見的是basePath + moduleID + ".js"咖为。例如:
require(["aaa", "bbb"], function(a, b){});
//將在require內(nèi)部被轉(zhuǎn)換成:
//require(["http://1.2.3.4/aaa.js",
// "http://1.2.3.4/bbb.js"], function(a, b){});
第二步秕狰,從檢測對象數(shù)組里查看該模塊是否被加載過(狀態(tài)是否為2),或正在被加載(狀態(tài)是否為1)躁染。只有從來沒加載過此節(jié)點鸣哀,才會進入加載流程。
第三步吞彤,將該模塊的狀態(tài)設(shè)為1我衬,表示正在被加載。創(chuàng)建script標簽饰恕,模塊的URL添加到src里挠羔,并綁定onload,onreadystatechange埋嵌,onerror事件褥赊,將script插入DOM樹中開始加載。瀏覽器加載完后會觸發(fā)綁定的on事件莉恼,里面可以將模塊的狀態(tài)改為2拌喉,表明已經(jīng)加載完畢。
第四步俐银,將模塊的URL尿背,依賴列表,狀態(tài)等構(gòu)建一個檢測對象數(shù)組捶惜,供第二步檢測用田藐。
你可以將所有需要用到模塊里變量和方法的代碼,都放到callback中吱七。require下面的代碼將繼續(xù)執(zhí)行汽久,這樣就能避免瀏覽器卡住假死的情況。
實現(xiàn)了AMD規(guī)范的JS庫有:require.js踊餐【按迹看一個用require.js的例子:
//myModule1.js
define(function() {
var m1 = {};
m1.say = function() {
console.log('Hi myModule1!');
}
return m1;
});
//myModule2.js
define(['myModule3'], function(m3) {
var m2 = {};
m2.say = function() {
m3.say();
console.log('Hi myModule2!');
}
return m2;
});
//myModule3.js
define(function() {
var m3 = {};
m3.say = function() {
console.log('Hi myModule3!');
}
return m3;
});
//HTML
console.log("before require");
require(['myModule1','myModule2'], function(m1, m2){
m1.say();
m2.say();
});
console.log("after require");
//before require
//after require
//Hi myModule1!
//Hi myModule3!
//Hi myModule2!
HTML中先執(zhí)行console.log(“before require”);
,打印出第一條吝岭。
然后執(zhí)行require三痰,因為require.js是AMD異步加載,所以執(zhí)行require后的console.log(“after require”);
語句窜管,打印出第二條散劫。
執(zhí)行require依次加載模塊。先加載myModule1幕帆。發(fā)現(xiàn)myModule1的define里無[depends]參數(shù)获搏,不依賴其他模塊。因此運行define里的factory方法獲得myModule1的實例對象m1失乾。
再加載myModule2常熙。發(fā)現(xiàn)myModule2依賴myModule3,因此加載myModule3仗扬。myModule3無[depends]參數(shù)症概,不依賴其他模塊。因此運行factory獲得myModule3的實例對象m3早芭。
加載完myModule3后彼城,運行myModule2的factory獲得myModule2的實例對象m2。
myModule2也加載完畢后退个,require的所有模塊均加載完畢募壕,運行回調(diào)函數(shù)。前面獲得的實例對象m1和m2作為參數(shù)傳給回調(diào)函數(shù)语盈。
運行m1.say();
打印出第三條
運行m2.say();
舱馅,根據(jù)方法定義,先運行m3.say();
打印出第四條刀荒,再運行console.log(‘Hi myModule2!’);
打印出第五條代嗤。
有些庫并沒有嚴格遵照AMD棘钞,用define()函數(shù)定義的模塊,此時需要用shim技術(shù)(所謂shim就是墊片干毅,即把一個新的API引入到一個舊環(huán)境中宜猜,并且僅靠舊環(huán)境中已有的手段來實現(xiàn))。例如require.js就為require.config定義了shim屬性硝逢,我們用它來加載并沒有采用AMD規(guī)范的underscore和backbone這兩個庫:
require.config({
shim: {
'underscore':{
exports: '_'
},
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
當然本篇并不是介紹require.js姨拥,更多require.js的用法請自行參照官網(wǎng)
總結(jié)
無論是被Nodejs采用的服務(wù)器端的CommonJS規(guī)范,還是客戶端的AMD規(guī)范渠鸽,都可以將我們自定義的模塊規(guī)范化叫乌,便于供他人使用。