1.為什么需要模塊化
本段轉(zhuǎn)自https://segmentfault.com/a/1190000002591834合敦。
模塊化開發(fā)在編程開發(fā)中是一個(gè)非常重要的概念,一個(gè)優(yōu)秀的模塊化項(xiàng)目的后期維護(hù)成本可以大大降低验游。本文主要介紹JavaScript模塊化開發(fā)的那些事充岛,文中通過(guò)一個(gè)小故事比較直觀地闡述了模塊化開發(fā)的過(guò)程。
小A是某個(gè)創(chuàng)業(yè)團(tuán)隊(duì)的前端工程師耕蝉,負(fù)責(zé)編寫項(xiàng)目的Javascript程序崔梗。
全局變量沖突
根據(jù)自己的經(jīng)驗(yàn),小A先把一些常用的功能抽出來(lái)垒在,寫成函數(shù)放到一個(gè)公用文件base.js中:
var _ = {
$: function(id) { return document.getElementById(id); },
getCookie: function(key) { ... },
setCookie: function(key, value) { ... }
}
小A把這些函數(shù)都放在_對(duì)象內(nèi)蒜魄,以防過(guò)多的全局變量造成沖突。他告訴團(tuán)隊(duì)的其他成員场躯,如果誰(shuí)想使用這些函數(shù)权悟,只要引入base.js就可以了。
小C是小A的同事推盛,他向小A反映:自己的頁(yè)面引入了一個(gè)叫做underscore.js的類庫(kù),而且谦铃,這個(gè)類庫(kù)也會(huì)占用這個(gè)全局變量耘成,這樣一來(lái)就會(huì)跟base.js中的沖突了。小A心想驹闰,underscore.js是第三方類庫(kù)瘪菌,估計(jì)不好改,但是base.js已經(jīng)在很多頁(yè)面鋪開嘹朗,不可能改师妙。最后小A只好無(wú)奈地把underscore.js占用的全局變量改了。
此時(shí)屹培,小A發(fā)現(xiàn)默穴,把函數(shù)都放在一個(gè)名字空間內(nèi)怔檩,可以減少全局變量沖突的概率,卻沒有解決全局變量沖突這個(gè)問(wèn)題蓄诽。
依賴
隨著業(yè)務(wù)的發(fā)展薛训,小A又編寫了一系列的函數(shù)庫(kù)和UI組件,比方說(shuō)標(biāo)簽切換組件tabs.js仑氛,此組件需調(diào)用base.js以及util.js中的函數(shù)乙埃。
有一天,新同事小D跟小A反映锯岖,自己已經(jīng)在頁(yè)面中引用了tabs.js介袜,功能卻不正常。小A一看就發(fā)現(xiàn)問(wèn)題了出吹,原來(lái)小D不知道tabs.js依賴于base.js以及util.js遇伞,他并沒有添加這兩個(gè)文件的引用。于是趋箩,他馬上進(jìn)行修改:
<script src="tabs.js"></script>
<script src="base.js"></script>
<script src="util.js"></script>
然而赃额,功能還是不正常,此時(shí)小A教訓(xùn)小D說(shuō):“都說(shuō)是依賴叫确,那被依賴方肯定要放在依賴方之前啊”跳芳。原來(lái)小D把base.js和util.js放到tabs.js之后了。
小A心想竹勉,他是作者飞盆,自然知道組件的依賴情況,但是別人就難說(shuō)了次乓,特別是新人吓歇。
過(guò)了一段時(shí)間,小A給標(biāo)簽切換組件增加了功能票腰,為了實(shí)現(xiàn)這個(gè)功能城看,tabs.js還需要調(diào)用ui.js中的函數(shù)。這時(shí)杏慰,小A發(fā)現(xiàn)了一個(gè)嚴(yán)重的問(wèn)題测柠,他需要在所有調(diào)用了tabs.js的頁(yè)面上增加ui.js的引用!T道摹轰胁!
又過(guò)了一段時(shí)間,小A優(yōu)化了tabs.js朝扼,這個(gè)組件已經(jīng)不再依賴于util.js赃阀,所以他在所有用到tabs.js的頁(yè)面中移除了util.js的引用,以提高性能擎颖。他這一修改榛斯,出大事了观游,測(cè)試組MM告訴他,有些頁(yè)面不正常了肖抱。小A一看备典,恍然大悟,原來(lái)某些頁(yè)面的其他功能用到了util.js中的函數(shù)意述,他把這個(gè)文件的引用去掉導(dǎo)致出錯(cuò)了提佣。為了保證功能正常,他又把代碼恢復(fù)了荤崇。
小A又想拌屏,有沒有辦法在修改依賴的同時(shí)不用逐一修改頁(yè)面,也不影響其他功能呢术荤?
模塊化
小A逛互聯(lián)網(wǎng)的時(shí)候倚喂,無(wú)意中發(fā)現(xiàn)了一種新奇的模塊化編碼方式,可以把它之前遇到的問(wèn)題全部解決瓣戚。
在模塊化編程方式下端圈,每個(gè)文件都是一個(gè)模塊。每個(gè)模塊都由一個(gè)名為define的函數(shù)創(chuàng)建子库。例如舱权,把base.js改造成一個(gè)模塊后,代碼會(huì)變成這樣:
define(function(require, exports, module) {
exports.$ = function(id) { return document.getElementById(id); };
exports.getCookie = function(key) { ... };
exports.setCookie = function(key, value) { ... };
});
base.js向外提供的接口都被添加到exports這個(gè)對(duì)象仑嗅。而exports是一個(gè)局部變量宴倍,整個(gè)模塊的代碼都沒有占用半個(gè)全局變量。
那如何調(diào)用某個(gè)模塊提供的接口呢仓技?以tabs.js為例鸵贬,它要依賴于base.js和util.js:
define(function(require, exports, module) {
var _ = require('base.js'), util = require('util.js');
var div_tabs = _.$('tabs');
// .... 其他代碼
});
一個(gè)模塊可以通過(guò)局部函數(shù)require獲取其他模塊的接口。此時(shí)脖捻,變量和util都是局部變量阔逼,并且,變量名完全是受開發(fā)者控制的地沮,如果你不喜歡颜价,那也可以用base:
define(function(require, exports, module) {
var base = require('base.js'), util = require('util.js');
var div_tabs = base.$('tabs');
// .... 其他代碼
});
一旦要移除util.js、添加ui.js诉濒,那只要修改tabs.js就可以了:
define(function(require, exports, module) {
var base = require('base.js'), ui = require('ui.js');
var div_tabs = base.$('tabs');
// .... 其他代碼
});
注意:關(guān)于“模塊化”都是基于開發(fā)階段的,應(yīng)用階段不存在模塊化的概念夕春。
2.CommonJS
commonjs是一個(gè)模塊化的規(guī)范未荒。在CommonJS中,定義了一個(gè)全局性方法require()及志,用于加載模塊片排。假定有一個(gè)數(shù)學(xué)模塊math.js寨腔,就可以像下面這樣加載。
var math = require('math');
然后率寡,就可以調(diào)用模塊提供的方法:
var math = require('math');
math.add(2,3); // 5
CommonJS定義的模塊分為:{模塊引用(require)} {模塊定義(exports)} {模塊標(biāo)識(shí)(module)}
require()用來(lái)引入外部模塊迫卢;exports對(duì)象用于導(dǎo)出當(dāng)前模塊的方法或變量,唯一的導(dǎo)出口冶共;module對(duì)象就代表模塊本身乾蛤。如node.js的模塊化就是基于commonjs,還有browserify等捅僵。
3.AMD
基于commonJS規(guī)范的nodeJS出來(lái)以后家卖,服務(wù)端的模塊概念已經(jīng)形成,很自然地庙楚,大家就想要客戶端模塊上荡。而且最好兩者能夠兼容,一個(gè)模塊不用修改馒闷,在服務(wù)器和瀏覽器都可以運(yùn)行酪捡。但是,由于一個(gè)重大的局限纳账,使得CommonJS規(guī)范不適用于瀏覽器環(huán)境逛薇。還是上面的代碼,如果在瀏覽器中運(yùn)行塞祈,會(huì)有一個(gè)很大的問(wèn)題金刁,你能看出來(lái)嗎?
var math = require('math');
math.add(2, 3);
第二行math.add(2, 3)议薪,在第一行require('math')之后運(yùn)行尤蛮,因此必須等math.js加載完成。也就是說(shuō)斯议,如果加載時(shí)間很長(zhǎng)产捞,整個(gè)應(yīng)用就會(huì)停在那里等。您會(huì)注意到** require 是同步的**哼御。
這對(duì)服務(wù)器端不是一個(gè)問(wèn)題坯临,因?yàn)樗械哪K都存放在本地硬盤,可以同步加載完成恋昼,等待時(shí)間就是硬盤的讀取時(shí)間看靠。但是,對(duì)于瀏覽器液肌,這卻是一個(gè)大問(wèn)題挟炬,因?yàn)槟K都放在服務(wù)器端,等待時(shí)間取決于網(wǎng)速的快慢,可能要等很長(zhǎng)時(shí)間谤祖,瀏覽器處于"假死"狀態(tài)婿滓。
因此,瀏覽器端的模塊粥喜,不能采用"同步加載"(synchronous)凸主,只能采用"異步加載"(asynchronous)。這就是AMD規(guī)范誕生的背景额湘。
CommonJS是主要為了JS在后端的表現(xiàn)制定的卿吐,他是不適合前端的,AMD(異步模塊定義)出現(xiàn)了缩挑,它就主要為前端JS的表現(xiàn)制定規(guī)范但两。
AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"供置。它采用異步方式加載模塊谨湘,模塊的加載不影響它后面語(yǔ)句的運(yùn)行。所有依賴這個(gè)模塊的語(yǔ)句芥丧,都定義在一個(gè)回調(diào)函數(shù)中紧阔,等到加載完成之后,這個(gè)回調(diào)函數(shù)才會(huì)運(yùn)行续担。
AMD也采用require()語(yǔ)句加載模塊擅耽,但是不同于CommonJS,它要求兩個(gè)參數(shù):
require([module], callback);
第一個(gè)參數(shù)[module]物遇,是一個(gè)數(shù)組乖仇,里面的成員就是要加載的模塊;第二個(gè)參數(shù)callback询兴,則是加載成功之后的回調(diào)函數(shù)乃沙。如果將前面的代碼改寫成AMD形式,就是下面這樣:
require(['math','jquery'], function (math,$) {
math.add(2, 3);
});
math.add()與math模塊加載不是同步的诗舰,瀏覽器不會(huì)發(fā)生假死警儒。所以很顯然,AMD比較適合瀏覽器環(huán)境眶根。目前蜀铲,主要有兩個(gè)Javascript庫(kù)實(shí)現(xiàn)了AMD規(guī)范:require.js和curl.js.
AMD規(guī)范寫法
假定現(xiàn)在有一個(gè)math.js文件,它定義了一個(gè)math模塊属百。那么记劝,math.js就要這樣寫:
// math.js
define('math',function (){
var add = function (x,y){
return x+y;
};
return {
add: add
};
});
加載方法如下:
// main.js
require(['math'], function (math){
alert(math.add(1,1));
});
如果這個(gè)模塊還依賴其他模塊,那么define()函數(shù)的第一個(gè)參數(shù)族扰,必須是一個(gè)數(shù)組隆夯,指明該模塊的依賴性
define('模塊名',['myLib'], function(myLib){
function foo(){
myLib.doSomething();
}
return {
foo : foo
};
});
4.CMD
大名遠(yuǎn)揚(yáng)的玉伯寫了seajs钳恕,就是遵循他提出的CMD規(guī)范,與AMD蠻相近的蹄衷,不過(guò)用起來(lái)感覺更加方便些,最重要的是中文版厘肮,應(yīng)有盡有:seajs官方doc
define(function(require,exports,module){...});
用過(guò)seajs吧愧口,這個(gè)不陌生吧,對(duì)吧类茂。
前面說(shuō)AMD耍属,說(shuō)RequireJS實(shí)現(xiàn)了AMD,CMD看起來(lái)與AMD好像呀巩检,那RequireJS與SeaJS像不像呢厚骗?
雖然CMD與AMD蠻像的,但區(qū)別還是挺明顯的兢哭,官方非官方都有闡述和理解领舰,我覺得吧,說(shuō)的都挺好:
官方闡述SeaJS與RequireJS異同
SeaJS與RequireJS的最大異同(這個(gè)說(shuō)的也挺好)