JavaScript模塊規(guī)范(CommonJS,AMD)

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ī)范化叫乌,便于供他人使用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末徽缚,一起剝皮案震驚了整個濱河市憨奸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌猎拨,老刑警劉巖膀藐,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異红省,居然都是意外死亡额各,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門吧恃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虾啦,“玉大人,你說我怎么就攤上這事痕寓“磷恚” “怎么了?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵呻率,是天一觀的道長硬毕。 經(jīng)常有香客問我,道長礼仗,這世上最難降的妖魔是什么吐咳? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮元践,結(jié)果婚禮上韭脊,老公的妹妹穿的比我還像新娘。我一直安慰自己单旁,他們只是感情好沪羔,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著象浑,像睡著了一般蔫饰。 火紅的嫁衣襯著肌膚如雪琅豆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天死嗦,我揣著相機與錄音趋距,去河邊找鬼。 笑死越除,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的外盯。 我是一名探鬼主播摘盆,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼饱苟!你這毒婦竟也來了孩擂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤箱熬,失蹤者是張志新(化名)和其女友劉穎类垦,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體城须,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡蚤认,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了糕伐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片砰琢。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖良瞧,靈堂內(nèi)的尸體忽然破棺而出陪汽,到底是詐尸還是另有隱情,我是刑警寧澤褥蚯,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布挚冤,位于F島的核電站,受9級特大地震影響赞庶,放射性物質(zhì)發(fā)生泄漏训挡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一尘执、第九天 我趴在偏房一處隱蔽的房頂上張望舍哄。 院中可真熱鬧,春花似錦誊锭、人聲如沸表悬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蟆沫。三九已至籽暇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饭庞,已是汗流浹背戒悠。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留舟山,地道東北人绸狐。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像累盗,于是被迫代替她去往敵國和親寒矿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容