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