外部JavaScriptの動的ロード
JavaScriptからJavaScriptを読み込むのは、(IEに対応する場合は特に)結構面倒です。
ちょうど昨日公開されたid:amachangのはてブにアクセスチャートを出す Greasemonkeyでは、こんな実装になっていました。
// for IE if (!document.evaluate) { var script = document.createElement('script'); script.src = 'http://svn.coderepos.org/share/lang/javascript/javascript-xpath/trunk/release/javascript-xpath-0.1.11.js'; document.body.appendChild(script); var callee = arguments.callee; var interval = setInterval(function() { if (!document.evaluate) return; clearInterval(interval); interval = null; callee(); }, 100); return; }
setIntervalで100ms毎に読み込めているかチェックしています。
以前、xAutoPagerizeでJavaScript-XPathを使用する実装をしたときは、setTimeoutでこんな感じにしました。
function timerLoader(callback){ var src = 'http://svn.coderepos.org/share/lang/javascript/javascript-xpath/trunk/release/javascript-xpath-0.1.11.js'; var script = document.createElement('script'); script.type = 'text/javascript'; (function(){ var f = arguments.callee; setTimeout(function(){ if (document.evaluate) callback(); else f(); }, 500); })(); script.src = src; document.body.appendChild(script); }
で、折角なので、なんとかしてTimerを使わずに読み込む方法を考えてみました。
下準備
読み込みを遅らせる必要があるので、script要素のdefer属性の実装 - Thousand Yearsを参考に
<?php sleep(2); // 2秒待 header('text/javascript'); ?> hogehoge_data = {};
こんな感じのPHPを用意しました。
callback(JSONP)⇒一応成功
若干反則気味ですが、読み込み対象のScriptの最後に、
if(typeof external_script_onload=='function')external_script_onload("loaded");
こんな1行を追加します。もちろん、external_script_onloadって名前は任意です。
読み込む側はexternal_script_onloadにcallback関数を定義してあげればOKです。これで、Scriptが読み込まれて 実行された最後に、external_script_onloadが呼ばれることになります。
function onloadLoader(callback){ var src = 'http://ss-o.net/js/callback_test.php?'+(new Date-0); var sc = document.createElement('script'); sc.type = 'text/javascript'; window.external_script_onload = callback; sc.src = src; document.body.appendChild(sc); }
読み込み対象に手を加える必要があるものの、上の1行は特に害がないので割と気軽に追加できます。
callback+defer⇒失敗
で、ひょっとしたら、deferと組み合わせることで、汎用化(読み込み対象に手を加えなくても良いように)できるかもと期待したのですが、やはりうまく行きませんでした。
function deferLoader(callback){ var src = 'http://ss-o.net/js/callback_test.php?'+(new Date-0); // 2秒遅延 var src_def = 'http://ss-o.net/js/script_defer.js?'+(new Date-0); // すぐに読み込まれるけど、deferをつける var sc = document.createElement('script'); var sc_def = document.createElement('script'); sc.type = 'text/javascript'; sc_def.type = 'text/javascript'; sc_def.setAttribute("defer",true); window.external_script_onload = callback; sc.src = src; sc_def.src = src_def; document.body.appendChild(sc); document.body.appendChild(sc_def); }
やはりdeferはdocumentがcloseする前でないと動かないみたいですね。
img.onerror⇒失敗?
Operaでも非同期リクエストが並列処理できる img-JSONP | TAKESAKO @ Yet another Cybozu Labsのネタ*1が使えないかなと試して見たんですが、イマイチうまく行ってないかも。
function onerrorLoader(callback){ var src = 'http://ss-o.net/js/onerror_test.php?'+(new Date-0); var sc = document.createElement('script'); var sc_on = new Image; sc.type = 'text/javascript'; sc_on.onerror = sc_on.onload = function(){ sc.src = src; document.body.appendChild(sc); callback(); }; sc_on.src = src; }
特にerrorが発生するまで微妙にタイムラグがあるのがマイナスポイント。
onreadystatechange⇒成功!?
コメントで、37toさんに「onreadystatechangeが使える」と教えていただいたので、試してみました。
function onreadyLoader(callback){ var src = 'http://ss-o.net/js/onloadedready_test.php?'+(new Date-0); var sc = document.createElement('script'); sc.type = 'text/javascript'; sc.onreadystatechange = function(){ if (sc.readyState == 'complete') callback(sc.readyState); if (sc.readyState == 'loaded') callback(sc.readyState); }; sc.src = src; document.body.appendChild(sc); }
確かにいけてます。キャッシュされていないファイルについてはreadyStateがcompleteではなく、loadedになるようです。なので、どちらでも動作するようにしてみました。
というわけで、クロスブラウザでシンプルな関数にするとこんな感じでしょうか。
function JavaScriptLoader(src, callback){ var sc = document.createElement('script'); sc.type = 'text/javascript'; if (window.ActiveXObject) { sc.onreadystatechange = function(){ if (sc.readyState == 'complete') callback(sc.readyState); if (sc.readyState == 'loaded') callback(sc.readyState); }; } else { sc.onload = function(){ callback('onload'); }; } sc.src = src; document.body.appendChild(sc); }
IE6のonreadystatechangeにはメモリリークがあるそうなので注意。IEメモリリークの最後の壁はAjaxのonreadystatechangeやった!! - SEの行き着くところ…
テストページ
http://ss-o.net/test/script.html
結局、これっていう決め手にかける感じですが、callbackはそこそこ使えそうなのでここまで(ライブラリとかがcallbackに対応したら、動的な読み込みがかなり楽になりそう)。deferもiframeと組み合わせれば使えないこともなさそうですが、それはそれで使いにくいことになるので。
IE以外
IEに対応しなくても良いなら、普通にscript要素のonloadを指定すればよいので、
function onloadLoader(callback){ var src = 'http://ss-o.net/js/callback_test.php?'+(new Date-0); var sc = document.createElement('script'); sc.type = 'text/javascript'; sc.onload = callback; sc.src = src; document.body.appendChild(sc); }
とするだけです。
*1:余談ですが、Opera9.5では解決している問題です [http://d.hatena.ne.jp/os0x/20080728/opera95:title]