Hatena::ブログ(Diary)

Okiraku Programming RSSフィード Twitter

2011-02-06 JSONPの動的取得+エラー処理

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

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

クロスドメインJavaScript呼び出しをクラス化, クロージャにも対応 - Okiraku Programming


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

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

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


上記のような状況でもエラー処理をしたい場合、iframeを動的に生成して、その中でJSONPを読み込んで結果をiframe内に保持しておき、そのiframeのonloadイベントで結果を親フレーム側から取得する、という方法が使えます。

エラー等でJSONP呼出しが失敗すると、onload発生後もフレーム内に結果が入っていないため、これでエラーと判定することができます。

詳しくは次のサイトが参考になります。

参考サイト閉鎖

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

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

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

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, コールバック関数, エラーハンドラ関数);

のようにします。


APIURLは、呼出時に 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...") }
);

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

という動作をします。

なお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を全て中止します。内部的にタブ切り替え時に呼び出しています。

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。