標籤:
其實本文的標題應該是「為什麼我不推薦使用 AMD 的 Simplified CommonJS wrapping」,但太長了不好看,為了美觀我只能砍掉一截。
它是什嗎?
為了複用已有的 CommonJS 模組,AMD 規定了 Simplified CommonJS wrapping,然後 RequireJS 實現了它(先後順序不一定對)。它提供了類似於 CommonJS 的模組定義方式,如下:
JSdefine(function(require, exports, module) { var A = require(‘a‘); return function () {};});
這樣,模組的依賴可以像 CommonJS 一樣「就近定義」。但就是這個看上去兩全其美的做法,給大家帶來了很多困擾。
它做了什嗎?
由於 RequireJS 是最流行的 AMD 載入器,後續討論都基於 RequireJS 進行。
直接看 RequireJS 這部分邏輯:
JS//If no name, and callback is a function, then figure out if it a//CommonJS thing with dependencies.if (!deps && isFunction(callback)) { deps = []; if (callback.length) { callback .toString() .replace(commentRegExp, ‘‘) .replace(cjsRequireRegExp, function (match, dep) { deps.push(dep); }); deps = (callback.length === 1 ? [‘require‘] : [‘require‘, ‘exports‘, ‘module‘]).concat(deps); }}
可以看到,為了支援 CommonJS Wrapper 這種寫法,define 函數裡需要做這些事情:
- 通過
factory.toString() 拿到 factory 的源碼;
- 去掉源碼中的注釋(避免匹配到注釋掉的相依模組);
- 通過正則匹配
require 的方式得到依賴資訊;
寫模組時要把 require 當成保留字。模組載入器和構建工具都要實現上述邏輯。
對於 RequireJS,本文最開始定義的模組,最終會變成:
JSdefine([‘a‘], function(require, exports, module) { var A = require(‘a‘); return function () {};});
等價於:
JSdefine([‘a‘], function(A) { return function () {};});
結論是,CommonJS Wrapper 只是書寫上相容了 CommonJS 的寫法,模組運行邏輯並不會改變。
AMD 運行策略
AMD 運行時核心思想是「Early Executing」,也就是提前執行依賴。這個好理解:
JS//main.jsdefine([‘a‘, ‘b‘], function(A, B) { //運行至此,a.js 和 b.js 已下載完成(運行於瀏覽器的 Loader 必須如此); //A、B 兩個模組已經執行完,直接可用(這是 AMD 的特性); return function () {};});
個人覺得,AMD 的這個特性有好有壞:
首先,儘早執行依賴可以儘早發現錯誤。上面的代碼中,假如 a 模組中拋異常,那麼 main.js 在調用 factory 方法之前一定會收到錯誤,factory 不會執行;如果按需執行依賴,結果是:1)沒有進入使用 a 模組的分支時,不會發生錯誤;2)出錯時,main.js 的 factory 方法很可能執行了一半。
另外,儘早執行依賴通常可以帶來更好的使用者體驗,也容易產生浪費。例如模組 a 依賴了另外一個需要非同步載入資料的模組 b,儘早執行 b 可以讓等待時間更短,同時如果 b 最後沒被用到,頻寬和記憶體開銷就浪費了;這種情境下,按需執行依賴可以避免浪費,但是帶來更長的等待時間。
我個人更傾向於 AMD 這種做法。舉一個不太恰當的例子:Chrome 和 Firefox 為了更好的體驗,對於某些類型的檔案,點擊後會詢問是否儲存,這時候實際上已經開始了下載。有時候等了很久才點確認,會開心地發現檔案已經下好;如果點取消,瀏覽器會取消下載,已下載的部分就浪費了。
瞭解到 AMD 這個特性後,再來看一段代碼:
JS//mod1.jsdefine(function() { console.log(‘require module: mod1‘); return { hello: function() { console.log("hello mod1"); } };});
JS//mod2.jsdefine(function() { console.log(‘require module: mod2‘); return { hello: function() { console.log("hello mod2"); } };});
JS//main.jsdefine([‘mod1‘, ‘mod2‘], function(mod1, mod2) { //運行至此,mod1.js 和 mod2.js 已經下載完成; //mod1、mod2 兩個模組已經執行完,直接可用; console.log(‘require module: main‘); mod1.hello(); mod2.hello(); return { hello: function() { console.log(‘hello main‘); } };});
HTML<!--index.html--><script> require([‘main‘], function(main) { main.hello(); });</script>
在本地測試,通常結果是這樣的:
BASHrequire module: mod1require module: mod2require module: mainhello mod1hello mod2hello main
這個結果符合預期。但是這就是全部嗎?用 Fiddler 把 mod1.js 請求 delay 200 再測試,這次輸出:
BASHrequire module: mod2require module: mod1require module: mainhello mod1hello mod2hello main
這是因為 main.js 中 mod1 和 mod2 兩個模組並行載入,且載入完就執行,所以前兩行輸出順序取決於哪個 js 先載入完。如果一定要讓 mod2 在 mod1 之後執行,需要在 define 模組時申明依賴,或者通過 require.config 配置依賴:
JSrequire.config({ shim: { ‘mod2‘: { deps : [‘mod1‘] } }});
嚴重問題!
我們再回過頭來看 CommonJS Wrapper 會帶來什麼問題。前面說過,AMD 規範中,上面的 main.js 等價於這樣:
JS//main.jsdefine(function(require, exports, module) { //運行至此,mod1.js 和 mod2.js 已經下載完成; console.log(‘require module: main‘); var mod1 = require(‘./mod1‘); //這裡才執行 mod1 ? mod1.hello(); var mod2 = require(‘./mod2‘); //這裡才執行 mod2 ? mod2.hello(); return { hello: function() { console.log(‘hello main‘); } };});
這種「就近」書寫的依賴,非常容易讓人認為 main.js 執行到對應 require 語句時才執行 mod1 或 mod2,但這是錯誤的,因為 CommonJS Wrapper 並不會改變 AMD「儘早執行」依賴的本質!
實際上,對於按需執行依賴的載入器,如 SeaJS,上述代碼結果一定是:
BASHrequire module: mainrequire module: mod1hello mod1require module: mod2hello mod2hello main
於是,瞭解過 CommonJS 或 CMD 模組規範的同學,看到使用 CommonJS Wrapper 方式寫的 AMD 模組,容易產生理解偏差,從而誤認為 RequireJS 有 bug。
我覺得「儘早執行」或「按需執行」兩種策略沒有明顯的優劣之分,但 AMD 這種「模仿別人寫法,卻提供不一樣的特性」這個做法十分愚蠢。這年頭,做自己最重要!
其他問題
還有一個小問題也順帶提下:預設情況下,定義 AMD 模組時通過參數傳入依賴列表,簡單可依賴。而用了 CommonJS Wrapper 之後,RequireJS 需要通過正則從 factory.toString() 中提取依賴,複雜並容易出錯。如 RequireJS 下這段代碼會出錯:
JSdefine(function(require, exports, module) { ‘/*‘; var mod1 = require(‘mod1‘), mod2 = require(‘mod2‘); ‘*/‘; mod1.hello();});//Uncaught Error: Module name "mod1" has not been loaded yet for context: _
當然,這個因為 RequireJS 的正則沒寫好,把正常語句當注釋給過濾了,SeaJS 用的正則處理上述代碼沒問題,同時複雜了許多。
雖然實際項目中很難出現上面這樣的代碼,但如果放棄對腦殘的 CommonJS Wrapper 支援後,再寫 AMD 載入器就更加簡單可靠。例如雨夜帶刀同學寫的 seed,代碼十分簡潔;構建工具通常基於字串分析,仍然需要過濾注釋,但可以採用 uglifyjs 壓縮等取巧的方法。
考慮到不是每個 AMD Loader 都支援 CommonJS Wrapper,用參數定義依賴也能保證更好的模組通用性。至於「就近」定義依賴,我一直覺得可有可無,我們寫 php 或 python 時,include 和 import 都會放在頂部,這樣看代碼時能一目瞭然地看到所有依賴,修改起來也方便。
本文部分樣本來自於 SeaJS 與 RequireJS 最大的區別,致謝!
本文連結:https://imququ.com/post/amd-simplified-commonjs-wrapping.html,參與評論 ?
--EOF--
AMD 的 CommonJS wrapping