服務(wù)端向前端進(jìn)軍Modules/1.0規(guī)范源于服務(wù)端,無法直接用于瀏覽器端,原因表現(xiàn)為:
1. 外層沒有function包裹,變量全暴漏在全局。如上面例子中increment.js中的add。
2. 資源的加載方式與服務(wù)端完全不同。服務(wù)端require一個模塊,直接就從硬盤或者內(nèi)存中讀取了,消耗的時間可以忽略。而瀏覽器則不同,需要從服務(wù)端來下載這個文件,然后運(yùn)行里面的代碼才能得到API,需要花費(fèi)一個http請求,也就是說,require后面的一行代碼,需要資源請求完成才能執(zhí)行。由于瀏覽器端是以插入<script>標(biāo)簽的形式來加載資源的(ajax方式不行,有跨域問題),沒辦法讓代碼同步執(zhí)行,所以像commonjs那樣的寫法會直接報錯。
所以,社區(qū)意識到,要想在瀏覽器環(huán)境中也能模塊化,需要對規(guī)范進(jìn)行升級。順便說一句,CommonJs原來是叫ServerJs,從名字可以看出是專攻服務(wù)端的,為了統(tǒng)一前后端而改名CommonJs。(論起名的重要性~)
而就在社區(qū)討論制定下一版規(guī)范的時候,內(nèi)部發(fā)生了比較大的分歧,分裂出了三個主張,漸漸的形成三個不同的派別:
1.Modules/1.x派
這一波人認(rèn)為,在現(xiàn)有基礎(chǔ)上進(jìn)行改進(jìn)即可滿足瀏覽器端的需要,既然瀏覽器端需要function包裝,需要異步加載,那么新增一個方案,能把現(xiàn)有模塊轉(zhuǎn)化為適合瀏覽器端的就行了,有點(diǎn)像“?;逝伞?。基于這個主張,制定了Modules/Transport(http://wiki./wiki/Modules/Transport)規(guī)范,提出了先通過工具把現(xiàn)有模塊轉(zhuǎn)化為復(fù)合瀏覽器上使用的模塊,然后再使用的方案。
browserify就是這樣一個工具,可以把nodejs的模塊編譯成瀏覽器可用的模塊。(Modules/Transport規(guī)范晦澀難懂,我也不確定browserify跟它是何關(guān)聯(lián),有知道的朋友可以講一下)
目前的最新版是Modules/1.1.1(http://wiki./wiki/Modules/1.1.1),增加了一些require的屬性,以及模塊內(nèi)增加module變量來描述模塊信息,變動不大。
2. Modules/Async派
這一波人有點(diǎn)像“革新派”,他們認(rèn)為瀏覽器與服務(wù)器環(huán)境差別太大,不能沿用舊的模塊標(biāo)準(zhǔn)。既然瀏覽器必須異步加載代碼,那么模塊在定義的時候就必須指明所依賴的模塊,然后把本模塊的代碼寫在回調(diào)函數(shù)里。模塊的加載也是通過下載-回調(diào)這樣的過程來進(jìn)行,這個思想就是AMD的基礎(chǔ),由于“革新派”與“保皇派”的思想無法達(dá)成一致,最終從CommonJs中分裂了出去,獨(dú)立制定了瀏覽器端的js模塊化規(guī)范AMD(Asynchronous Module Definition)(https://github.com/amdjs/amdjs-api/wiki/AMD)
本文后續(xù)會繼續(xù)討論AMD規(guī)范的內(nèi)容。
3. Modules/2.0派
這一波人有點(diǎn)像“中間派”,既不想丟掉舊的規(guī)范,也不想像AMD那樣推到重來。他們認(rèn)為,Modules/1.0固然不適合瀏覽器,但它里面的一些理念還是很好的,(如通過require來聲明依賴),新的規(guī)范應(yīng)該兼容這些,AMD規(guī)范也有它好的地方(例如模塊的預(yù)先加載以及通過return可以暴漏任意類型的數(shù)據(jù),而不是像commonjs那樣exports只能為object),也應(yīng)采納。最終他們制定了一個Modules/Wrappings(http://wiki./wiki/Modules/Wrappings)規(guī)范,此規(guī)范指出了一個模塊應(yīng)該如何“包裝”,包含以下內(nèi)容:
使用該規(guī)范的例子看起來像這樣:
//可以使用exprots來對外暴漏API module.declare(function(require, exports, module) { exports.foo = "bar"; }); //也可以直接return來對外暴漏數(shù)據(jù) module.declare(function(require) { return { foo: "bar" }; }); AMD/RequireJs的崛起與妥協(xié)AMD的思想正如其名,異步加載所需的模塊,然后在回調(diào)函數(shù)中執(zhí)行主邏輯。這正是我們在瀏覽器端開發(fā)所習(xí)慣了的方式,其作者親自實(shí)現(xiàn)了符合AMD規(guī)范的requirejs,AMD/RequireJs迅速被廣大開發(fā)者所接受。
AMD規(guī)范包含以下內(nèi)容:
基于以上幾點(diǎn)基本規(guī)范,我們便可以用這樣的方式來進(jìn)行模塊化組織代碼了:
//a.js define(function(){ console.log('a.js執(zhí)行'); return { hello: function(){ console.log('hello, a.js'); } } }); //b.js define(function(){ console.log('b.js執(zhí)行'); return { hello: function(){ console.log('hello, b.js'); } } }); //main.js require(['a', 'b'], function(a, b){ console.log('main.js執(zhí)行'); a.hello(); $('#b').click(function(){ b.hello(); }); }) 上面的main.js被執(zhí)行的時候,會有如下的輸出:
a.js執(zhí)行
b.js執(zhí)行 main.js執(zhí)行 hello, a.js 在點(diǎn)擊按鈕后,會輸出:
hello, b.js
這結(jié)局,如你所愿嗎?大體來看,是沒什么問題的,因?yàn)槟阋膬蓚€hello方法都正確的執(zhí)行了。
但是如果細(xì)細(xì)來看,b.js被預(yù)先加載并且預(yù)先執(zhí)行了,(第二行輸出),b.hello這個方法是在點(diǎn)擊了按鈕之后才會執(zhí)行,如果用戶壓根就沒點(diǎn),那么b.js中的代碼應(yīng)不應(yīng)該執(zhí)行呢?
這其實(shí)也是AMD/RequireJs被吐槽的一點(diǎn),預(yù)先下載沒什么爭議,由于瀏覽器的環(huán)境特點(diǎn),被依賴的模塊肯定要預(yù)先下載的。問題在于,是否需要預(yù)先執(zhí)行?如果一個模塊依賴了十個其他模塊,那么在本模塊的代碼執(zhí)行之前,要先把其他十個模塊的代碼都執(zhí)行一遍,不管這些模塊是不是馬上會被用到。這個性能消耗是不容忽視的。
另一點(diǎn)被吐槽的是,在定義模塊的時候,要把所有依賴模塊都羅列一遍,而且還要在factory中作為形參傳進(jìn)去,要寫兩遍很大一串模塊名稱,像這樣:
define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){ ..... })
編碼過程略有不爽。
好的一點(diǎn)是,AMD保留了commonjs中的require、exprots、module這三個功能(上面提到的第4條)。你也可以不把依賴羅列在dependencies數(shù)組中。而是在代碼中用require來引入,如下:
define(function(){ console.log('main2.js執(zhí)行'); require(['a'], function(a){ a.hello(); }); $('#b').click(function(){ require(['b'], function(b){ b.hello(); }); }); }); 我們在define的參數(shù)中未寫明依賴,那么main2.js在執(zhí)行的時候,就不會預(yù)先加載a.js和b.js,只是執(zhí)行到require語句的時候才會去加載,上述代碼的輸出如下:
main2.js執(zhí)行
a.js執(zhí)行 hello, a.js 可以看到b.js并未執(zhí)行,從網(wǎng)絡(luò)請求中看,b.js也并未被下載。只有在按鈕被點(diǎn)擊的時候b.js才會被下載執(zhí)行,并且在回調(diào)函數(shù)中執(zhí)行模塊中的方法。這就是名副其實(shí)的“懶加載”了。
這樣的懶加載無疑會大大減輕初始化時的損耗(下載和執(zhí)行都被省去了),但是弊端也是顯而易見的,在后續(xù)執(zhí)行a.hello和b.hello時,必須得實(shí)時下載代碼然后在回調(diào)中才能執(zhí)行,這樣的用戶體驗(yàn)是不好的,用戶的操作會有明顯的延遲卡頓。
但這樣的現(xiàn)實(shí)并非是無法接受的,畢竟是瀏覽器環(huán)境,我們已經(jīng)習(xí)慣了操作網(wǎng)頁時伴隨的各種loading。。。
但是話說過來,有沒有更好的方法來處理問題呢?資源的下載階段還是預(yù)先進(jìn)行,資源執(zhí)行階段后置,等到需要的時候再執(zhí)行。這樣一種折衷的方式,能夠融合前面兩種方式的優(yōu)點(diǎn),而又回避了缺點(diǎn)。
這就是Modules/Wrappings規(guī)范,還記得前面提到的“中間派”嗎?
在AMD的陣營中,也有一部分人提出這樣的觀點(diǎn),代碼里寫一堆回調(diào)實(shí)在是太惡心了,他們更喜歡這樣來使用模塊:
于是,AMD也終于決定作妥協(xié),兼容Modules/Wrappings的寫法,但只是部分兼容,例如并沒有使用module.declare來定義模塊,而還是用define,模塊的執(zhí)行時機(jī)也沒有改變,依舊是預(yù)先執(zhí)行。因此,AMD將此兼容稱為Simplified CommonJS wrapping,即并不是完整的實(shí)現(xiàn)Modules/Wrappings。
作了此兼容后,使用requirejs就可以這么寫代碼了:
//d.js define(function(require, exports, module){ console.log('d.js執(zhí)行'); return { helloA: function(){ var a = require('a'); a.hello(); }, run: function(){ $('#b').click(function(){ var b = require('b'); b.hello(); }); } } }); 注意定義模塊時候的輕微差異,dependencies數(shù)組為空,但是factory函數(shù)的形參必須手工寫上require,exports,module,(這不同于之前的dependencies和factory形參全不寫),這樣寫即可使用Simplified CommonJS wrapping風(fēng)格,與commonjs的格式一致了。
雖然使用上看起來簡單,然而在理解上卻給后人埋下了一個大坑。因?yàn)锳MD只是支持了這樣的語法,而并沒有真正實(shí)現(xiàn)模塊的延后執(zhí)行。什么意思呢?上面的代碼,正常來講應(yīng)該是預(yù)先下載a.js和b.js,然后在執(zhí)行模塊的helloA方法的時候開始執(zhí)行a.js里面的代碼,在點(diǎn)擊按鈕的時候開始執(zhí)行b.js中的方法。實(shí)際卻不是這樣,只要此模塊被別的模塊引入,a.js和b.js中的代碼還是被預(yù)先執(zhí)行了。
我們把上面的代碼命名為d.js,在別的地方使用它:
require(['d'], function(d){
});
上面的代碼會輸出
a.js執(zhí)行
b.js執(zhí)行 d.js執(zhí)行 可以看出,盡管還未調(diào)用d模塊的API,里面所依賴的a.js和b.js中的代碼已經(jīng)執(zhí)行了。AMD的這種只實(shí)現(xiàn)語法卻未真正實(shí)現(xiàn)功能的做法容易給人造成理解上的困難,被強(qiáng)烈吐槽。
(在requirejs2.0中,作者聲明已經(jīng)處理了此問題(https://github.com/jrburke/requirejs/wiki/Upgrading-to-RequireJS-2.0#delayed),但是我用2.1.20版測試的時候還是會預(yù)先執(zhí)行,我有點(diǎn)不太明白原因,如果有懂的高手請指教)
兼容并包的CMD/seajs既然requirejs有上述種種不甚優(yōu)雅的地方,所以必然會有新東西來完善它,這就是后起之秀seajs,seajs的作者是國內(nèi)大牛淘寶前端步道者玉伯。seajs全面擁抱Modules/Wrappings規(guī)范,不用requirejs那樣回調(diào)的方式來編寫模塊。而它也不是完全按照Modules/Wrappings規(guī)范,seajs并沒有使用declare來定義模塊,而是使用和requirejs一樣的define,或許作者本人更喜歡這個名字吧。(然而這或多或少又會給人們造成理解上的混淆),用seajs定義模塊的寫法如下:
//a.js define(function(require, exports, module){ console.log('a.js執(zhí)行'); return { hello: function(){ console.log('hello, a.js'); } } }); //b.js define(function(require, exports, module){ console.log('b.js執(zhí)行'); return { hello: function(){ console.log('hello, b.js'); } } }); //main.js define(function(require, exports, module){ console.log('main.js執(zhí)行'); var a = require('a'); a.hello(); $('#b').click(function(){ var b = require('b'); b.hello(); }); }); 定義模塊時無需羅列依賴數(shù)組,在factory函數(shù)中需傳入形參require,exports,module,然后它會調(diào)用factory函數(shù)的toString方法,對函數(shù)的內(nèi)容進(jìn)行正則匹配,通過匹配到的require語句來分析依賴,這樣就真正實(shí)現(xiàn)了commonjs風(fēng)格的代碼。
上面的main.js執(zhí)行會輸出如下:
main.js執(zhí)行
a.js執(zhí)行 hello, a.js a.js和b.js都會預(yù)先下載,但是b.js中的代碼卻沒有執(zhí)行,因?yàn)檫€沒有點(diǎn)擊按鈕。當(dāng)點(diǎn)擊按鈕的時候,會輸出如下:
b.js執(zhí)行
hello, b.js 可以看到b.js中的代碼此時才執(zhí)行。這樣就真正實(shí)現(xiàn)了“就近書寫,延遲執(zhí)行“,不可謂不優(yōu)雅。
如果你一定要挑出一點(diǎn)不爽的話,那就是b.js的預(yù)先下載了。你可能不太想一開始就下載好所有的資源,希望像requirejs那樣,等點(diǎn)擊按鈕的時候再開始下載b.js。本著兼容并包的思想,seajs也實(shí)現(xiàn)了這一功能,提供require.async API,在點(diǎn)擊按鈕的時候,只需這樣寫:
var b = require.async('b');
b.hello();
b.js就不會在一開始的時候就加載了。這個API可以說是簡單漂亮。
關(guān)于模塊對外暴漏API的方式,seajs也是融合了各家之長,支持commonjs的exports.xxx = xxx和module.exports = xxx的寫法,也支持AMD的return寫法,暴露的API可以是任意類型。
你可能會覺得seajs無非就是一個抄,把別人家的優(yōu)點(diǎn)都抄過來組合了一下。其實(shí)不然,seajs是commonjs規(guī)范在瀏覽器端的踐行者,對于requirejs的優(yōu)點(diǎn)也加以吸收。看人家的名字,就是海納百川之意。(再論起名的重要性~),既然它的思想是海納百川,討論是不是抄就沒意義了。
鑒于seajs融合了太多的東西,已經(jīng)無法說它遵循哪個規(guī)范了,所以玉伯干脆就自立門戶,起名曰CMD(Common Module Definition)規(guī)范,有了綱領(lǐng),就不會再存在非議了。
面向未來的ES6模塊標(biāo)準(zhǔn)既然模塊化開發(fā)的呼聲這么高,作為官方的ECMA必然要有所行動,js模塊很早就列入草案,終于在2015年6月份發(fā)布了ES6正式版。然而,可能由于所涉及的技術(shù)還未成熟,ES6移除了關(guān)于模塊如何加載/執(zhí)行的內(nèi)容,只保留了定義、引入模塊的語法。所以說現(xiàn)在的ES6 Module還只是個雛形,半成品都算不上。但是這并不妨礙我們先窺探一下ES6模塊標(biāo)準(zhǔn)。
定義一個模塊不需要專門的工作,因?yàn)橐粋€模塊的作用就是對外提供API,所以只需用exoprt導(dǎo)出就可以了:
//方式一, a.js export var a = 1; export var obj = {name: 'abc', age: 20}; export function run(){....} //方式二, b.js var a = 1; var obj = {name: 'abc', age: 20}; function run(){....} export {a, obj, run} 使用模塊的時候用import關(guān)鍵字,如:
import {run as go} from 'a' run() 如果想要使用模塊中的全部API,也可以不必把每個都列一遍,使用module關(guān)鍵字可以全部引入,用法:
module foo from 'a' console.log(foo.obj); a.run(); 在花括號中指明需使用的API,并且可以用as指定別名。
ES6 Module的基本用法就是這樣,可以看到確實(shí)是有些薄弱,而且目前還沒有瀏覽器能支持,只能說它是面向未來了。
目前我們可以使用一些第三方模塊來對ES6進(jìn)行編譯,轉(zhuǎn)化為可以使用的ES5代碼,或者是符合AMD規(guī)范的模塊,例如ES6 module transpiler。另外有一個項(xiàng)目也提供了加載ES6模塊的方法,es6-module-loader(https://github.com/ModuleLoader/es6-module-loader),不過這都是一些臨時的方案,或許明年ES7一發(fā)布,模塊的加載有了標(biāo)準(zhǔn),瀏覽器給與了實(shí)現(xiàn),這些工具也就沒有用武之地了。
未來還是很值得期待的,從語言的標(biāo)準(zhǔn)上支持模塊化,js就可以更加自信的走進(jìn)大規(guī)模企業(yè)級開發(fā)。
=======================
參考資料:
|
|
來自: 昵稱10504424 > 《工作》