このエントリは、
東京Node学園祭 2012 アドベントカレンダー 5日目の記事です。
■ 前置き - AMD とは
AMD (Asynchronous Module Definition) は、Javascript のコードをモジュールとして定義して、非同期ないし遅延ロードするための仕組みです。
http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition (現在、接続が遅い模様)
CommonJS により提唱されたものですが、昨年あたりからクライアントサイド (ブラウザ) で JavaScript モジュールを構築する仕組みとして各所で一気に取り上げられ、現在ではクライアントサイドの主要なライブラリでもサポートされてきている(AMD によるモジュールとして利用できる)状態にあります。
モジュールに依存性を指定する仕組みも用意されています。遅延ロードされますので、依存モジュールの読み込みが完了次第、目的の(依存元の)モジュールの定義処理が実行されます。
比較的カオスになりがちなクライアントサイド Javascript が、このモジュール化によりすっきりしたものになります。
また処理は非同期で行われるため、ブラウザ上での読み込みの速度向上が見込まれます。実際に、Twitter の Web ページの高速化にも一役買ったとの話があります。
Twitter Engineering Blog:
http://engineering.twitter.com/2012/05/improving-performance-on-twittercom.html
Publickey による日本語紹介:
http://www.publickey1.jp/blog/12/twitter51.html
クライアントサイドで AMD を利用するには、AMD 機能を提供してくれるライブラリを用います。たとえば、 RequireJS があります。
http://requirejs.org/
■ サーバサイド/クライアントサイド Javascript のモジュール共通化
もし仮に Web アプリケーションのサーバ実装を Node.js で行っていて、かつある処理をサーバ/クライアントのどちらでも行いたいとするならば、その処理を行うコードを改変することなくサーバ/クライアントどちらでも実行できればという考えが出てきます。
そこで、その処理のコードを Node モジュールかつ AMD モジュールとして実装しておき、環境にあわせて適切にモジュールとして読み込まれるようにします。
今回は例として、「その年の 1/1 から今日までの日数の回数 hello world を出力する」というつまらない機能を、Node/AMD 共通モジュールとして作ります。
この問題に対して、Lo-Dash (
http://lodash.com/) と Moment.js (
http://momentjs.com/) の2種類のライブラリを使おうと考えました。
Lo-Dash, moment ともに Node モジュールとして npm で導入できると共に、AMD モジュールとしても利用できるもので提供されています。
実際に実装したコードが次のものです。
(function(define) {
define('hellodays', ['lodash', 'moment'], function (_, moment) {
function hellodays() {
var days = Number( moment().utc().format('DDD') );
_.times(days, function (d) {
console.log('Hello, world ! (' + (d + 1) + ' days)');
});
}
return hellodays;
});
})(
// 1: AMD
typeof define === 'function' ? define :
// 2: Node
typeof module !== 'undefined' ? function(id, deps, factory) {
module.exports = factory.apply(this, deps.map(require));
} :
// 3: Plain (window object)
function(id, deps, factory) {
var dependencies = [ this._, this.moment ];
this[id] = factory.apply(this, dependencies);
});
2行目で実行している define は、AMD API の仕様に基づき Javascript モジュールを定義するための関数です。引数として AMD 内モジュール名 (以後、id と呼びます)、依存モジュールのリスト (以後、deps と呼びます)、およびモジュールの構築を行う関数 (以後、factory と呼びます) を指定します。
モジュール構築関数 factory 内でオブジェクトあるいは関数を返すと、それがモジュールとして利用できるようになります。ここでは、実際に 1/1 から現在日までの日数を算出し、その回数分 hello, world を出力する処理を記述しています。
実際に Node/AMD モジュールのためのコントロールを行っているのは、1行目から定義している即時関数の引数として与えている、13 行目からのコードです。若干分かりやすくなるよう、コメントを明記しています。
もしグローバル空間に define 関数が定義されているのであれば、AMD によるモジュール機構を備えているため、そのまま define を即時関数に与えて実行させるようにします。これで、AMD モジュールとして hellodays が利用できるようになります。
AMD define 関数はないものの、module が定義されているのであれば Node のモジュール機構と見なし、define のような振る舞いをする関数を与えます。
define への引数はモジュール名 id、依存するモジュール名のリスト deps と、モジュール構築のための関数 factory が与えられるわけなので、Node の require を使って deps で指定されている依存モジュールを読み込み、得られたインスタンスを factory の引数として与えます。factory を実行すると構築された関数が返されるので、それを module.exports にセットします。これで、Node でこの hellodays モジュールを require すると、factory で作られた関数が得られます。
最後は、AMD でも Node でもないパターンです。これまでのブラウザ用 Javascript ライブラリであれば、window オブジェクトへ割り当てるくらいはしていることと思います。lodash, moment もそれぞれ _, moment という名称で参照可能になっていることでしょうから、それらを factory の引数に与えてモジュール構築を行います。名称として id が define の引数で渡ってくるので、その値で window オブジェクトの要素の1つとして定義しています。
以上、AMD > Node > plain という優先度でモジュール定義を行うことができるようになりました。
状況にあわせて依存モジュールの実体であるインスタンスが適切に factory に渡っている、factory の結果が利用されている、あたりがポイントになります。
■ 共通モジュールを利用する
サーバサイドである Node ならば次のように利用します。
普通の Node モジュールの利用となんら違いはありません。(require で読み込めるようモジュールパスが解決されている場合)
var hellodays = require('hellodays');
hellodays();
一方クライアントサイドの場合は、AMD が利用できるよう RequireJS を使います。
上記で作成したモジュールが読み込まれ、hellodays 変数として参照可能になってやってきます。
<script src="./scripts/require.js"></script>
<script>
requirejs.config({
paths: {
"lodash": "./scripts/lodash.min",
"moment": "./scripts/moment.min",
"hellodays": "./scripts/hellodays"
}
});
requirejs(['hellodays'], function (hellodays) {
hellodays();
});
</script>
Jam (
http://jamjs.org/) などのクライアントサイドパッケージマネージャを使うと、依存先となるモジュールの導入や、AMD (RequireJS) で読み込むパスの管理などを一手に引き受けてくれるので、もう少し楽に利用できるようになります。
$ jam install lodash
$ jam install moment
<script src="./jam/require.js"></script>
<script>
requirejs.config({
paths: {
"hellodays": "./scripts/hellodays"
}
});
requirejs(['hellodays'], function (hellodays) {
hellodays();
});
</script>
最後に、クライアントサイドで AMD を利用しない場合です。必要なファイルをすべて読み込んだ後、直接 hellodays 変数の関数を実行します。利用の機会はほとんどないと思いますが、利用すること自体は問題はありません。
<script src="./scripts/lodash.min.js"></script>
<script src="./scripts/moment.min.js"></script>
<script src="./scripts/hellodays.js"></script>
<script>
hellodays();
</script>
■ 共通モジュールとして用意する対象について
今回依存モジュールとして利用した Lo-Dash や Moment.js のような、サーバ/クライアント関係なく汎用的に利用できるライブラリなどは、共通するモジュールとして利用できると大きな価値がありそうです。他にもフローコントロールやイベント管理などは同じように共通モジュール化の価値があると考えられます。
一方、自前で提供するサービスのためのコードではと考えると、データの正規化 / 妥当性確認 (Filter / Validator) ルールの共通モジュール化に一定の価値がありそうです。
例えばユーザ情報の更新処理において、あるルールに基づいた妥当性確認をサーバサイドだけでなくクライアントサイドでも行って確認結果をいち早く掲示したい、といった要件がある場合。これまでなら同一ルールながらも異なるコードで実装したり、妥当性確認処理を非同期で逐一サーバと通信して行ったりしていたところです。
共通モジュール化されているならば、妥当性確認処理をクライアントで行い、サーバへ POST されてきた情報も同じモジュールで再度確認する、といったことができます。
■ 備考. テストについて
共通モジュールとして実装されたコードのテストをどうするか?
現在の私の場合はですが、まずは使い慣れた環境による、提供されうる機能のテストを書きます。
すなわち、Node モジュールとして提供されて、Node 上でなんら問題なく利用できるである確信のためのテストを用意します。
その上で、AMD モジュールとして読み込んで利用可能な状態になるかのテストを用意しています。
Node モジュールの amdefine (
https://github.com/jrburke/amdefine) を使うことで、Node 上で define を使ったコードが一時的に利用可能になります。
これを用いてモジュールをロードし、読み込んだモジュールが不自由なく(そこまでの機能テストは省略しつつ。機能の妥当性は前述のテストコードで満たされているはずなので。)利用できれば、良しとしています。
■ 備考2. Node の RequireJS モジュールと AMD 採用の是非
実は Node 上でも RequireJS モジュールが用意され、利用することができます。
http://requirejs.org/docs/node.html
非同期にモジュールを読み込む AMD のための RequireJS ですが、Node の場合は立ち上がったサーバインスタンスが基本的に永続した上でモジュールインスタンスの永続化がコードの書き方次第で容易だったり、Node 本体の require 時におけるインスタンスの再利用性が悪くなかったりといった理由から、その Node モジュールが RequireJS に依存する、というのは必要ないのではないかと個人的には思っています。