外部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毎に読み込めているかチェックしています。

以前、xAutoPagerizeJavaScript-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]