AMD 的 CommonJS wrapping

來源:互聯網
上載者:User

標籤:

其實本文的標題應該是「為什麼我不推薦使用 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 函數裡需要做這些事情:

  1. 通過 factory.toString() 拿到 factory 的源碼;
  2. 去掉源碼中的注釋(避免匹配到注釋掉的相依模組);
  3. 通過正則匹配 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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.