為何選擇 AMD?

此頁面說明 JavaScript 模組的 非同步模組定義 (AMD) API 的設計力量和使用,RequireJS 支援的模組 API。有另一個頁面說明 網頁上模組的一般方法

模組目的 § 1

什麼是 JavaScript 模組?它們的目的是什麼?

  • 定義:如何將一段程式碼封裝成一個有用的單元,以及如何註冊其功能/匯出模組的值。
  • 相依性參考:如何參考其他程式碼單元。

今日的網路 § 2

(function () {
    var $ = this.jQuery;

    this.myExample = function () {};
}());

JavaScript 程式碼片段今日如何定義?

  • 透過立即執行的工廠函式定義。
  • 透過 HTML script 標籤載入的全球變數名稱來參考相依性。
  • 相依性非常薄弱:開發人員需要知道正確的相依性順序。例如,包含 Backbone 的檔案不能在 jQuery 標籤之前。
  • 它需要額外的工具將一組 script 標籤替換成一個標籤,以進行最佳化部署。

這在大型專案中可能難以管理,特別是當腳本開始有許多相依性,而且可能會重疊和巢狀時。手寫 script 標籤的可擴充性不高,而且它排除了依需求載入腳本的功能。

CommonJS § 3

var $ = require('jquery');
exports.myExample = function () {};

原始 CommonJS (CJS) 清單 參與者決定制定一種模組格式,適用於今日的 JavaScript 語言,但不一定受限於瀏覽器 JS 環境的限制。希望在瀏覽器中使用一些權宜措施,並希望影響瀏覽器製造商建置解決方案,讓他們的模組格式能夠原生運作得更好。權宜措施

  • 使用伺服器將 CJS 模組轉換成瀏覽器中可用的東西。
  • 或使用 XMLHttpRequest (XHR) 載入模組的文字,並在瀏覽器中進行文字轉換/剖析。

CJS 模組格式每個檔案只允許一個模組,因此「傳輸格式」將用於將多個模組綑綁在一個檔案中,以進行最佳化/綑綁。

透過此方法,CommonJS 群組得以找出相依性參照、處理循環相依性,以及取得目前模組的某些屬性。然而,他們並未完全採用瀏覽器環境中無法變更,但仍會影響模組設計的一些事物

  • 網路載入
  • 內在非同步性

這也表示他們讓網路開發人員承擔更多實作格式的負擔,而權宜措施表示偵錯會更糟。基於 eval 的偵錯或偵錯多個串接成一個檔案的檔案在實務上都有弱點。這些弱點可能會在未來某個時間點於瀏覽器工具中獲得解決,但最終結果是:在最常見的 JS 環境(瀏覽器)中使用 CommonJS 模組,在今日並非最佳做法。

AMD § 4

define(['jquery'] , function ($) {
    return function () {};
});

AMD 格式源自於想要一個比今日「撰寫一堆具有您必須手動排序的內隱相依性的指令碼標籤」更好的模組格式,以及一個可直接在瀏覽器中輕鬆使用的格式。一個具有良好偵錯特性,且不需要特定伺服器工具即可開始使用的格式。它源自於 Dojo 使用 XHR+eval 的真實世界經驗,並希望避免其在未來的弱點。

它比網路目前的「全域變數和指令碼標籤」有改善,因為

  • 使用 CommonJS 的字串 ID 作為相依性的做法。明確宣告相依性,並避免使用全域變數。
  • ID 可以對應到不同的路徑。這允許替換實作。這對於建立單元測試的模擬非常棒。對於上述的程式碼範例,程式碼只預期實作 jQuery API 和行為的某個東西。它不一定是 jQuery。
  • 封裝模組定義。提供您避免污染全域名稱空間的工具。
  • 定義模組值的路徑明確。使用「傳回值;」或 CommonJS 的「exports」慣用語,這對於循環相依性很有用。

