なぜAMDなのか?

このページでは、JavaScriptモジュール用の非同期モジュール定義(AMD)API(RequireJSでサポートされているモジュールAPI)の設計思想と使用法について説明します。 ウェブにおけるモジュールへの一般的なアプローチについては別のページで説明しています。

モジュールの目的 § 1

JavaScriptモジュールとは何ですか? その目的は何ですか?

  • 定義:コードの一部を有用な単位にカプセル化する方法と、モジュールの機能/値を登録/エクスポートする方法。
  • 依存関係の参照:他のコード単位を参照する方法。

今日のウェブ § 2

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

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

今日のJavaScriptコードはどのように定義されていますか?

  • 即時実行されるファクトリー関数によって定義されます。
  • 依存関係への参照は、HTMLスクリプトタグを介してロードされたグローバル変数名を使用して行われます。
  • 依存関係は非常に曖昧に記述されています。開発者は適切な依存関係の順序を知る必要があります。たとえば、Backboneを含むファイルは、jQueryタグの前に配置することはできません。
  • 最適化されたデプロイメントのために、一連のスクリプトタグを1つのタグに置き換えるには、追加のツールが必要です。

これは大規模なプロジェクトでは管理が難しくなる可能性があり、特にスクリプトが多くの依存関係を持ち始め、それらが重複してネストするようになるにつれて、管理が難しくなります。手書きのスクリプトタグはあまりスケーラブルではなく、必要に応じてスクリプトをロードする機能がありません。

CommonJS § 3

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

オリジナルのCommonJS(CJS)リスト参加者は、今日のJavaScript言語で動作するが、ブラウザのJS環境の制約に必ずしも束縛されないモジュール形式を作成することを決定しました。目的は、ブラウザでいくつかの応急処置を使用し、ブラウザメーカーにモジュール形式がネイティブでより良く動作するソリューションを構築するように影響を与えることでした。応急処置は

  • サーバーを使用してCJSモジュールをブラウザで使用可能なものに変換します。
  • または、XMLHttpRequest(XHR)を使用してモジュールのテキストをロードし、ブラウザでテキスト変換/解析を実行します。

CJSモジュール形式では、ファイルごとに1つのモジュールのみが許可されていたため、最適化/バンドリングの目的で、ファイルに複数のモジュールをバンドルするために「トランスポート形式」が使用されます。

このアプローチにより、CommonJSグループは依存関係の参照、循環依存関係の処理方法、および現在のモジュールに関するいくつかのプロパティを取得する方法を解決することができました。ただし、変更できないにもかかわらずモジュール設計に影響を与えるブラウザ環境のいくつかのものを完全には受け入れていませんでした

  • ネットワークローディング
  • 固有の非同期性

また、Web開発者にフォーマットの実装をより負担させることになり、応急処置はデバッグを悪化させることを意味しました。 evalベースのデバッグまたは1つのファイルに連結された複数のファイルをデバッグすることは、実際的な弱点があります。これらの弱点は、将来的にはブラウザツールで対処される可能性がありますが、最終的な結果は、最も一般的なJS環境であるブラウザでCommonJSモジュールを使用することは、今日では最適ではありません。

AMD § 4

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

AMD形式は、今日の「手動で順序付ける必要がある暗黙的な依存関係を持つスクリプトタグをたくさん書く」よりも優れており、ブラウザで直接簡単に使用できるモジュール形式を求めていました。サーバー固有のツールを必要とせずに、優れたデバッグ特性を持つものです。それはDojoのXHR + evalの使用に関する実世界の経験から生まれ、将来のためにその弱点を回避したいというものでした。

それはウェブの現在の「グローバル変数とスクリプトタグ」よりも優れています。なぜなら

  • 依存関係に文字列IDを使用するというCommonJSの慣例を使用します。依存関係を明確に宣言し、グローバル変数の使用を回避します。
  • IDを異なるパスにマッピングできます。これにより、実装を入れ替えることができます。これは、単体テスト用のモックを作成するのに最適です。上記のコード例では、コードはjQuery APIと動作を実装するものを期待するだけです。jQueryである必要はありません。
  • モジュールの定義をカプセル化します。グローバル名前空間を汚染しないようにするためのツールを提供します。
  • モジュール値を定義するための明確なパス。 "return value;" または、循環依存関係に役立つ可能性のあるCommonJS "exports" イディオムを使用します。

CommonJSモジュールよりも優れている理由は

  • ブラウザでより良く動作し、落とし穴が最も少なくなっています。他のアプローチでは、デバッグ、クロスドメイン/ CDNの使用、file://の使用、およびサーバー固有のツールが必要になるという問題があります。
  • 1つのファイルに複数のモジュールを含める方法を定義します。 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 () {};
});

自分でモジュールに名前を付けるのは避け、開発中は1つのファイルに1つのモジュールのみを配置する必要があります。ただし、ツールとパフォーマンスのために、モジュールソリューションでは、構築されたリソース内のモジュールを識別する方法が必要です。

糖衣構文 § 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');
}

これらのケースは、AMDローダーに通常存在するコールバックrequirerequire([moduleName], function (){})によって処理されます。

