來源:玉伯也叫射鵰
最近不斷有人問及,想起前些天跟 @dexteryy 等人的討論:dexteryy/OzJS#10 當時有過簡單總結,重新梳理如下。
寫在前面
- 不談什麼:傳統的模組化開發方式,比如檔案拆分、全域性變數、名稱空間,以及 YUI3 式的模組化開發方式。有興趣的可閱讀:#547
- 談什麼: 關於 CommonJS、AMD、Node.js、CMD 等相關的故事與未來趨勢,很有意思。
- 不一定精準:本文是基於史實的扯淡,因此部分文字特別是時間都是模糊記憶,不一定精準。關於流派、趨勢則是個人在社群的感受,不代表客觀看法。(看法都是主觀的,呵呵)
CommonJS 社群
大概 09 年 – 10 年期間,CommonJS 社群大牛雲集。CommonJS 原來叫 ServerJS,推出 Modules/1.0 規範後,在 Node.js 等環境下取得了很不錯的實踐。
09年下半年這幫充滿幹勁的小夥子們想把 ServerJS 的成功經驗進一步推廣到瀏覽器端,於是將社群改名叫 CommonJS,同時激烈爭論 Modules 的下一版規範。分歧和衝突由此誕生,逐步形成了三大流派:
- Modules/1.x 流派。這個觀點覺得 1.x 規範已經夠用,只要移植到瀏覽器端就好。要做的是新增 Modules/Transport 規範,即在瀏覽器上執行前,先通過轉換工具將模組轉換為符合 Transport 規範的程式碼。主流代表是服務端的開發人員。現在值得關注的有兩個實現:越來越火的 component 和走在前沿的 es6 module transpiler。
- Modules/Async 流派。這個觀點覺得瀏覽器有自身的特徵,不應該直接用 Modules/1.x 規範。這個觀點下的典型代表是 AMD 規範及其實現 RequireJS。這個稍後再細說。
- Modules/2.0 流派。這個觀點覺得瀏覽器有自身的特徵,不應該直接用 Modules/1.x 規範,但應該儘可能與 Modules/1.x 規範保持一致。這個觀點下的典型代表是 BravoJS 和 FlyScript 的作者。BravoJS 作者對 CommonJS 的社群的貢獻很大,這份 Modules/2.0-draft 規範花了很多心思。FlyScript 的作者提出了 Modules/Wrappings 規範,這規範是 CMD 規範的前身。可惜的是 BravoJS 太學院派,FlyScript 後來做了自我閹割,將整個網站(flyscript.org)下線了。這個故事有點悲壯,下文細說。
AMD 與 RequireJS
再來說 AMD 規範。真正的 AMD 規範在這裡:Modules/AsynchronousDefinition。AMD 規範一直沒有被 CommonJS 社群認同,核心爭議點如下:
執行時機有異議
看程式碼
Modules/1.0:
1 |
var a = require("./a") // 執行到此處時,a.js 才同步下載並執行 |
AMD:
1 2 3 4 5 6 |
define(["require"], function(require) { // 在這裡,模組 a 已經下載並執行好 // ... var a = require("./a") // 此處僅僅是取模組 a 的 exports }) |
AMD 裡提前下載 a.js 是瀏覽器的限制,沒辦法做到同步下載,這個社群都認可。
但執行,AMD 裡是 Early Executing,Modules/1.0 裡是第一次 require 時才執行。這個差異很多人不能接受,包括持 Modules/2.0 觀點的也不能接受。
這個差異,也導致實質上 Node 的模組與 AMD 模組是無法共享的,存在潛在衝突。
模組書寫風格有爭議
AMD 風格下,通過引數傳入依賴模組,破壞了 就近宣告 原則。比如:
1 2 3 4 5 6 7 8 9 10 |
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { // 等於在最前面申明並初始化了要用到的所有模組 if (false) { // 即便壓根兒沒用到某個模組 b,但 b 還是提前執行了 b.foo() } }) |
還有就是 AMD 下 require 的用法,以及增加了全域性變數 define 等細節,當時在社群被很多人不認可。
最後,AMD 從 CommonJS 社群獨立了出去,單獨成為了 AMD 社群。有陣子,CommonJS 社群還要求 RequireJS 的文件裡,不能再打 CommonJS 的旗幟(這個 CommonJS 社群做得有點小氣)。
脫離了 CommonJS 社群的 AMD 規範,實質上演化成了 RequireJS 的附屬品。比如
- AMD 規範裡增加了對 Simplified CommonJS Wrapper 格式的支援。這個背後是因為 RequireJS 社群有很多人反饋想用 require 的方式,最後 RequireJS 作者妥協,才有了這個半殘的 CJS 格式支援。(注意這個是偽支援,背後依舊是 AMD 的執行邏輯,比如提前執行。)
- AMD 規範的演進,離不開 RequireJS。這有點像 IE…… 可能是我的偏見。
AMD 的流行,很大程度上取決於 RequireJS 作者的推廣,這有點像 less 因 Bootstrap 而火起來一樣。但火起來的東西未必好,比如個人覺得 stylus 就比 less 更優雅好用。
關於 AMD 和 RequireJS,暫且按下不表。來看另一條暗流:Modules/2.0 流派。
Modules/2.0
BravoJS 的作者 Wes Garland 有很深厚的程式功底,在 CommonJS 社群也非常受人尊敬。但 BravoJS 本身非常學院派,是為了論證 Modules/2.0-draft 規範而寫的一個專案。學院派的 BravoJS 在實用派的 RequireJS 面前不堪一擊,現在基本上只留存了一些美好的回憶。
這時,Modules/2.0 陣營也有一個實戰派:FlyScript。FlyScript 拋去了 Modules/2.0 中的學究氣,提出了非常簡潔的Modules/Wrappings 規範:
1 2 3 4 5 |
module.declare(function(require, exports, module) { var a = require("a"); exports.foo = a.name; }); |
這個簡潔的規範考慮了瀏覽器的特殊性,同時也儘可能相容了 Modules/1.0 規範。悲催的是,FlyScript 在推出正式版和官網之後,RequireJS 當時正直紅火。期間 FlyScript 作者 khs4473 和 RequireJS 作者 James Burke 有過一些爭論。再後來,FlyScript 作者做了自我閹割,將 GitHub 上的專案和官網都清空了,官網上當時留了一句話,模糊中記得是
我會回來的,帶著更好的東西。
這中間究竟發生了什麼,不得而知。後來我有發郵件給 @khs4473 詢問,khs 給了兩點挺讓我尊重的理由,大意是
- 我並非前端出身,RequireJS 的作者 James Burke 比我更懂瀏覽器。
- 我們應該協同起來推動一個社群的發展,即便它不是你喜歡的。
這兩句話對我影響很大。也是那之後,開始仔細研究 RequireJS,並通過郵件等方式給 RequireJS 提出過不少建議。
再後來,在實際使用 RequireJS 的過程中,遇到了很多坑。那時 RequireJS 雖然很火,但真不夠完善。期間也在尋思著 FlyScript 離開時的那句話:“我會回來的,帶著更好的東西”
我沒 FlyScript 的作者那麼偉大,在不斷給 RequireJS 提建議,但不斷不被採納後,開始萌生了自己寫一個 loader 的念頭。
這就是 SeaJS。
SeaJS 借鑑了 RequireJS 的不少東西,比如將 FlyScript 中的 module.declare
改名為 define
等。SeaJS 更多地來自 Modules/2.0 的觀點,但儘可能去掉了學院派的東西,加入了不少實戰派的理念。
最後
寫著寫著,有點滄桑感,不想寫了。
歷史不是過去,歷史正在上演。隨著 W3C 等規範、以及瀏覽器的飛速發展,前端的模組化開發會逐步成為基礎設施。一切終究都會成為歷史,未來會更好。