はじめに
Dojoにはモジュールを遅延ロード(lazy load)する機能が備わっており、これをうまく使うと、ページのダウンロードサイズを最小化することによってAJAXアプリケーションのパフォーマンスを最適化することができます。
Dojo 1.7では、目玉の新機能としてAsynchronous Module Definition(AMDと略されます)ローダーというものが新しく導入され、Dojoがモジュールを遅延ロードする方法、特に非同期にロードする方法が大幅に改善されました。また、Dojo 1.6からDojo 1.7にかけて、AMD APIと呼ばれる標準化された新しいモジュールの定義方法が採用されています。
ここではこれらのDojoに新しく採用されたAMD API/ローダーについて扱います。
Asynchronous Module Definition(AMD)API
AMD APIとは
AMD APIはモジュールを定義するAPIで、CommonJSというグループに標準とされるべく提唱されています。このAPIで定義されたモジュールはそれが依存するモジュールを含めて非同期にロードすることができます。これは同期ロードに対し、ブラウザのJavaScript環境にとって次の観点から特に有効です:
- 同期ロードをすると、その間アプリケーション(ブラウザによってはブラウザ全体も)はユーザが何をしても応答できない(固まってしまう)が、非同期ロードではそれがない。
- 同期ロードでは同時に1つのHTTPリクエストしか発行できない(上と似た理由によります)が、非同期ロードでは同時に複数のHTTPリクエストを発行できる。これによりコンテンツの量とHTTP接続の数のバランスを最適化することによってアプリケーションのパフォーマンスを向上することができる。
- 同期ロードで一般的に使われるXMLHttpRequestではセキュリティ上の理由からメインのHTMLファイルが置かれているサーバーとしか通信できないという制約(クロスドメイン制約)があるが、非同期ロードで一般的に使われる動的に<script>タグを作成する方法ではその制約はない。これによりメインのHTMLファイルを自社サーバに置きながら社外サーバに置かれたJavaScriptモジュールを利用することができる。
- 同期ロードで一般的に使われるXMLHttpRequestで読まれたモジュールはその後eval()という関数を使うことにより初めてJavaScriptとして評価される。eval()で評価されたJavaScriptコードは一般的にデバッグがしにくい。非同期ロードで一般的に使われる動的に<script>タグを作成する方法ではそういったデバッグのしにくさはない。
AMD APIをサポートするライブラリ(AMDローダーと呼ばれます)はDojo 1.7のほかに(抜粋すると)次のようなものがあります:
これら2つはどちらもある程度のDojoとの互換性に関する検証がなされています。またDojoと似通ったカテゴリに入るAJAXライブラリのjQueryでも1.7においてAMDのサポートがされたそうです。こういった互換性はAMD APIのもう一つの大きなメリットで、AMDに準拠するどのモジュールも好きなAMDローダーを使ってロードすることができます。
AMD APIの文法
AMDでは、define()という関数でモジュールを定義します。define()関数は次の形をとります:
define(id?, dependencies?, factory);
- id(文字列, 任意): 定義されるモジュールのID。モジュールのIDはdefine()関数内で定義しなくても後述のAMDローダーが自動的に決めてくれるので、省略されることが多い。
- dependencies(文字列の配列, 任意): 連鎖依存するモジュールのID。
- factory(関数または任意のオブジェクト, 必須): モジュールを返す関数またはモジュール自体。関数がここで使われた場合、その引数は依存するモジュールとなる。
モジュールのIDは一般的にモジュールのパスから.jsを取り除いたものとなります。例えばdojo/_base/array.jsのモジュールIDはdojo/_base/arrayとなります。
AMD APIを使ったモジュールの書き方
上記のAMD APIを使って簡単な価格引き合わせモジュールを書いてみます。これは次の2つのモジュールに依存します:
- my/pricelist: 商品名に対応する価格。
- my/discount: 日付に対応する割引率を返す関数。
これらの実装は次のようになります:
define({ apple: 350, orange: 250 });
define(function(){ var normalrate = 1; // 通常の掛け率 var specialrate = 0.7; // 3のつく日の掛け率 return function(d){ var rate = normalrate; if(String(d).indexOf("3") >= 0){ rate = specialrate; // 3のつく日は3割引!! } return rate; }; });
どちらの例でもモジュールIDを明示的に定義する必要も連鎖依存するモジュールもないため、define()関数の第一引数(id)、第二引数(dependencies)ともに省略されています。
my/pricelist.jsの例では第三引数(factory)はりんごが350円、みかんが250円というオブジェクトで、これがモジュールとして定義されます。my/discount.jsの例ではfactoryは定義されるモジュールを返す関数で、その関数の返り値、つまり日付に対して割引率を返す関数がモジュールとして定義されます。
そして目的の価格引き合わせモジュールは、上の2つのモジュールを依存モジュールとして定義し、次のように書くことができます:
define([ "my/pricelist", "my/discount" ], function(pricelist, discount){ return function(product){ if(!(product in pricelist)){ return NaN; // 価格表にない製品 } return pricelist[product] * discount((new Date()).getDate()); }; });
この例では、define()関数の第二引数(dependencies)に”my/pricelist”と”my/discount”が指定され、my/priceモジュールがmy/pricelistとモジュールとmy/discountモジュールに依存することが示されています。第三引数(factory)はmy/pricelistモジュールとmy/discountモジュールを引数とする関数となり、その返り値、つまり製品の当日価格を返す関数がmy/priceモジュールとして定義されます。