它比 CommonJS 模組有改善,因為

  • 它在瀏覽器中運作得更好,它有最少的陷阱。其他方法在偵錯、跨網域/CDN 使用、file:// 使用和需要特定伺服器工具方面有問題。
  • 定義一種在一個檔案中包含多個模組的方法。在 CommonJS 術語中,這個術語稱為「傳輸格式」,而該群組尚未就傳輸格式達成共識。
  • 允許將函式設定為傳回值。這對於建構函式非常有用。在 CommonJS 中,這比較麻煩,總是必須在 exports 物件上設定一個屬性。Node 支援 module.exports = function () {},但那不是 CommonJS 規範的一部分。

模組定義 § 5

使用 JavaScript 函式進行封裝已記錄為 模組模式

(function () {
   this.myGlobal = function () {};
}());

此類型的模組依賴於將屬性附加到全域物件以匯出模組值,而且使用此模型難以宣告相依性。執行此函式時,假設相依性會立即可用。這會限制相依性的載入策略。

AMD 透過下列方式解決這些問題

  • 呼叫 define() 註冊工廠函式,而不是立即執行它。
  • 將相依性傳遞為字串值陣列,不要擷取全域變數。
  • 僅在載入並執行所有相依性後執行工廠函式。
  • 將相依模組傳遞為引數給工廠函式。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

命名模組 § 6

請注意,上述模組未宣告自己的名稱。這讓模組非常具有可攜性。它允許開發人員將模組放置在不同的路徑中,以給予其不同的 ID/名稱。AMD 載入器會根據其他指令碼參照模組的方式,給予模組 ID。

但是,將多個模組組合在一起以提升效能的工具需要一種方法,在最佳化檔案中為每個模組命名。因此,AMD 允許字串作為 define() 的第一個引數

//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {

    //Define the module value by returning a value.
    return function () {};
});

您應該避免自己命名模組,並且在開發時只將一個模組放在一個檔案中。但是,對於工具和效能,模組解決方案需要一種方法來識別內建資源中的模組。

簡化 § 7

上述 AMD 範例可在所有瀏覽器中執行。但是,命名函式引數可能會導致相依性名稱不匹配,而且如果您的模組有許多相依性,它可能會開始看起來有點奇怪

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

為了簡化此作業,並讓您輕鬆地對 CommonJS 模組進行簡單包裝,因此支援此形式的 define,有時稱為「簡化的 CommonJS 包裝」

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

AMD 載入器會使用 Function.prototype.toString() 解析 require('') 呼叫,然後在內部將上述 define 呼叫轉換為此

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

這允許載入器非同步載入 dependency1 和 dependency2,執行這些相依性,然後執行此函式。

並非所有瀏覽器都能提供可用的 Function.prototype.toString() 結果。截至 2011 年 10 月,PS 3 和較舊的 Opera Mobile 瀏覽器無法提供。這些瀏覽器更有可能需要針對網路/裝置限制進行最佳化的模組建置,因此只要使用知道如何將這些檔案轉換為正規化依賴陣列形式的最佳化器進行建置,例如 RequireJS 最佳化器

由於無法支援此 toString() 掃描的瀏覽器數量非常少,因此可以安全地對所有模組使用此糖化形式,特別是如果您喜歡將依賴名稱與將儲存其模組值的變數對齊時。

CommonJS 相容性 § 8

儘管此糖化形式稱為「簡化的 CommonJS 封裝」,但它與 CommonJS 模組並非 100% 相容。然而,不受支援的情況在瀏覽器中可能也會中斷,因為它們通常假設同步載入依賴項。

根據我(徹底非科學的)個人經驗,大多數 CJS 模組(約 95%)與簡化的 CommonJS 封裝完全相容。

中斷的模組是對依賴項進行動態計算的模組,任何不對 require() 呼叫使用字串常數的模組,以及任何看起來不像宣告式 require() 呼叫的模組。因此,以下項目會失敗

//BAD
var mod = require(someCondition ? 'a' : 'b');

//BAD
if (someCondition) {
    var a = require('a');
} else {
    var a = require('a1');
}

這些情況由 callback-require 處理,require([moduleName], function (){}) 通常存在於 AMD 載入器中。

AMD 執行模型與 ECMAScript Harmony 模組的指定方式更為一致。在 AMD 封裝中無法運作的 CommonJS 模組也無法作為 Harmony 模組運作。AMD 的程式碼執行行為更具未來相容性。

