こんにちは、ほそ道です。
今回はクライアントサイドのJSモジュール管理の仕組みを提供するRequireJSを取り上げ、その仕組みを調べてみます。
公式ページによる紹介
公式のトップページのイントロダクションでおおまかな目的を述べてくれてます。
RequireJSはJSファイルとモジュールのローダです。
ブラウザで使用するように最適化されておりますがNodeやRhinoでもいけますよ。
RequireJSのようなローダーを使う事はあなたのコードの速度と質を発展させる事になりますよ。
また、IE6以上で使うことができるようで現在では大概の環境下で動作可能と言えますでしょう。
サンプルで仕組みをつかむ
サンプルを作ってみましょう。
コンストラクタを持つモジュールとオプション群を持つモジュールを組み合わせてオーダーメイドなマイカーを作ってみます。
ファイル・ディレクトリ構成は下記のようになってます。
PROJECT_HOME
├── index.html
└── scripts
├── main.js (最初に実行されるエントリーポイント)
├── modules
│ ├── Car.js (コンストラクタモジュールを提供するファイル)
│ └── parts.js (オプションモジュールを提供するファイル)
└── require.js (require.js本体)
require.jsは公式のダウンロードページからゲッツ出来ます。
それではindex.htmlを見ていきます。ポイントはheadタブの中だけなのでheadタブのみを載せます。
<head>
<script data-main="scripts/main" src="scripts/require.js"></script>
<script>
require.config({
urlArgs: "bust=" + (new Date()).getTime()
});
</script>
</head>
- 1つめのscript要素ではrequire.jsを呼び出しつつ
data-main
属性を付与しています。
これはRequireJSの仕組みが最初に呼び出すエントリーポイントとなるJSファイルで、「.js」の部分は省略します。 - 2つのscript要素にrequire.configとしてタイムスタンプを生成したりしていますが、これは今回モジュールとエントリーポイントのキャッシュを避ける為にこんな記述を入れてます。
次にコンストラクタモジュールを提供するscripts/modules/Car.jsです。
define(function() {
return function(_body, _tire) {
this.body = _body
this.tire = _tire
};
});
-
define
関数で要求元に渡すモジュールを定義します - 引数にはオブジェクトリテラルと関数が渡せます
- 関数にした場合にはreturnしたものが要求元に渡されるので、今回はコンストラクタ関数を渡してみました
次にオプションモジュールを提供するscripts/modules/parts.jsです。
define({
bodyType: {
wagon: 'wagon specs...',
sedan: 'sedan specs...'
},
tire: {
small: 220,
large: 280
}
});
- 今度はオブジェクトリテラルを渡すようにしてみました。
最後にエントリーポイントであるscripts/main.jsを見ていきます。ここでマイカーを組み上げます。
require(['modules/Car', 'modules/parts'], function(Car, parts) {
var myCar = new Car(parts.bodyType.wagon, parts.tire.small);
console.log(myCar); // Object {body: "wagon specs...", tire: 220}
});
-
require
関数の第一引数ではエントリーポイントからの相対参照でモジュール群を要求します。ここでも「.js」は省略します - ファイルロード完了時にはロードされたオブジェクトを引数にコールバックを実行します
- コールバックの中ではコンストラクタとオプションを組み合わせてCarインスタンスを生成しています
- コンストラクタ関数を返すようにしたのでコールバックの引数Carはコンストラクタ関数となっています
オプションの内容がショボいですが、これでRequireJSを利用してオーダーメイドなマイカーを作る事ができました!
ブラウザ上のロードの仕組み
今度はどうやってモジュールが読み込まれているのかを見てみましょう。
答えはhead要素にあります。
各ファイルがscript要素としてheadに追加されています。
require.jsのソース的にはreq.load
メソッドのあたりでhead要素への追加を行っている事がわかります。
モジュール同士でのアクセス
サンプルでCarコンストラクタとpartsコンストラクタは互いにアクセス出来ない関係にありましたが、下記のようにする事で参照する事が出来ます。
define(["./parts"], function(parts) {
console.log(parts);
return function(_body, _tire) {
this.body = _body
this.tire = _tire
};
});
define
関数のコールバックの前に配列でモジュールを指定する事で、コールバックの引数にモジュールが渡ります。
モジュール設計的な観点では凝集度が下がってしまうため、基本的にはモジュール同士は粗結合にさせるべきだと思いますが、利用する側とされる側の責務分担が明確に分かれているケースではこの仕組みを使うと良いと思います。
スコープ
モジュールを要求するrequire
ないしはrequirejs
オブジェクトがグローバル空間に展開されるオブジェクトとなっているようです。この2つは別名の同一オブジェクトです。
あとはdefine
関数もグローバルに展開されています。
よって、理屈的にはhtmlファイル上でrequireやdefine関数を使用する事は可能ですし、
ひとつのファイル内で複数回require,defineを実行する事も可能ですね。
ロードしたモジュールを使えるのはコールバック内部だけなのでグローバル空間を汚す事がない事はいい感じですね。
ただしrequireやdefineという名前は十分に衝突の可能性を秘めた名前だと思いますのでその辺は意識した方が良さそうです。
require.config
require.config(obj)
メソッドを使用することでRequireJSの振る舞い設定ができます。
設定内容はオブジェクトリテラルの形式で引数に設定します。
いくつか例を紹介しておきます。
barUrl: mainを始めとしたモジュール群のルートパスを文字列で指定する
paths: モジュールごとのパスをオブジェクトリテラルで指定する
shim: 外部ライブラリと依存関係を定義してLoad順序を制御する
===
今回は以上です。
RequireJSですが下記の点で良いんじゃないかと思います。
チーム開発でも威力を発揮できそうに思います。
・シンプルな決まり事
・VIEWをscript要素だらけにしない事
・グローバル空間に宣言される名前がrequireとdefineくらいで衝突を防ぎやすい
またモジュール管理、依存関係管理系のライブラリに関してはチェックしていきますんでその際にはRequireJSとの比較等やっていきたいと思います。