电竞比分网-中国电竞赛事及体育赛事平台

分享

js模塊化歷程

 昵稱10504424 2015-10-10

 

服務(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ǔ),由于“革新派”與“?;逝伞钡乃枷霟o法達(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)容:
1. 全局有一個module變量,用來定義模塊
2. 通過module.declare方法來定義一個模塊
3. module.declare方法只接收一個參數(shù),那就是模塊的factory,次factory可以是函數(shù)也可以是對象,如果是對象,那么模塊輸出就是此對象。
4. 模塊的factory函數(shù)傳入三個參數(shù):require,exports,module,用來引入其他依賴和導(dǎo)出本模塊API
5. 如果factory函數(shù)最后明確寫有return數(shù)據(jù)(js函數(shù)中不寫return默認(rèn)返回undefined),那么return的內(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)容:
1. 用全局函數(shù)define來定義模塊,用法為:define(id?, dependencies?, factory);
2. id為模塊標(biāo)識,遵從CommonJS Module Identifiers規(guī)范
3. dependencies為依賴的模塊數(shù)組,在factory中需傳入形參與之一一對應(yīng)
4. 如果dependencies的值中有"require"、"exports"或"module",則與commonjs中的實(shí)現(xiàn)保持一致
5. 如果dependencies省略不寫,則默認(rèn)為["require", "exports", "module"],factory中也會默認(rèn)傳入require,exports,module
6. 如果factory為函數(shù),模塊對外暴漏API的方法有三種:return任意類型的數(shù)據(jù)、exports.xxx=xxx、module.exports=xxx
7. 如果factory為對象,則該對象即為模塊的返回值
基于以上幾點(diǎn)基本規(guī)范,我們便可以用這樣的方式來進(jìn)行模塊化組織代碼了:
復(fù)制代碼
//a.js
define(function(){
     console.log('a.js執(zhí)行');
     return {
          hello: function(){
               console.log('hello, a.js');
          }
     }
});
復(fù)制代碼
復(fù)制代碼
//b.js
define(function(){
     console.log('b.js執(zhí)行');
     return {
          hello: function(){
               console.log('hello, b.js');
          }
     }
});
復(fù)制代碼
復(fù)制代碼
//main.js
require(['a', 'b'], function(a, b){
     console.log('main.js執(zhí)行');
     a.hello();
     $('#b').click(function(){
          b.hello();
     });
})
復(fù)制代碼
上面的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來引入,如下:
復(fù)制代碼
define(function(){
     console.log('main2.js執(zhí)行');

     require(['a'], function(a){
          a.hello();    
     });

     $('#b').click(function(){
          require(['b'], function(b){
               b.hello();
          });
     });
});
復(fù)制代碼
我們在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í)在是太惡心了,他們更喜歡這樣來使用模塊:
復(fù)制代碼
var a = require('a');
a.hello();

$('#b').click(function(){
        var b = require('b');
        b.hello();
});
復(fù)制代碼
于是,AMD也終于決定作妥協(xié),兼容Modules/Wrappings的寫法,但只是部分兼容,例如并沒有使用module.declare來定義模塊,而還是用define,模塊的執(zhí)行時機(jī)也沒有改變,依舊是預(yù)先執(zhí)行。因此,AMD將此兼容稱為Simplified CommonJS wrapping,即并不是完整的實(shí)現(xiàn)Modules/Wrappings。
作了此兼容后,使用requirejs就可以這么寫代碼了:
復(fù)制代碼
//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();
               });
          }
     }
});
復(fù)制代碼
注意定義模塊時候的輕微差異,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定義模塊的寫法如下:
復(fù)制代碼
//a.js
define(function(require, exports, module){
     console.log('a.js執(zhí)行');
     return {
          hello: function(){
               console.log('hello, a.js');
          }
     }
});
復(fù)制代碼
復(fù)制代碼
//b.js
define(function(require, exports, module){
     console.log('b.js執(zhí)行');
     return {
          hello: function(){
               console.log('hello, b.js');
          }
     }
});
復(fù)制代碼
復(fù)制代碼
//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();
     });
    
});
復(fù)制代碼
定義模塊時無需羅列依賴數(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。另外有一個項目也提供了加載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ā)。
=======================

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多