冗長性與實用性

對 AMD 的批評之一,至少與 CJS 模組相比,是它需要一定程度的縮排和函式封裝。

但以下為事實:感知到的額外輸入和使用 AMD 的縮排程度並不重要。以下為您在編碼時花費時間的地方

  • 思考問題。
  • 閱讀程式碼。

您的編碼時間主要花在思考上,而不是輸入上。雖然通常較少的字較好,但這種方法的回報有限,而且 AMD 中的額外輸入並不多。

大多數網路開發人員使用函式封裝,以避免使用全域變數污染頁面。看到函式封裝在功能周圍是很常見的,而且不會增加模組的閱讀成本。

CommonJS 格式也有一些隱藏成本

  • 工具依賴成本
  • 在瀏覽器中會中斷的邊緣案例,例如跨網域存取
  • 更差的除錯,這項成本會隨著時間持續累積

AMD 模組需要較少的工具,邊緣案例問題較少,而且有更好的除錯支援。

重要的是:能夠實際與他人共用程式碼。AMD 是達成此目標的最低能量路徑。

擁有可以在當今瀏覽器中運作的、容易除錯的模組系統,表示在為 JavaScript 製作最佳模組系統的過程中獲得實際經驗。

AMD 及其相關 API 已協助顯示以下事項,以供任何未來的 JS 模組系統參考

  • 將函式傳回作為模組值,特別是建構函式,會導致更好的 API 設計。Node 有 module.exports 可以做到這一點,但能夠使用「return function (){}」會更簡潔。這表示不必取得「module」的控制權來執行 module.exports,而且這是一個更清楚的程式碼表達方式。
  • 動態程式碼載入(在 AMD 系統中透過 require([], function (){}) 執行)是一項基本需求。CJS 討論過這一點,提出了一些建議,但並未完全採納。Node 沒有支援此需求,而是依賴 require('') 的同步行為,這無法移植到網路。
  • 載入器外掛程式非常有用。它有助於避免在基於回呼的程式設計中常見的巢狀大括號縮排。
  • 有選擇地將一個模組對應到從另一個位置載入,可以輕鬆提供模擬物件進行測試。
  • 每個模組最多應該只有一個 IO 動作,而且應該很簡單。網路瀏覽器無法容忍多次 IO 查詢來尋找模組。這與 Node 目前執行的多路徑查詢相左,而且避免使用 package.json 的「main」屬性。只要使用可以輕鬆對應到一個位置的模組名稱,並根據專案位置使用合理的預設慣例,這樣就不需要冗長的設定,但需要時可以進行簡單的設定。
  • 最好有一個 「選擇加入」呼叫,這樣舊的 JS 程式碼才能參與新系統。

如果 JS 模組系統無法提供上述功能,與 AMD 及其相關 API(例如 callback-require載入器外掛程式和基於路徑的模組 ID)相比,它將處於顯著的劣勢。

AMD 目前使用情況 § 9

截至 2011 年 10 月中旬,AMD 已在網路上廣泛採用

您可以做的事 § 10

如果您撰寫應用程式

如果您是指令碼/函式庫作者:

  • 選擇性呼叫 define() (如果可用)。好處是您仍可以在不依賴 AMD 的情況下編寫函式庫,只要在可用時參與即可。這允許您的模組消費者
    • 避免在頁面中傾倒全域變數
    • 使用更多選項進行程式碼載入、延遲載入
    • 使用現有的 AMD 工具最佳化其專案
    • 參與當今瀏覽器中可行的 JS 模組系統。

如果您撰寫 JavaScript 的程式碼載入器/引擎/環境

  • 實作 AMD API。有一個 討論串相容性測試。透過實作 AMD,您將減少多模組系統的樣板程式碼,並協助證明網路上可行的 JavaScript 模組系統。這可以回饋到 ECMAScript 程序,以建置更好的原生模組支援。
  • 同時支援 callback-require載入器外掛程式。載入器外掛程式是減少巢狀 callback 症候群的好方法,這種症候群在 callback/非同步樣式程式碼中很常見。