JSONPの動的取得+エラー処理

JavaScriptから外部ドメインにあるAPIを呼び出すために使われるJSONPですが、scriptタグを動的に追加する方法(下記の記事など)JSONPの取得時にサーバ過負荷などでエラーが出た場合、エラー処理ができないという欠点がありました。
クロスドメインJavaScript呼び出しをクラス化, クロージャにも対応 - Okiraku Programming


scriptタグに onerror= という属性を付加するとエラー発生時にスクリプトを実行させることができるブラウザもあります。しかし試してみると、

  • Firefox: サーバがステータス4xx, 5xxを返した際にonerrorが実行される。
  • Safari: サーバがステータス404を返した際にのみonerrorが実行される。
  • Opera: 実行されない
  • IE: 実行されない

といったように、ブラウザごとに挙動がまちまち。

またステータスが200で何か返ってきたものの、スクリプトとしてコンパイルエラーが発生するようなデータを受信した際には、いずれもonerrorは実行されません。


上記のような状況でもエラー処理をしたい場合、iframeを動的に生成して、その中でJSONPを読み込んで結果をiframe内に保持しておき、そのiframeのonloadイベントで結果を親フレーム側から取得する、という方法が使えます。
エラー等でJSONP呼出しが失敗すると、onload発生後もフレーム内に結果が入っていないため、これでエラーと判定することができます。
詳しくは次のサイトが参考になります。

参考サイト:閉鎖


この方法でクロスドメインJSONPを取得するコードを書いてみました。

上記サイトにあるコードに対して、

  • エラー時にリトライする回数を指定可能(指定回数以上エラー時にエラーハンドラが呼ばれる。)
  • URL中でcallback関数名を指定する引数を変更できる
  • 複数の引数を返すAPIにも対応
  • 呼び出しを中止する abort メソッドを追加

といった機能追加を行いました。

Opera 10と11, IE8, Firefox 3.6, Safari 5, Chrome 9で動作を確認しています(古いバージョンは未確認)。

コードは以下。
※当初公開したコードだと、Operaで一部のuser.jsを入れている環境でonloadが二回発生し、コールバックが二重に呼び出されていました。下記ではcntを追加して二重実行を防止しています。

var xds = {
	load: function(url, callback, onerror, retry, callback_key) {
		var ifr = document.createElement("iframe");
		ifr.style.display = "none";
		document.body.appendChild(ifr);
		var d = ifr.contentWindow.document;
		var cnt = 0;
		ifr[ifr.readyState/*IE*/ ? "onreadystatechange" : "onload"] = function() {
			if (this.readyState && this.readyState != 'complete' || cnt++) return;
			if (d.x) {
				if (callback) callback.apply(this, d.x);
			} else if (retry && retry > 1) {
				setTimeout(function(){ xds.load(url, callback, onerror, retry-1) }, 1000);
			} else if (onerror)
				onerror();
			setTimeout(function(){ try { ifr.parentNode.removeChild(ifr); } catch(e) {} }, 0);
		};
		var url2 = url + (url.indexOf('?')<0?'?':'&') +
			(callback_key?callback_key:'callback') + '=cb';
		d.write('<scr'+'ipt>function cb(){document.x=arguments}</scr'+'ipt>' +
			'<scr'+'ipt src="'+url2+'"></scr'+'ipt>');
		d.close();
		return ifr;
	},
	abort: function(ifr) {
		if (ifr && ifr.parentNode)
			ifr.parentNode.removeChild(ifr);
	}
}

APIを呼び出すには、

xds.load(APIのURL, コールバック関数, エラーハンドラ関数);

のようにします。


APIのURLは、呼出時に callback=〜 という指定が自動的に追加されます。
コールバック関数名の指定が callback= 以外の場合は、第5引数に = の前の部分を指定します。(flickrREST APIなら 'jsoncallback' とか)


成功時はコールバック関数にJSONPの結果が、エラー時はエラーハンドラ関数が呼び出されます。


例えばGoogle翻訳APIなら

var f = xds.load("http://www.google.com/uds/Gtranslate?v=1.0" +
		"&langpair=|ja&context=test&q=" + encodeURIComponent("Hello!"),
		function(id,result) { alert(result.translatedText) },
		function() { alert("Error...") }
);

のように、成功時に呼び出す関数、エラーハンドラ関数をそれぞれ指定します。これで、

  • 呼び出し成功時: 翻訳されたテキストを表示
  • エラー時: "Error..." というダイアログを表示

という動作をします。

なおGoogle翻訳APIは元々引数としてエラーを返す機能を持っていますが、このスクリプトではインターネット接続が不可能だったりAPIがメンテ中等の状況でもエラーハンドラが実行させることができます。


上記に加えて、第4引数に数値を指定すると、エラー時に指定回数まで1秒毎にリトライし、それでもダメならエラーハンドラを呼び出します。

また、xds.loadの返り値をコールバック関数やエラーハンドラ関数が呼ばれる前に xds.abort に渡すと、APIの呼出しを中止させることができます。

xds.abort(f);  // 途中でxds.loadを中止させる

twicliのxds.loadを更新

このエラーハンドリング機能付きのJSONPローダをtwicliでも利用するようにしました。


これで、Twitter APIが過負荷のためにエラーや鯨のページを返してきた場合でも、リトライをかけることができます。
(ただしGETで呼び出すAPIのみ。POSTが必要な発言(update)やfav、フォロー等はこれではリトライできません。。。)


twicliのxds.loadは、上記に加えて

という拡張が加えてあります。

また、以下のメソッドを追加しています。プラグイン作成の参考にして下さい。なお以前のloadXDomainScript()もそのまま利用可能です。

  • xds.load_default(url, callback, old, callback_key):

エラーハンドラの指定を省略したもの。エラー時に自動的に3回までリトライし、それでもダメならエラーダイアログを表示します。
oldに前回のxds.load_defaultの返り値を渡すと、API呼出し前に前回の呼出しをabortで中止します(null指定も可能)。
以前のloadXDomainScriptの代用として利用可能です。

  • xds.load_for_tab(url, callback, callback_key)

TL、@以外のタブに表示するデータを読み込む時に使用します。
読み込み中にタブを切り替えると、それまでに発行中だったAPIが中止されます。

  • xds.abort_tab()

load_for_tabで発行中のAPIを全て中止します。内部的にタブ切り替え時に呼び出しています。