前端模塊化框架肩負(fù)著 模塊管理、資源加載兩項(xiàng)重要的功能篇裁,這兩項(xiàng)功能與工具沛慢、性能、業(yè)務(wù)达布、部署等工程環(huán)節(jié)都有著非常緊密的聯(lián)系团甲。因此,模塊化框架的設(shè)計(jì)應(yīng)該最高優(yōu)先級(jí)考慮工程需要黍聂。
CMD
CMD 模塊依賴(lài)聲明方式:
define(function (require) {
var a = require('./a');
var b = require('./b'); // more code ..})
CMD 依賴(lài)是就近聲明躺苦,通過(guò)內(nèi)部require
方法進(jìn)行聲明身腻。但是因?yàn)槭钱惒侥K,加載器需要提前加載這些模塊匹厘,所以模塊真正使用前需要提取模塊里面所有的依賴(lài)嘀趟。無(wú)論是加載器即時(shí)提取,還是通過(guò)自動(dòng)化工具預(yù)先提取愈诚,CMD 的這種依賴(lài)聲明格式只能通過(guò)靜態(tài)分析方式實(shí)現(xiàn)她按,這也正是 CMD 的弊端所在。
CMD 規(guī)范的弊端
不能直接壓縮:require
是局部變量炕柔,意味著不能直接的通過(guò)壓縮工具進(jìn)行壓縮酌泰,若require
這個(gè)變量被替換,加載器與自動(dòng)化工具將無(wú)法獲取模塊的依賴(lài)匕累。
模塊書(shū)寫(xiě)有額外約定:路徑參數(shù)不能進(jìn)行字符串運(yùn)算陵刹,不能使用變量代替,否則加載器與自動(dòng)化工具無(wú)法正確提取路徑欢嘿。
規(guī)范之外的約定意味著更多的文檔說(shuō)明衰琐,除非它們也是規(guī)范中的一部分。
注:SeaJS 靜態(tài)分析實(shí)現(xiàn)是把模塊包toString()
后使用正則提取require
部分得到依賴(lài)的模塊路徑际插。
AMD
AMD 模塊依賴(lài)聲明方式:
define(['./a', './b'], function (a, b) {
// more code ..
})
AMD 的依賴(lài)是提前聲明碘耳。這種優(yōu)勢(shì)的好處就是依賴(lài)無(wú)需通過(guò)靜態(tài)分析,無(wú)論是加載器還是自動(dòng)化工具都可以很直接的獲取到依賴(lài)框弛,規(guī)范的定義可以更簡(jiǎn)單辛辨,意味著可能產(chǎn)生更強(qiáng)大的實(shí)現(xiàn),這對(duì)加載器與自動(dòng)化分析工具都是有利的瑟枫。
AMD 規(guī)范的弊端
依賴(lài)提前聲明在代碼書(shū)寫(xiě)上不是那么友好
模塊內(nèi)部與 NodeJS 的 Modules 有一定的差異
關(guān)于第二點(diǎn)的問(wèn)題需要特別說(shuō)明下斗搞。其實(shí)無(wú)論是 CMD 還是 AMD 的異步模塊,都無(wú)法與同步模塊規(guī)范保持一致(NodeJS 的 Modules)慷妙,只有誰(shuí)比誰(shuí)更像同步模塊而已僻焚。AMD 要轉(zhuǎn)換為同步模塊,除了去掉define
函數(shù)的包裹外膝擂,需要在頭部使用require
把依賴(lài)聲明好虑啤,而 CMD 只需要去掉define
函數(shù)的包裹即可。
從規(guī)范上來(lái)說(shuō)架馋,AMD 更加簡(jiǎn)單且嚴(yán)謹(jǐn)狞山,適用性更廣,而在 RequireJS 強(qiáng)力的推動(dòng)下叉寂,在國(guó)外幾乎成了事實(shí)上的異步模塊標(biāo)準(zhǔn)萍启,各大類(lèi)庫(kù)也相繼支持 AMD 規(guī)范。
但從 SeaJS 與 CMD 來(lái)說(shuō),也做了很多不錯(cuò)東西:1勘纯、相對(duì)自然的依賴(lài)聲明風(fēng)格 2局服、小而美的內(nèi)部實(shí)現(xiàn) 3、貼心的外圍功能設(shè)計(jì) 4驳遵、更好的中文社區(qū)支持
模塊化框架在工程方面的缺點(diǎn):
-
requirejs和seajs二者在加載上都有缺陷淫奔,就是模塊的依賴(lài)要等到模塊加載完成后,通過(guò)靜態(tài)分析(seajs)或者deps參數(shù)(requirejs)來(lái)獲取堤结,這就為 合并請(qǐng)求 和 按需加載 帶來(lái)了實(shí)現(xiàn)上的矛盾:
- 要么放棄按需加載搏讶,把所有js合成一個(gè)文件,從而滿(mǎn)足請(qǐng)求合并(兩個(gè)框架的官方demo都有這樣的例子)霍殴;
- 要么放棄請(qǐng)求合并,請(qǐng)求獨(dú)立的模塊文件系吩,從而滿(mǎn)足按需加載来庭。
AMD規(guī)范在執(zhí)行callback的時(shí)候,要初始化所有依賴(lài)的模塊穿挨,而CMD只有執(zhí)行到require的時(shí)候才初始化模塊月弛。所以用AMD實(shí)現(xiàn)某種if-else邏輯分支加載不同的模塊的時(shí)候,就會(huì)比較麻煩了科盛∶毖茫考慮這種情況:
//AMD for SPArequire(['page/index', 'page/detail'], function(index, detail){
//在執(zhí)行回調(diào)之前,index和detail模塊的factory均執(zhí)行過(guò)了 switch(location.hash){
case '#index':
index();
break;
case '#detail':
detail();
break;
}});
在執(zhí)行回調(diào)之前贞绵,已經(jīng)同時(shí)執(zhí)行了index和detail模塊的factory厉萝,而CMD只有執(zhí)行到require才會(huì)調(diào)用對(duì)應(yīng)模塊的factory。這種差別帶來(lái)的不僅僅是性能上的差異榨崩,也可能為開(kāi)發(fā)增加一點(diǎn)小麻煩谴垫,比如不方便實(shí)現(xiàn)換膚功能,factory注意不要直接操作dom等母蛛。當(dāng)然翩剪,我們可以多層嵌套require來(lái)解決這個(gè)問(wèn)題,但又會(huì)引起模塊請(qǐng)求串行的問(wèn)題彩郊。
結(jié)論:以純前端方式實(shí)現(xiàn)模塊化框架 不能 同時(shí)滿(mǎn)足 按需加載前弯,請(qǐng)求合并
和 依賴(lài)管理 三個(gè)需求醉者。導(dǎo)致這個(gè)問(wèn)題的根本原因是 純前端方式只能在運(yùn)行時(shí)分析依賴(lài)關(guān)系瘫拣。
解決模塊化管理的新思路
由于根本問(wèn)題出在 運(yùn)行時(shí)分析依賴(lài)暑脆,因此新思路的策略很簡(jiǎn)單:不在運(yùn)行時(shí)分析依賴(lài)箍铭。這就要借助 構(gòu)建工具做線下分析了颅崩,其基本原理就是:
利用構(gòu)建工具在線下進(jìn)行 模塊依賴(lài)分析赵誓,然后把依賴(lài)關(guān)系數(shù)據(jù)寫(xiě)入到構(gòu)建結(jié)果中菲宴,并調(diào)用模塊化框架的 依賴(lài)關(guān)系聲明接口 艰管,實(shí)現(xiàn)模塊管理前方、請(qǐng)求合并以及按需加載等功能狈醉。
舉個(gè)例子廉油,假設(shè)我們有一個(gè)這樣的工程:
project
├ lib
│ └ xmd.js #模塊化框架
├ mods #模塊目錄
│ ├ a.js
│ ├ b.js
│ ├ c.js
│ ├ d.js
│ └ e.js
└ index.html #入口頁(yè)面
工程中,index.html的源碼內(nèi)容為:
<!doctype html>
...
<script src="lib/xmd.js"></script> <!-- 模塊化框架 -->
<script>
//等待構(gòu)建工具生成數(shù)據(jù)替換 `__FRAMEWORK_CONFIG__' 變量 require.config(__FRAMEWORK_CONFIG__);
</script>
<script>
//用戶(hù)代碼苗傅,異步加載模塊
require.async(['a', 'e'], function(a, e){
//do something with a and e.
});
</script>...
工程中抒线,mods/a.js 的源碼內(nèi)容為(采用類(lèi)似CMD的書(shū)寫(xiě)規(guī)范):
define('a', function(require, exports, module){
console.log('a.init');
var b = require('b');
var c = require('c');
exports.run = function(){
//do something with b and c.
console.log('a.run');
};
});
具體實(shí)現(xiàn)過(guò)程
- 用工具在下線對(duì)工程文件進(jìn)行掃描,得到依賴(lài)關(guān)系表:
{
"a" : [ "b", "c" ],
"b" : [ "d" ]}
- 工具把依賴(lài)表構(gòu)建到頁(yè)面或者腳本中渣慕,并調(diào)用模塊化框架的配置接口嘶炭,index.html的構(gòu)建結(jié)果為:
<!doctype html>
...
<script src="lib/xmd.js"></script> <!-- 模塊化框架 -->
<script>
//構(gòu)建工具生成的依賴(lài)數(shù)據(jù)
require.config({
"deps" : {
"a" : [ "b", "c" ],
"b" : [ "d" ]
}
});
</script>
<script>
//用戶(hù)代碼,異步加載模塊
require.async(['a', 'e'], function(a, e){
//do something with a and e.
});
</script>
- 模塊化框架根據(jù)依賴(lài)表加載資源逊桦,比如上述例子眨猎,入口需要加載a、e兩個(gè)模塊强经,查表得知完整依賴(lài)關(guān)系睡陪,配合combo服務(wù),可以發(fā)起一個(gè)合并后的請(qǐng)求:
http://www.example.com/??d.js,b.js,c.js,a.js,e.js
先來(lái)看一下這種方案的優(yōu)點(diǎn)
采用類(lèi)似CMD的書(shū)寫(xiě)規(guī)范(同步require函數(shù)聲明依賴(lài))匿情,可以在執(zhí)行到require語(yǔ)句的時(shí)候才調(diào)用模塊的factory兰迫。
雖然采用CMD書(shū)寫(xiě)規(guī)范,但放棄了運(yùn)行時(shí)分析依賴(lài)炬称,改成工具輸出依賴(lài)表汁果,因此 依賴(lài)分析完成后可以壓縮掉require關(guān)鍵字
框架并沒(méi)有嚴(yán)格依賴(lài)工具,它只是約定了一種數(shù)據(jù)結(jié)構(gòu)玲躯。不使用工具据德,人工維護(hù) require.config({...})相關(guān)的數(shù)據(jù)也是可以的。對(duì)于小項(xiàng)目跷车,文件全部合并的情況晋控,更加不需要deps表了,只要在入口的require.async調(diào)用之前加載所有模塊化的文件姓赤,依賴(lài)關(guān)系無(wú)需額外維護(hù)
構(gòu)建工具設(shè)計(jì)非常簡(jiǎn)單赡译,而且可靠。工作就是掃描模塊文件目錄不铆,得到依賴(lài)表蝌焚,JSON序列化之后插入到構(gòu)建代碼中
由于框架預(yù)先知道所有模塊的依賴(lài)關(guān)系,因此可以借助combo服務(wù)實(shí)現(xiàn)請(qǐng)求合并誓斥,而不用等到一級(jí)模塊加載完成才能知道后續(xù)的依賴(lài)關(guān)系只洒。
如果構(gòu)建工具可以自動(dòng)包裝define函數(shù),那么整個(gè)系統(tǒng)開(kāi)發(fā)起來(lái)會(huì)感覺(jué)跟nodejs非常接近劳坑,比較舒服毕谴。
再來(lái)討論一下這種方案的缺點(diǎn):
由于采用require函數(shù)作為依賴(lài)標(biāo)記,因此如果需要變量方式require,需要額外聲明涝开,這個(gè)時(shí)候可以實(shí)現(xiàn)兼容AMD規(guī)范寫(xiě)法循帐,比如
define('a', ['b', 'c'], function(require, exports, module){
console.log('a.init');
var name = isIE ? 'b' : 'c';
var mod = require(name);
exports.run = function(){
//do something with mod.
console.log('a.run');
};})
只要工具把define函數(shù)中的 deps
參數(shù),或者factory內(nèi)的require都作為依賴(lài)聲明標(biāo)記來(lái)識(shí)別舀武,這樣工程性就比較完備了拄养。
問(wèn)題
- 但是在網(wǎng)購(gòu)型系統(tǒng)里,很可能頂部的購(gòu)物車(chē)银舱、支付模塊等瘪匿,不是來(lái)自本系統(tǒng),而是來(lái)自其他業(yè)務(wù)部門(mén)寻馏,這些東西卻非要集成在一個(gè)頁(yè)面里棋弥,它們的公共項(xiàng)就很難處理。所以我理解阿里把模塊拆得這么碎诚欠,然后用看上去很怪異的方式嘁锯,在nginx那邊搞combiner來(lái)合并,然后也正是為此聂薪,可能js會(huì)有亂序,必須晚期依賴(lài)蝗羊。
答:
對(duì)于大型系統(tǒng)藏澳,都不是整站構(gòu)建的,而是按業(yè)務(wù)拆成了很多個(gè)子系統(tǒng)耀找,每個(gè)產(chǎn)品會(huì)產(chǎn)生一張資源表翔悠,跨業(yè)務(wù)的依賴(lài)會(huì)引入對(duì)應(yīng)產(chǎn)品庫(kù)的表,每個(gè)業(yè)務(wù)子系統(tǒng)是獨(dú)立構(gòu)建上線的野芒。舉個(gè)例子:
每個(gè)子系統(tǒng)獨(dú)立構(gòu)建蓄愁,并產(chǎn)生獨(dú)立的表,線上部署的大致效果為:
每個(gè)子系統(tǒng)的靜態(tài)資源id結(jié)構(gòu)為: 系統(tǒng)名:資源id狞悲,比如common系統(tǒng)下的jquery代碼撮抓,其id為 common:lib/jquery/jquery-2.0.2.js,所有的依賴(lài)關(guān)系可以記錄在模板或模板所引用的js中的摇锋,模板中提供了靜態(tài)資源管理和加載的接口丹拯,比如user子系統(tǒng)中希望使用message系統(tǒng)下的資源,其代碼為(在user.git下的widget/user-info/user-info.php):
模板中的import函數(shù)荸恕,會(huì)在運(yùn)行時(shí)讀取資源表來(lái)實(shí)現(xiàn)靜態(tài)資源按需乖酬,資源表中也記錄了子系統(tǒng)內(nèi)代碼的合并情況,可以在模板運(yùn)行期間計(jì)算靜態(tài)資源的最優(yōu)組合(帶寬融求、請(qǐng)求數(shù)等)
每個(gè)系統(tǒng)獨(dú)立構(gòu)建咬像,只有運(yùn)行時(shí)的交叉引用,不會(huì)出現(xiàn)整站構(gòu)建的情況
-
樓主對(duì)Browserify這個(gè)解決方案怎么看?