AMDの実行モデルは、ECMAScript Harmonyモジュールの仕様策定とより密接に連携しています。AMDラッパーでは動作しないCommonJSモジュールは、Harmonyモジュールとしても動作しません。AMDのコード実行の振る舞いは、将来の互換性が高いです。

冗長性 vs. 有用性

AMDに対する批判の1つ(少なくともCJSモジュールと比較して)は、インデントと関数によるラッピングが必要になる点です。

しかし、真実はこうです。AMDを使用するために認識される余分なタイピングとインデントは問題ではありません。コーディング時に時間が費やされるのは、以下の点です。

  • 問題について考える。
  • コードを読む。

コーディング時間は主に思考に費やされ、タイピングにはあまり費やされません。一般的に言葉が少ない方が望ましいですが、そのアプローチが有効であるには限界があり、AMDでの余分なタイピングはそれほど多くありません。

ほとんどのWeb開発者は、グローバル変数でページを汚染しないように、いずれにせよ関数ラッパーを使用します。機能が関数でラップされているのを見るのは非常に一般的な光景であり、モジュールの読解コストを増大させることはありません。

CommonJS形式には、隠れたコストもあります。

  • ツール依存のコスト
  • クロスドメインアクセスのような、ブラウザで動作しないエッジケース
  • より悪いデバッグ、時間の経過とともに増え続けるコスト

AMDモジュールは必要なツールが少なく、エッジケースの問題が少なく、より良いデバッグサポートがあります。

重要なのは、実際に他の人とコードを共有できることです。AMDは、その目標への最も低いエネルギー経路です。

今日のブラウザで動作する、動作しやすく、デバッグしやすいモジュールシステムを持つことは、将来のJavaScriptに最適なモジュールシステムを作る上で、実世界での経験を得ることを意味します。

AMDとその関連APIは、将来のJSモジュールシステムのために以下を示しました。

  • モジュール値として関数を返すこと、特にコンストラクタ関数は、より良いAPI設計につながります。Nodeはこれを許可するためにmodule.exportsを持っていますが、「return function (){}」を使用できる方がはるかにクリーンです。module.exportsを行うために「module」のハンドルを取得する必要がなくなり、より明確なコード表現になります。
  • 動的なコードローディング(AMDシステムではrequire([], function (){})を介して行われます)は基本的な要件です。CJSはそれについて議論し、いくつかの提案がありましたが、完全には受け入れられませんでした。Nodeはこのニーズをサポートしておらず、代わりにrequire('')の同期的な振る舞いに依存しており、これはWebに移植できません。
  • ローダープラグインは非常に便利です。コールバックベースのプログラミングでよく見られるネストされたブレースインデントを避けるのに役立ちます。
  • 1つのモジュールを選択的に別の場所からロードするようにマッピングすると、テスト用のモックオブジェクトを簡単に提供できます。
  • モジュールごとに最大で1つのIOアクションのみであるべきであり、それは簡単でなければなりません。Webブラウザは、モジュールを見つけるために複数のIOルックアップを行うことを許容しません。これは、Nodeが現在行っている複数のパスルックアップに反対し、package.jsonの「main」プロパティの使用を避けることを主張します。プロジェクトの場所に基づいて1つの場所に簡単にマッピングできるモジュール名を使用し、冗長な構成を必要としない適切なデフォルトの規約を使用しますが、必要な場合は簡単な構成を許可します。
  • 古いJSコードが新しいシステムに参加できるように、「オプトイン」呼び出しを実行できるのが最適です。

JSモジュールシステムが上記の機能を提供できない場合、AMDと、callback-requireローダープラグイン、パスベースのモジュールIDに関する関連APIと比較すると、大きな不利な立場に立たされます。

今日のAMDの使用 § 9

2011年10月中旬現在、AMDはすでにWebで広く採用されています

あなたができること § 10

アプリケーションを作成する場合

スクリプト/ライブラリの作成者の場合:

  • 利用可能な場合は、オプションでdefine()を呼び出してください。良い点は、AMDに依存せずにライブラリをコーディングでき、利用可能な場合にのみ参加できることです。これにより、モジュールの消費者は以下が可能になります
    • ページにグローバル変数をダンプすることを避ける
    • コードローディング、遅延ローディングのためのより多くのオプションを使用する
    • 既存のAMDツールを使用してプロジェクトを最適化する
    • 今日のブラウザで動作するJavaScript用のモジュールシステムに参加する。

JavaScript用のコードローダー/エンジン/環境を作成する場合

  • AMD APIを実装してください。ディスカッションリスト互換性テストがあります。AMDを実装することにより、マルチモジュールシステムのボイラープレートを削減し、Web上で動作するJavaScriptモジュールシステムを実証するのに役立ちます。これは、ECMAScriptプロセスにフィードバックされ、より良いネイティブモジュールサポートを構築できます。
  • callback-requireローダープラグインもサポートしてください。ローダープラグインは、コールバック/非同期スタイルのコードでよく見られるネストされたコールバック症候群を減らすための優れた方法です。