requirejsは、htmlにscriptタグを量産することなく、依存性管理を行える非常に便利なライブラリです。
今回は、これを使って、「必要になるまでモジュールを読み込まない」、遅延ローディングの方法を考えてみます。
通常の利用方法
通常は、requirejsを利用するコードは以下のように書きます。
// app.js
require(["mod"],function(Module){
console.log("app.js loaded");
document.querySelector("#hello").onclick = Module.hello;
});
// mod.js
define([],function(){
console.log("mod.js loaded");
return {
hello : function(){
alert("hello");
}
};
});
index.html
<!DOCTYPE htm>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.2/require.js" data-main="app.js"></script>
<button id="hello">hello!</button>
このコードを実行すると、以下のログが出力されます。
mod.js:2 mod.js loaded
app.js:3 app.js loaded
つまり、(当然ではありますが)helloボタンを押す前にモジュール側が先に読み込まれるわけです。
この形では、ロード時にすべてのjsファイルを読み込むような構造になるため、表示したいページ以外の本来不要なモジュールも全て初回に読み込んでしまい、パフォーマンス上の問題が生じます。
必要になるまで読み込まないよう(オンデマンド形式)にする
それでは、先ほどの例を参考にして、app.jsを変更してみましょう。
// app.js
require([],function(){
console.log("app.js loaded");
document.querySelector("#hello").onclick = function(){
require(["mod"],function(Module){
Module.hello();
});
};
});
ラップしているrequire関数では何も読み込まないようにして、clickイベントでrequireをコールするようにしています。これは、以下のようにも書けます。
console.log("app.js loaded");
document.querySelector("#hello").onclick = function(){
require(["mod"],function(Module){
Module.hello();
});
};
即時関数などで囲っても構いません。
(function(){
console.log("app.js loaded");
document.querySelector("#hello").onclick = function(){
require(["mod"],function(Module){
Module.hello();
});
};
})();
実行してみると、ボタンを押すまでは、
app.js:2 app.js loaded
としかコンソールには表示されませんが、ボタンを押した瞬間に、アラートと同時に、
mod.js:2 module.js loaded
が表示され、モジュールがロードされたことがわかります。
仕組み
至極単純なことですが、requirejsは、require([],function(){});
が実行された瞬間にscriptタグを生成し、scriptのonloadでコールバック関数を実行しています。
つまり、そのrequire
関数を必要になるまで実行しないようにすれば、任意のタイミングでモジュールのロードを行うことができます。
注意点
読み込みは非同期で行われますので、同期的な処理を行う場合は、Deferredパターンなどを組み合わせるとよいでしょう。
var xxx = require("xxx");
の場合
define(function(require){});
などの構文でモジュールを定義する場合は、同期読み込み的な構文を書くことができます。app.jsを変更して実行結果を見てみましょう。
// app.js
define(function(require){
console.log("app.js loaded");
document.querySelector("#hello").onclick = function(){
var Module = require("mod");
Module.hello();
};
});
このコードを読んだ人は、var Module = require("mod");
のタイミングで、初めてモジュールが読み込まれることを期待しますが、実際は、以下の順番で読み込まれます。
mod.js:2 mod.js loaded
app.js:3 app.js loaded
これはどういうことでしょうか?
requirejsのソースコードを調べてみると、2000行目付近に以下のようなコードが存在しています。
callback
.toString()
.replace(commentRegExp, commentReplace)
.replace(cjsRequireRegExp, function (match, dep) {
deps.push(dep);
});
defineのコールバックをtoStringしてreplaceしています。replaceの引数に渡されている正規表現は以下です。
commentRegExp = /\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/mg,
function commentReplace(match, singlePrefix) {
return singlePrefix || '';
}
cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
これは、functionの内容をStringとして構文解析して、事前に(defineで指定したときと同じタイミングで)読み込んでいます。
考えてみれば当然の話で、jsファイルが非同期で読み込まれる関係上、戻り値を同期的に変数に格納するようなvar xxx = require("xxx")
という関数は、どうやっても成立しないわけです。
しかし、同期的に読み込む(ように見せる)ようなコードの方が可読性が高くなることも多いので、あえてこのような構文も用意しているのでしょう。
まとめ
依存性管理として、require.jsは有力な候補のひとつです。本記事のような使い方をすれば、あらゆるjsファイルを必要になるまで読み込まなくすることができ、パフォーマンスに大きく寄与することが期待できます。
うまく活用して、パフォーマンスの高いシステムを作りましょう!