這是一篇關於js模塊化歷程的長長的流水帳,記錄js模塊化思想的誕生與變遷,展望ES6模塊化標準的將來。經歷過這段歷史的人或許會感到滄桑,沒經歷過的人也應該知道這段歷史。html
1、無模塊時代前端
在ajax還未提出以前,JS還主要用來在網頁上進行表單校驗、實現簡單的動畫效果等等,你能夠回想一下那個網頁上處處有公告塊飄來飄去的時代。java
這個時候並無前端工程師,服務端工程師只需在頁面上隨便寫寫js就能搞定需求。那個時候的前端代碼大概像這樣:node
if(xx){ //....... } else{ //xxxxxxxxxxx } for(var i=0; i<10; i++){ //........ } element.onclick = function(){ //....... }
代碼簡單的堆在一塊兒,只要能從上往下依次執行就能夠了。jquery
2、萌芽時代git
2006年,ajax的概念被提出,前端擁有了主動向服務端發送請求並操做返回數據的能力,隨着Google將此概念的發揚光大,傳統的網頁慢慢的向「富客戶端」發展。前端的業務邏輯愈來愈多,代碼也愈來愈多,因而一些問題就暴漏了出來:es6
一、全局變量災難github
二、函數命名衝突ajax
三、複雜依賴關係(如,jquery插件依賴jquery,就必須在引入插件以前引入jquery。項目愈來愈大時,簡直是災難)後端
萌芽時代的解決方案:
一、用自執行函數來包裝代碼
modA = function(){ var a,b; //變量a、b外部不可見 return { add : function(c){ a + b + c; }, format: function(){ //...... } } }()
優勢:function內部的變量就對全局隱藏了,達到是封裝的目的。
缺陷:modA這個變量仍是暴漏到全局了,隨着模塊的增多,全局變量仍是會愈來愈多。
二、java風格的命名空間
爲了不全局變量形成的衝突,人們想到或許能夠用多級命名空間來進行管理,因而,代碼就變成了這個風格:
app.util.modA = xxx; app.tools.modA = xxx; app.tools.modA.format = xxx;
Yahoo的YUI早期就是這麼作的。可是這樣調用函數,仍是挺噁心的.
3. jQuery風格的匿名自執行函數
(function(window){ //代碼 window.jQuery = window.$ = jQuery;//經過給window添加屬性而暴漏到全局 })(window);
jQuery的封裝風格曾經被不少框架模仿,經過匿名函數包裝代碼,所依賴的外部變量傳給這個函數,在函數內部可使用這些依賴,而後在函數的最後把模塊自身暴漏給window。
若是須要添加擴展,則能夠做爲jQuery的插件,把它掛載到$上。
這種風格雖然靈活了些,但並未解決根本問題:所需依賴仍是得外部提早提供、仍是增長了全局變量。
3、模塊化要解決的問題
從以上的嘗試中,能夠概括出js模塊化須要解決那些問題:
1. 如何安全的包裝一個模塊的代碼?(不污染模塊外的任何代碼)
2. 如何惟一標識一個模塊?
3. 如何優雅的把模塊的API暴漏出去?(不能增長全局變量)
4. 如何方便的使用所依賴的模塊?
圍繞着這些問題,js模塊化開始了一段艱苦而曲折的征途。
4、源自nodejs的規範CommonJs
2009年,nodejs橫空出世,開創了一個新紀元,人們能夠用js來編寫服務端的代碼了。若是說瀏覽器端的js即使沒有模塊化也能夠忍的話,那服務端是萬萬不能的。
大牛雲集的CommonJs社區發力,制定了Modules/1.0(http://wiki.commonjs.org/wiki/Modules/1.0)規範,首次定義了一個模塊應該長啥樣。
具體來講,Modules/1.0規範包含如下內容:
1. 模塊的標識應遵循的規則(書寫規範)
2. 定義全局函數require,經過傳入模塊標識來引入其餘模塊,執行的結果即爲別的模塊暴漏出來的API
3. 若是被require函數引入的模塊中也包含依賴,那麼依次加載這些依賴
4. 若是引入模塊失敗,那麼require函數應該報一個異常
5. 模塊經過變量exports來嚮往暴漏API,exports只能是一個對象,暴漏的API須做爲此對象的屬性。
此規範一出,馬上產生了良好的效果,因爲其簡單而直接,在nodejs中,這種模塊化方案馬上被推廣開了。
//math.js exports.add = function() { var sum = 0, i = 0, args = arguments, l = args.length; while (i < l) { sum += args[i++]; } return sum; };
/increment.js var add = require('math').add; exports.increment = function(val) { return add(val, 1); };
//program.js var inc = require('increment').increment; var a = 1; inc(a); // 2
五、服務端向前端進軍
Modules/1.0規範源於服務端,沒法直接用於瀏覽器端,緣由表現爲:
1. 外層沒有function包裹,變量全暴漏在全局。如上面例子中increment.js中的add。
2. 資源的加載方式與服務端徹底不一樣。服務端require一個模塊,直接就從硬盤或者內存中讀取了,消耗的時間能夠忽略。而瀏覽器則不一樣,須要從服務端來下載這個文件,而後運行裏面的代碼才能獲得API,須要花費一個http請求,也就是說,require後面的一行代碼,須要資源請求完成才能執行。因爲瀏覽器端是以插入<script>標籤的形式來加載資源的(ajax方式不行,有跨域問題),沒辦法讓代碼同步執行,因此像commonjs那樣的寫法會直接報錯。
因此,社區意識到,要想在瀏覽器環境中也能模塊化,須要對規範進行升級。順便說一句,CommonJs原來是叫ServerJs,從名字能夠看出是專攻服務端的,爲了統一先後端而更名CommonJs。(論起名的重要性~)
而就在社區討論制定下一版規範的時候,內部發生了比較大的分歧,分裂出了三個主張,漸漸的造成三個不一樣的派別:
Modules/1.x派:
這一波人認爲,在現有基礎上進行改進便可知足瀏覽器端的須要,既然瀏覽器端須要function包裝,須要異步加載,那麼新增一個方案,能把現有模塊轉化爲適合瀏覽器端的就好了,有點像「保皇派」。基於這個主張,制定了Modules/Transport(http://wiki.commonjs.org/wiki/Modules/Transport)規範,提出了先經過工具把現有模塊轉化爲複合瀏覽器上使用的模塊,而後再使用的方案。
browserify就是這樣一個工具,能夠把nodejs的模塊編譯成瀏覽器可用的模塊。
目前的最新版是Modules/1.1.1(http://wiki.commonjs.org/wiki/Modules/1.1.1),增長了一些require的屬性,以及模塊內增長module變量來描述模塊信息,變更不大。
Modules/Async派:
這一波人有點像「革新派」,他們認爲瀏覽器與服務器環境差異太大,不能沿用舊的模塊標準。既然瀏覽器必須異步加載代碼,那麼模塊在定義的時候就必須指明所依賴的模塊,而後把本模塊的代碼寫在回調函數裏。模塊的加載也是經過下載-回調這樣的過程來進行,這個思想就是AMD的基礎,因爲「革新派」與「保皇派」的思想沒法達成一致,最終從CommonJs中分裂了出去,獨立制定了瀏覽器端的js模塊化規範AMD(Asynchronous Module Definition)
Modules/2.0派:
這一波人有點像「中間派」,既不想丟掉舊的規範,也不想像AMD那樣推到重來。他們認爲,Modules/1.0當然不適合瀏覽器,但它裏面的一些理念仍是很好的,(如經過require來聲明依賴),新的規範應該兼容這些,AMD規範也有它好的地方(例如模塊的預先加載以及經過return能夠暴漏任意類型的數據,而不是像commonjs那樣exports只能爲object),也應採納。最終他們制定了一個Modules/Wrappings規範,此規範指出了一個模塊應該如何「包裝」,包含如下內容:
1. 全局有一個module變量,用來定義模塊 2. 經過module.declare方法來定義一個模塊 3. module.declare方法只接收一個參數,那就是模塊的factory,次factory能夠是函數也能夠是對象,若是是對象,那麼模塊輸出就是此對象。 4. 模塊的factory函數傳入三個參數:require,exports,module,用來引入其餘依賴和導出本模塊API 5. 若是factory函數最後明確寫有return數據(js函數中不寫return默認返回undefined),那麼return的內容即爲模塊的輸出。
使用該規範的例子看起來像這樣:
//可使用exprots來對外暴漏API module.declare(function(require, exports, module) { exports.foo = "bar"; }); //也能夠直接return來對外暴漏數據 module.declare(function(require) { return { foo: "bar" }; });
6、AMD/RequireJs的崛起與妥協
AMD的思想正如其名,異步加載所需的模塊,而後在回調函數中執行主邏輯。這正是咱們在瀏覽器端開發所習慣了的方式,其做者親自實現了符合AMD規範的requirejs,AMD/RequireJs迅速被廣大開發者所接受。
AMD規範包含如下內容:
1. 用全局函數define來定義模塊,用法爲:define(id?, dependencies?, factory); 2. id爲模塊標識,聽從CommonJS Module Identifiers規範 3. dependencies爲依賴的模塊數組,在factory中需傳入形參與之一一對應 4. 若是dependencies的值中有」require」、」exports」或」module」,則與commonjs中的實現保持一致 5. 若是dependencies省略不寫,則默認爲["require", "exports", "module"],factory中也會默認傳入require,exports,module 6. 若是factory爲函數,模塊對外暴漏API的方法有三種:return任意類型的數據、exports.xxx=xxx、module.exports=xxx 7. 若是factory爲對象,則該對象即爲模塊的返回值
基於以上幾點基本規範,咱們即可以用這樣的方式來進行模塊化組織代碼了:
//a.js define(function(){ console.log('a.js執行'); return { hello: function(){ console.log('hello, a.js'); } } }); //b.js define(function(){ console.log('b.js執行'); return { hello: function(){ console.log('hello, b.js'); } } }); //main.js require(['a', 'b'], function(a, b){ console.log('main.js執行'); a.hello(); $('#b').click(function(){ b.hello(); }); })
上面的main.js被執行的時候,會有以下的輸出:
a.js執行
b.js執行
main.js執行
hello, a.js
在點擊按鈕後,會輸出:
hello, b.js
這結局,如你所願嗎?大致來看,是沒什麼問題的,由於你要的兩個hello方法都正確的執行了。
可是若是細細來看,b.js被預先加載而且預先執行了,(第二行輸出),b.hello這個方法是在點擊了按鈕以後纔會執行,若是用戶壓根就沒點,那麼b.js中的代碼應不該該執行呢?
這其實也是AMD/RequireJs被吐槽的一點,預先下載沒什麼爭議,因爲瀏覽器的環境特色,被依賴的模塊確定要預先下載的。問題在於,是否須要預先執行?若是一個模塊依賴了十個其餘模塊,那麼在本模塊的代碼執行以前,要先把其餘十個模塊的代碼都執行一遍,無論這些模塊是否是立刻會被用到。這個性能消耗是不容忽視的。
另外一點被吐槽的是,在定義模塊的時候,要把全部依賴模塊都羅列一遍,並且還要在factory中做爲形參傳進去,要寫兩遍很大一串模塊名稱,像這樣:
define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){ ..... })
編碼過程略有不爽。
好的一點是,AMD保留了commonjs中的require、exprots、module這三個功能(上面提到的第4條)。你也能夠不把依賴羅列在dependencies數組中。而是在代碼中用require來引入,以下:
define(function(){ console.log('main2.js執行'); require(['a'], function(a){ a.hello(); }); $('#b').click(function(){ require(['b'], function(b){ b.hello(); }); }); });
咱們在define的參數中未寫明依賴,那麼main2.js在執行的時候,就不會預先加載a.js和b.js,只是執行到require語句的時候纔會去加載,上述代碼的輸出以下:
main2.js執行
a.js執行
hello, a.js
能夠看到b.js並未執行,從網絡請求中看,b.js也並未被下載。只有在按鈕被點擊的時候b.js纔會被下載執行,而且在回調函數中執行模塊中的方法。這就是名副其實的「懶加載」了。
這樣的懶加載無疑會大大減輕初始化時的損耗(下載和執行都被省去了),可是弊端也是顯而易見的,在後續執行a.hello和b.hello時,必須得實時下載代碼而後在回調中才能執行,這樣的用戶體驗是很差的,用戶的操做會有明顯的延遲卡頓。
但這樣的現實並不是是沒法接受的,畢竟是瀏覽器環境,咱們已經習慣了操做網頁時伴隨的各類loading。。。
可是話說過來,有沒有更好的方法來處理問題呢?
資源的下載階段仍是預先進行,資源執行階段後置,等到須要的時候再執行。這樣一種折衷的方式,可以融合前面兩種方式的優勢,而又迴避了缺點。
這就是Modules/Wrappings規範,還記得前面提到的「中間派」嗎?
在AMD的陣營中,也有一部分人提出這樣的觀點,代碼裏寫一堆回調實在是太噁心了,他們更喜歡這樣來使用模塊:
var a = require('a'); a.hello(); $('#b').click(function(){ var b = require('b'); b.hello(); });
因而,AMD也終於決定做妥協,兼容Modules/Wrappings的寫法,但只是部分兼容,例如並無使用module.declare來定義模塊,而仍是用define,模塊的執行時機也沒有改變,依舊是預先執行。所以,AMD將此兼容稱爲Simplified CommonJS wrapping,即並非完整的實現Modules/Wrappings。
做了此兼容後,使用requirejs就能夠這麼寫代碼了:
//d.js define(function(require, exports, module){ console.log('d.js執行'); return { helloA: function(){ var a = require('a'); a.hello(); }, run: function(){ $('#b').click(function(){ var b = require('b'); b.hello(); }); } } });
注意定義模塊時候的輕微差別,dependencies數組爲空,可是factory函數的形參必須手工寫上require,exports,module,(這不一樣於以前的dependencies和factory形參全不寫),這樣寫便可使用Simplified CommonJS wrapping風格,與commonjs的格式一致了。
雖然使用上看起來簡單,然而在理解上卻給後人埋下了一個大坑。由於AMD只是支持了這樣的語法,而並無真正實現模塊的延後執行。什麼意思呢?上面的代碼,正常來說應該是預先下載a.js和b.js,而後在執行模塊的helloA方法的時候開始執行a.js裏面的代碼,在點擊按鈕的時候開始執行b.js中的方法。實際卻不是這樣,只要此模塊被別的模塊引入,a.js和b.js中的代碼仍是被預先執行了。
咱們把上面的代碼命名爲d.js,在別的地方使用它:
require(['d'], function(d){ });
上面的代碼會輸出
a.js執行
b.js執行
d.js執行
能夠看出,儘管還未調用d模塊的API,裏面所依賴的a.js和b.js中的代碼已經執行了。AMD的這種只實現語法卻未真正實現功能的作法容易給人形成理解上的困難,被強烈吐槽。
在requirejs2.0中,做者聲明已經處理了此問題(https://github.com/jrburke/requirejs/wiki/Upgrading-to-RequireJS-2.0#delayed)
7、兼容幷包的CMD/seajs
既然requirejs有上述種種不甚優雅的地方,因此必然會有新東西來完善它,這就是後起之秀seajs。
seajs的做者是國內大牛淘寶前端步道者玉伯。seajs全面擁抱Modules/Wrappings規範,不用requirejs那樣回調的方式來編寫模塊。而它也不是徹底按照Modules/Wrappings規範,seajs並無使用declare來定義模塊,而是使用和requirejs同樣的define,或許做者本人更喜歡這個名字吧。(然而這或多或少又會給人們形成理解上的混淆),用seajs定義模塊的寫法以下:
//a.js define(function(require, exports, module){ console.log('a.js執行'); return { hello: function(){ console.log('hello, a.js'); } } }); //b.js define(function(require, exports, module){ console.log('b.js執行'); return { hello: function(){ console.log('hello, b.js'); } } });
//main.js define(function(require, exports, module){ console.log('main.js執行'); var a = require('a'); a.hello(); $('#b').click(function(){ var b = require('b'); b.hello(); }); });
定義模塊時無需羅列依賴數組,在factory函數中需傳入形參require,exports,module,而後它會調用factory函數的toString方法,對函數的內容進行正則匹配,經過匹配到的require語句來分析依賴,這樣就真正實現了commonjs風格的代碼。
上面的main.js執行會輸出以下:
main.js執行
a.js執行
hello, a.js
a.js和b.js都會預先下載,可是b.js中的代碼卻沒有執行,由於尚未點擊按鈕。當點擊按鈕的時候,會輸出以下:
b.js執行
hello, b.js
能夠看到b.js中的代碼此時才執行。這樣就真正實現了「就近書寫,延遲執行「,不可謂不優雅。
若是你必定要挑出一點不爽的話,那就是b.js的預先下載了。你可能不太想一開始就下載好全部的資源,但願像requirejs那樣,等點擊按鈕的時候再開始下載b.js。本着兼容幷包的思想,seajs也實現了這一功能,提供require.async API,在點擊按鈕的時候,只需這樣寫:
var b = require.async('b'); b.hello();
b.js就不會在一開始的時候就加載了。這個API能夠說是簡單漂亮。
關於模塊對外暴漏API的方式,seajs也是融合了各家之長,支持commonjs的exports.xxx = xxx和module.exports = xxx的寫法,也支持AMD的return寫法,暴露的API能夠是任意類型。
你可能會以爲seajs無非就是一個抄,把別人家的優勢都抄過來組合了一下。其實否則,seajs是commonjs規範在瀏覽器端的踐行者,對於requirejs的優勢也加以吸取。看人家的名字,就是海納百川之意。(再論起名的重要性~),既然它的思想是海納百川,討論是否是抄就沒意義了。
鑑於seajs融合了太多的東西,已經沒法說它遵循哪一個規範了,因此玉伯乾脆就自立門戶,起名曰CMD(Common Module Definition)規範,有了綱領,就不會再存在非議了。
8、面向將來的ES6模塊標準
既然模塊化開發的呼聲這麼高,做爲官方的ECMA必然要有所行動,js模塊很早就列入草案,終於在2015年6月份發佈了ES6正式版。然而,可能因爲所涉及的技術還未成熟,ES6移除了關於模塊如何加載/執行的內容,只保留了定義、引入模塊的語法。因此說如今的ES6 Module還只是個雛形,半成品都算不上。可是這並不妨礙咱們先窺探一下ES6模塊標準。
定義一個模塊不須要專門的工做,由於一個模塊的做用就是對外提供API,因此只需用exoprt導出就能夠了:
//方式一, 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關鍵字,如:
import {run as go} from 'a'
run()
若是想要使用模塊中的所有API,也能夠沒必要把每一個都列一遍,使用module關鍵字能夠所有引入,用法:
module foo from 'a'
console.log(foo.obj);
a.run();
在花括號中指明需使用的API,而且能夠用as指定別名。
ES6 Module的基本用法就是這樣,能夠看到確實是有些薄弱,並且目前尚未瀏覽器能支持,只能說它是面向將來了。
目前咱們可使用一些第三方模塊來對ES6進行編譯,轉化爲可使用的ES5代碼,或者是符合AMD規範的模塊,例如ES6 module transpiler。另外有一個項目也提供了加載ES6模塊的方法,es6-module-loader(https://github.com/ModuleLoader/es6-module-loader),不過這都是一些臨時的方案,或許明年ES7一發布,模塊的加載有了標準,瀏覽器給與了實現,這些工具也就沒有用武之地了。
將來仍是很值得期待的,從語言的標準上支持模塊化,js就能夠更加自信的走進大規模企業級開發。
查看原文,請點擊原文連接