JavaScript 模塊化的歷史進程

作者:然后去遠足
https://segmentfault.com/a/1190000023017398
引言
昨天在思否上閑逛,發(fā)現(xiàn)了一個有意思的問題 。
因為這個問題,我產(chǎn)生了寫一個系列文章的想法,試圖從站在歷史的角度上來看待編程世界中林林總總的問題和解決方案。
目前中文網(wǎng)絡(luò)上充斥著大量互相“轉(zhuǎn)載”的內(nèi)容,基本是某一個技術(shù)問題的解決方案(what??how?),卻不涉及為什么這么做和歷史緣由(why??when?)。比如你要搜 “JavaScript 有哪些模塊化方案?它們有什么區(qū)別?”,能得到一萬個有用的結(jié)果;但要想知道 “為什么 JavaScript 有這么多模塊化方案?它們是誰創(chuàng)建的?”,卻幾乎不可能。
因此,這一系列文章內(nèi)會盡可能的不涉及具體代碼,只談歷史故事。但會在文末提供包含部分代碼的參考鏈接,以供感興趣的朋友自行閱讀。
這個系列暫定為十篇文章,內(nèi)容會涉及前端、后端、編程語言、開發(fā)工具、操作系統(tǒng)等等。也給自己立個 Flag,在今年年底之前把整個系列寫完。如果沒完成目標(biāo)……就當(dāng)我沒說過這句話(逃
正文
模塊化,是前端繞不過去的話題。
隨著 Node.js 和三大框架的流行,越來越多的前端開發(fā)者們腦海中都會時常浮現(xiàn)一個問題:
為什么 JavaScript 有這么多模塊化方案?
自從 1995 年 5 月,Brendan Eich?寫下了第一行 JavaScript 代碼起,JavaScript 已經(jīng)誕生了 25 年。
但這門語言早期僅僅作為輕量級的腳本語言,用于在 Web 上與用戶進行少量的交互,并沒有依賴管理的概念。
隨著 AJAX 技術(shù)得以廣泛使用,Web 2.0 時代迅猛發(fā)展,瀏覽器承載了愈來愈多的內(nèi)容與邏輯,JavaScript 代碼越來越復(fù)雜,全局變量沖突、依賴管理混亂等問題始終縈繞在前端開發(fā)者們的心頭。此時,JavaScript 亟需一種在其他語言中早已得到良好應(yīng)用的功能 —— 模塊化。
其實,JavaScript 本身的標(biāo)準(zhǔn)化版本 ECMAScript 6.0 (ES6/ES2015) 中,已經(jīng)提供了模塊化方案,即?ES Module。但目前在 Node.js 體系下,最常見的方案其實是?CommonJS。再加上大家耳熟能詳?shù)?AMD、CMD、UMD,模塊化的事實標(biāo)準(zhǔn)如此之多。
那么為什么有如此之多的模塊化方案?它們又是在怎樣的背景下誕生的?為什么沒有一個方案 “千秋萬代,一統(tǒng)江湖”?
接下來,我會按照時間順序講述模塊化的發(fā)展歷程,順帶也就回答了上述幾個問題。
萌芽初現(xiàn):從 YUI Library 和 jQuery 說起
時間回到 2006 年 1 月,當(dāng)時還是國際互聯(lián)網(wǎng)巨頭的 Yahoo(雅虎),開源了其內(nèi)部使用已久的組件庫?YUI Library。
YUI Library 采用了類似于 Java 命名空間的方式,來隔離各個模塊之間的變量,避免全局變量造成的沖突。其寫法類似于:
YUI.util.module.doSomthing();這種寫法無論是封裝還是調(diào)用時都十分繁瑣,而且當(dāng)時的 IDE 對于 JavaScript 來說智能感知非常弱,開發(fā)者很難知道他需要的某個方法存在于哪個命名空間下,經(jīng)常需要頻繁地查閱開發(fā)手冊,導(dǎo)致開發(fā)體驗十分不友好。
在 YUI 發(fā)布之后不久,John Resig?發(fā)布了?jQuery。當(dāng)時年僅 23 歲的他,不會知道自己這一時興起在 BarCamp 會議上寫下的代碼,將占據(jù)未來十幾年的 Web 領(lǐng)域。
jQuery?使用了一種新的組織方式,它利用了 JavaScript 的 IIFE(立即執(zhí)行函數(shù)表達式)和閉包的特性,將所依賴的外部變量傳給一個包裝了自身代碼的匿名函數(shù),在函數(shù)內(nèi)部就可以使用這些依賴,最后在函數(shù)的結(jié)尾把自身暴露給?window。這種寫法被很多后來的框架所模仿,其寫法類似于:
(function(root){
// balabala
root.jQuery = root.$ = jQuery;
})(window);這種寫法雖然靈活性大大提升,可以很方便地添加擴展,但它并未解決根本問題:所需依賴還是得外部提前提供,還是會增加全局變量。
從以上的嘗試中,可以歸納出 JavaScript 模塊化需要解決哪些問題:
如何給模塊一個唯一標(biāo)識?
如何在模塊中使用依賴的外部模塊?
如何安全地(不污染模塊外代碼)包裝一個模塊?
如何優(yōu)雅地(不增加全局變量)把模塊暴漏出去?
圍繞著這些問題,JavaScript 模塊化開始了一段曲折的探索之路。
探索之路:CommonJS 與 Node.js 的誕生
讓我們來到 2009 年 1 月,此時距離 ES6 發(fā)布尚有 5 年的時間,但前端領(lǐng)域已經(jīng)迫切地需要一套真正意義上的模塊化方案,以解決全局變量污染和依賴管理混亂等問題。
Mozilla 旗下的工程師?Kevin Dangoor,在工作之余,與同事們一起制訂了一套 JavaScript 模塊化的標(biāo)準(zhǔn)規(guī)范,并取名為?ServerJS。
ServerJS?最早用于服務(wù)端 JavaScript,旨在為配合自動化測試等工作而提供模塊導(dǎo)入功能。
這里插一句題外話,其實早期 1995 年,Netsacpe(網(wǎng)景)公司就提供了有在服務(wù)端執(zhí)行 JavaScript 能力的產(chǎn)品,名為?Netscape Enterprise Server。但此時服務(wù)端能做的 JavaScript 還是基于瀏覽器來實現(xiàn)的,本身沒有脫離其自帶的 API 范圍。直到 2009 年 5 月,Node.js?誕生,賦予了其文件系統(tǒng)、I/O 流、網(wǎng)絡(luò)通信等能力,才真正意義上的成為了一門服務(wù)端編程語言。
2009 年年初,Ryan Dahl?產(chǎn)生了創(chuàng)造一個跨平臺編程框架的想法,想要基于 Google(谷歌)的?Chromium V8?引擎來實現(xiàn)。經(jīng)過幾個月緊張的開發(fā)工作,在 5 月中旬,Node.js?首個預(yù)覽版本的開發(fā)工作已全部結(jié)束。同年 8 月,歐洲 JSConf 開發(fā)者大會上,Node.js?驚艷亮相。
但在此刻,Node.js?還沒有一款包管理工具,外部依賴依然要手動下載到項目目錄內(nèi)再引用。歐洲 JSConf 大會結(jié)束后,Isaac Z. Schlueter?注意到了?Node.js,兩人一拍即合,決定開發(fā)一款包管理工具,也就是后來大名鼎鼎的?Node Package Manager(即?npm)。
在開發(fā)之初,擺在二人面前的第一個問題就是,采用何種模塊化方案?。二人江目光鎖定在了幾個月前(2009 年 4 月)在華盛頓特區(qū)舉辦的美國 JSConf 大會上公布的?ServerJS。此時的?ServerJS?已經(jīng)更名為?CommonJS,并重新制訂了標(biāo)準(zhǔn)規(guī)范,即Modules/1.0,展現(xiàn)了更大的野心,企圖一統(tǒng)所有編程語言的模塊化方案。
具體來說,Modules/1.0標(biāo)準(zhǔn)規(guī)范包含以下內(nèi)容:
模塊的標(biāo)識應(yīng)遵循一定的書寫規(guī)則。
定義全局函數(shù)?
require(dependency),通過傳入模塊標(biāo)識來引入其他依賴模塊,執(zhí)行的結(jié)果即為別的模塊暴漏出來的 API。如果被?
require?函數(shù)引入的模塊中也包含外部依賴,則依次加載這些依賴。如果引入模塊失敗,那么?
require?函數(shù)應(yīng)該拋出一個異常。模塊通過變量?
exports?來向外暴露 API,exports?只能是一個?object?對象,暴漏的 API 須作為該對象的屬性。
由于這個規(guī)范簡單而直接,Node.js?和?npm?很快就決定采用這種模塊化的方案。至此,第一個 JavaScript 模塊化方案正式登上了歷史舞臺,成為前端開發(fā)中必不可少的一環(huán)。
需要注意的是,CommonJS?是一系列標(biāo)準(zhǔn)規(guī)范的統(tǒng)稱,它包含了多個版本,從最早?ServerJS?時的?Modules/0.1,到更名為?CommonJS?后的?Modules/1.0,再到現(xiàn)在成為主流的?Modules/1.1。這些規(guī)范有很多具體的實現(xiàn),且不只局限于 JavaScript 這一種語言,只要遵循了這一規(guī)范,都可以稱之為?CommonJS。其中,Node.js?的實現(xiàn)叫做?Common Node Modules。CommonJS?的其他實現(xiàn),感興趣的朋友可以閱讀本文最下方的參考鏈接。
值得一提的是,CommonJS?雖然沒有進入 ECMAScript 標(biāo)準(zhǔn)范圍內(nèi),但?CommonJS?項目組的很多成員,也都是 TC39(即制訂 ECMAScript 標(biāo)準(zhǔn)的委員會組織)的成員。這也為日后 ES6 引入模塊化特性打下了堅實的基礎(chǔ)。
分道揚鑣:CommonJS 歷史路口上的抉擇
在推出?Modules/1.0?規(guī)范后,CommonJS?在?Node.js?等環(huán)境下取得了很不錯的實踐。
但此時的?CommonJS?有兩個重要問題沒能得到解決,所以遲遲不能推廣到瀏覽器上:
由于外層沒有?
function?包裹,被導(dǎo)出的變量會暴露在全局中。在服務(wù)端?
require?一個模塊,只會有磁盤 I/O,所以同步加載機制沒什么問題;但如果是瀏覽器加載,一是會產(chǎn)生開銷更大的網(wǎng)絡(luò) I/O,二是天然異步,就會產(chǎn)生時序上的錯誤。
因此,社區(qū)意識到,要想在瀏覽器環(huán)境中也能順利使用?CommonJS,勢必重新制訂新的標(biāo)準(zhǔn)規(guī)范。但新的規(guī)范怎么制訂,成為了激烈爭論的焦點,分歧和沖突由此誕生,逐步形成了三大流派:
Modules/1.x?派:這派的觀點是,既然?Modules/1.0?已經(jīng)在服務(wù)器端有了很好的實踐經(jīng)驗,那么只需要將它移植到瀏覽器端就好。在瀏覽器加載模塊之前,先通過工具將模塊轉(zhuǎn)換成瀏覽器能運行的代碼了。我們可以理解為他們是“保守派”。
Modules/Async?派:這派認(rèn)為,既然瀏覽器環(huán)境于服務(wù)器環(huán)境差異過大,那么就不應(yīng)該繼續(xù)在?Modules/1.0?的基礎(chǔ)上小修小補,應(yīng)該遵循瀏覽器本身的特點,放棄?
require?方式改為回調(diào),將同步加載模塊變?yōu)楫惒郊虞d模塊,這樣就可以通過 ”下載 -> 回調(diào)“ 的方式,避免時序問題。我們可以理解為他們是“激進派”。Modules/2.0?派:這派同樣也認(rèn)為不應(yīng)該沿用?Modules/1.0,但也不向激進派一樣過于激進,認(rèn)為?
require?等規(guī)范還是有可取之處,不應(yīng)該隨隨便便放棄,而是要盡可能的保持一致;但激進派的優(yōu)點也應(yīng)該吸收,比如?exports?也可以導(dǎo)出其他類型、而不僅局限于?object?對象。我們可以理解為他們是“中間派”。
其中保守派的思路跟今天通過?babel?等工具,將 JavaScript 高版本代碼轉(zhuǎn)譯為低版本代碼如出一轍,主要目的就是為了兼容。有了這種想法,這派人馬提出了?Modules/Transport?規(guī)范,用于規(guī)定模塊如何轉(zhuǎn)譯。browserify?就是這一觀點下的產(chǎn)物。
激進派也提出了自己的規(guī)范?Modules/AsynchronousDefinition,奈何這一派的觀點并沒有得到?CommonJS?社區(qū)的主流認(rèn)可。
中間派同樣也有自己的規(guī)范?Modules/Wrappings,但這派人馬最后也不了了之,沒能掀起什么風(fēng)浪。
激進派、中間派與保守派的理念不和,最終為?CommonJS?社區(qū)分裂埋下伏筆。
百家爭鳴:激進派 —— AMD 的崛起
激進派的?James Burke?在 2009 年 9 月開發(fā)出了?RequireJS?這一模塊加載器,以實踐證明自己的觀點。
但激進派的想法始終得不到?CommonJS?社區(qū)主流認(rèn)可。雙方的分歧點主要在于執(zhí)行時機問題,Modules/1.0?是延遲加載、且同一模塊只執(zhí)行一次,而?Modules/AsynchronousDefinition?卻是提前加載,加之破壞了就近聲明(就近依賴)原則,還引入了?define?等新的全局函數(shù),雙方的分歧越來越大。
最終,在?James Burke、Karl Westin?等人的帶領(lǐng)下,激進派于同年年底宣布離開?CommonJS?社區(qū),自立門戶。
激進派在離開社區(qū)后,起初專注于?RequireJS?的開發(fā)工作,并沒有過多的涉足社區(qū)工作,也沒有此草新的標(biāo)準(zhǔn)規(guī)范。
2011 年 2 月,在?RequireJS?的擁躉們的共同努力下,由?Kris Zyp?起草的?Async Module Definition(簡稱?AMD)標(biāo)準(zhǔn)規(guī)范正式發(fā)布,并在?RequireJS?社區(qū)的基礎(chǔ)上建立了?AMD?社區(qū)。
AMD?標(biāo)準(zhǔn)規(guī)范主要包含了以下幾個內(nèi)容:
模塊的標(biāo)識遵循?CommonJS Module Identifiers。
定義全局函數(shù)?
define(id, dependencies, factory),用于定義模塊。dependencies?為依賴的模塊數(shù)組,在?factory?中需傳入形參與之一一對應(yīng)。如果?
dependencies?的值中有?require、exports?或module,則與?CommonJS?中的實現(xiàn)保持一致。如果?
dependencies?省略不寫,則默認(rèn)為?['require', 'exports', 'module'],factory?中也會默認(rèn)傳入三者。如果?
factory?為函數(shù),模塊可以通過以下三種方式對外暴漏 API:return?任意類型;exports.XModule = XModule、module.exports = XModule。如果?
factory?為對象,則該對象即為模塊的導(dǎo)出值。
其中第三、四兩點,即所謂的?Modules/Wrappings,是因為?AMD?社區(qū)對于要寫一堆回調(diào)這種做法頗有微辭,最后?RequireJS?團隊妥協(xié),搞出這么個部分兼容支持。
因為?AMD?符合在瀏覽器端開發(fā)的習(xí)慣方式,也是第一個支持瀏覽器端的 JavaScript 模塊化解決方案,RequireJS?迅速被廣大開發(fā)者所接受。
但有?CommonJS?珠玉在前,很多開發(fā)者對于要寫很多回調(diào)的方式頗有微詞。在呼吁高漲聲中,RequireJS?團隊最終妥協(xié),搞出個?Simplified CommonJS wrapping(簡稱?CJS)的兼容方式,即上文的第三、四兩點。但由于背后實際還是?AMD,所以只是寫法上做了兼容,實際上并沒有真正做到?CommonJS?的延遲加載。
與?CommonJS?規(guī)范有眾多實現(xiàn)不同的是,AMD?只專注于 JavaScript 語言,且實現(xiàn)并不多,目前只有?RequireJS?和?Dojo Toolkit,其中后者已經(jīng)停止維護。
一波三折:中間派 —— CMD 的衰落
由于?AMD?的提前加載的問題,被很多開發(fā)者擔(dān)心會有性能問題而吐槽。
例如,如果一個模塊依賴了十個其他模塊,那么在本模塊的代碼執(zhí)行之前,要先把其他十個模塊的代碼都執(zhí)行一遍,不管這些模塊是不是馬上會被用到。這個性能消耗是不容忽視的。
為了避免這個問題,上文提到,中間派試圖保留?CommonJS?書寫方式和延遲加載、就近聲明(就近依賴)等特性,并引入異步加載機制,以適配瀏覽器特性。
其中一位中間派的大佬?Wes Garland,本身是?CommonJS?的主要貢獻者之一,在社區(qū)中很受尊重。他在?CommonJS?的基礎(chǔ)之上,起草了?Modules/2.0,并給出了一個名為?BravoJS?的實現(xiàn)。
另一位中間派大佬?@khs4473?提出了?Modules/Wrappings,并給出了一個名為?FlyScript?的實現(xiàn)。
但?Wes Garland?本人是學(xué)院派,理論功底十分扎實,但寫出的作品卻既不優(yōu)雅也不實用。而實戰(zhàn)派的?@khs4473?則在與?James Burke?發(fā)生了一些爭論,最后刪除了自己的 GitHub 倉庫并停掉了?FlyScript?官網(wǎng)。
到此為止,中間一派基本已全軍覆滅,空有理論,沒有實踐。
讓我們前進到 2011 年 4 月,國內(nèi)阿里巴巴集團的前端大佬玉伯(本名王保平),在給?RequireJS?不斷提出建議卻被拒絕之后,萌生了自己寫一個模塊加載器的想法。
在借鑒了?CommonJS、AMD?等模塊化方案后,玉伯寫出了?SeaJS,不過這一實現(xiàn)并沒有嚴(yán)格遵守?Modules/Wrappings?的規(guī)范,所以嚴(yán)格來說并不能稱之為?Modules/2.0。在此基礎(chǔ)上,玉伯提出了?Common Module Definition(簡稱?CMD)這一標(biāo)準(zhǔn)規(guī)范。
CMD?規(guī)范的主要內(nèi)容與?AMD?大致相同,不過保留了?CommonJS?中最重要的延遲加載、就近聲明(就近依賴)特性。
隨著國內(nèi)互聯(lián)網(wǎng)公司之間的技術(shù)交流,SeaJS?在國內(nèi)得到了廣泛使用。不過在國外,也許是因為語言障礙等原因,并沒有得到非常大范圍的推廣。
兼容并濟:UMD 的統(tǒng)一
2014 年 9 月,美籍華裔?Homa Wong?提交了?UMD?第一個版本的代碼。
UMD?即?Universal Module Definition?的縮寫,它本質(zhì)上并不是一個真正的模塊化方案,而是將?CommonJS?和?AMD?相結(jié)合。
UMD?作出了如下內(nèi)容的規(guī)定:
優(yōu)先判斷是否存在?
exports?方法,如果存在,則采用?CommonJS?方式加載模塊;其次判斷是否存在?
define?方法,如果存在,則采用?AMD?方式加載模塊;最后判斷?
global?對象上是否定義了所需依賴,如果存在,則直接使用;反之,則拋出異常。
這樣一來,模塊開發(fā)者就可以使自己的模塊同時支持?CommonJS?和?AMD?的導(dǎo)出方式,而模塊使用者也無需關(guān)注自己依賴的模塊使用的是哪種方案。
姍姍來遲:欽定的 ES6/ES2015
時間前進到 2016 年 5 月,經(jīng)過了兩年的討論,ECMAScript 6.0 終于正式通過決議,成為了國際標(biāo)準(zhǔn)。
在這一標(biāo)準(zhǔn)中,首次引入了?import?和?export?兩個 JavaScript 關(guān)鍵字,并提供了被稱為?ES Module?的模塊化方案。
在 JavaScript 出生的第 21 個年頭里,JavaScript 終于迎來了屬于自己的模塊化方案。
但由于歷史上的先行者已經(jīng)占據(jù)了優(yōu)勢地位,所以?ES Module?遲遲沒有完全替換上文提到的幾種方案,甚至連瀏覽器本身都沒有立即作出支持。
2017 年 9 月上旬,Chrome?61.0 版本發(fā)布,首次在瀏覽器端原生支持了?ES Module。
2017 年 9 月中旬,Node.js?迅速跟隨,發(fā)布了 8.5.0,以支持原生模塊化,這一特性被稱之為?ECMAScript Modules(簡稱?MJS)。不過到目前為止,這一特性還處于試驗性階段。
不過隨著?babel、Webpack、TypeScript?等工具的興起,前端開發(fā)者們已經(jīng)不再關(guān)心以上幾種方式的兼容問題,習(xí)慣寫哪種就寫哪種,最后由工具統(tǒng)一轉(zhuǎn)譯成瀏覽器所支持的方式。
因此,預(yù)計在今后很長的一段時間里,幾種模塊化方案都會在前端開發(fā)中共存。
尾聲
本文以時間線為基準(zhǔn),從作者、社區(qū)、理念等幾個維度談到了 JavaScript 模塊化的幾大方案。
其實模塊化方案遠不止提到的這些,但其他的都沒有這些流行,這里也就不費筆墨。
文中并沒有提及各個模塊化方案是如何實現(xiàn)的,也沒有給出相關(guān)的代碼示例,感興趣的朋友可以自行閱讀下方的參考閱讀鏈接。
下面我們再總結(jié)梳理一下時間線:
| 時間 | 事件 |
|---|---|
| 1995.05? ? ?? | Brendan Eich?開發(fā) JavaScript。 |
| 2006.01 | Yahoo 開源?YUI Library,采用命名空間方式管理模塊。 |
| 2006.01 | John Resig?開發(fā)?jQuery,采用 IIFE + 閉包管理模塊。 |
| 2009.01 | Kevin Dangoor?起草?ServerJS,并公布第一個版本?Modules/0.1。 |
| 2009.04 | Kevin Dangoor?在美國 JSConf 公布?CommonJS。 |
| 2009.05 | Ryan Dahl?開發(fā)?Node.js。 |
| 2009.08 | Ryan Dahl?在歐洲 JSConf 公布?Node.js。 |
| 2009.08 | Kevin Dangoor?將?ServerJS?改名為?CommonJS,并起草第二個版本?Modules/1.0。 |
| 2009.09 | James Burke?開發(fā)?RequireJS。 |
| 2010.01 | Isaac Z. Schlueter?開發(fā)?npm,實現(xiàn)了基于?CommonJS?模塊化方案的?Common Node Modules。 |
| 2010.02 | Kris Zyp?起草?AMD,AMD/RequireJS?社區(qū)成立。 |
| 2011.01 | 玉伯開發(fā)?SeaJS,起草?CMD,CMD/SeaJS?社區(qū)成立。 |
| 2014.08 | Homa Wong?開發(fā)?UMD。 |
| 2015.05 | ES6 發(fā)布,新增特性?ES Module。 |
| 2017.09 | Chrome?和?Node.js?開始原生支持?ES Module。 |
注:文章中的所有人物、事件、時間、地點,均來自于互聯(lián)網(wǎng)公開內(nèi)容,由本人進行搜集整理,其中如有謬誤之處,還請多多指教。
參考閱讀
《Wikipedia - YUI Library》
《Wikipedia - jQuery》
《Wikipedia - List of server-side JavaScript implementations》
《Wikipedia - CommonJS》
《Wikipedia - Asynchronous Module Definition》
《CommonJS Project History》
《RequireJS Project History》
《JavaScript Modules: A Brief History》
《淺析 JS 模塊規(guī)范:AMD,CMD,CommonJS》
《JavaScript Module Loader - CommonJS,RequireJS,SeaJS 歸納筆記》
《前端模塊化開發(fā)那點歷史》
?? 看完三件事
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)
關(guān)注我的官網(wǎng)?https://muyiy.cn,讓我們成為長期關(guān)系
關(guān)注公眾號「高級前端進階」,公眾號后臺回復(fù)「面試題」 送你高級前端面試題,回復(fù)「加群」加入面試互助交流群
