最近、ちょっと作りたいWebサービスがあって、不慣れなjavascriptを書いてるのですが、上手い書き方がわからずに困っていたものがありました。
それは、Web APIからのコールバックをどう書くか、という問題です。
通常、javascriptでWeb APIを呼ぶ場合は、JSONPを使ってコールバック関数を呼び出す形にします。
具体的に例を挙げて説明しましょう。
Ustのチャンネル名からその動画を貼り付ける場合、getInfoなどのWeb APIを呼び出して、その中のembedTagを貼りつけてやります。
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script> <div id="ust"></div>
function getUstInfo( channel ) { var api_url = 'http://api.ustream.tv/json/channel/'; var api = api_url + encodeURIComponent( channel ) + '/getInfo?callback=?'; $.getJSON( api, function( info, status ) { displayUst( info.embedTag, '#ust' ); }); } function displayUst( embed_tag, target ) { $( target ).replaceWith( embed_tag ); } getUstInfo( 'ぬこのこ' );
この書き方にはいくつか問題があります。
getUstInfoでは本来、Ustの該当チャンネルのデータをもらってくるだけをしたいのですが、これだけで表示までが行われてしまいます。
実は、データだけを持ってくるような関数を書くことが出来ないので、仕方なくその後の表示まで一緒にやってしまっているのです。
だから、もし他のところでもinfoを取ってきたい場合には、同じコードをコピペで書かないといけません…
この問題は、コールバックが1つだけであればまだ良いのですが、複数のWeb APIを呼んでその結果をまとめて表示に利用する場合には、コールバック関数からさらにコールバックを呼び出して… というふうにコーディングしなければならず、だいぶ汚い書き方になります。
また、Web API呼び出しが並列に行えないため速度的にも不利です。
コールバック関数の中で値返せばいいじゃん、と考えて下記のように変更すると、上手く動きません。
var ust_tag; function getUstInfo( channel ) { var api_url = 'http://api.ustream.tv/json/channel/'; var api = api_url + encodeURIComponent( channel ) + '/getInfo?callback=?'; $.getJSON( api, function( info, status ) { ust_tag = info.embedTag; }); } function displayUst( embed_tag, target ) { $( target ).replaceWith( embed_tag ); } getUstInfo( 'ぬこのこ' ); displayUst( ust_tag, '#ust' );
こう書いてあるとコールバック関数はその場で実行されるような気がしてしまいますが、実際にはWeb APIが呼ばれていますから、その答えが返ってきたタイミングでgetJSONで渡されている中身(ust_tag = info.embedTag)が実行されます。
だから、displayUstが呼ばれるタイミングではまだ情報が返ってきていないため、表示がされません。
こういった場合どうしたらいいだろう… とtweetしてたら、@KojiSaitoさんと@hkobaさんからいろいろと助言をいただき、@hkobaさんから「Deferred使えばいいよ」と教えていただきました。
Deferrdとはなにかというと、詳しくはこちらの
「jQueryのDeferredとPromiseで応答性の良いアプリをー基本編 | ゆっくりと…」
の説明が超おすすめなのでそれを読んでもらうとして、ひとことで言うと「準備が出来た時点でこれやってね!というのを書ける仕組み」なのです。
Deferredの基本的な使い方は、promiseで一旦返しておいて、実際に処理が終わった時にはresolveというのを返します。そしてresolveした時に実行する内容はthenというメソッド内に書いておきます。
実際に書いたものを見たほうがわかりやすいと思うので、jQuery Deferrdを使って同じ処理を書いた例を見てください。
function getUstInfo( channel ) { var dfr = $.Deferred(); var api_url = 'http://api.ustream.tv/json/channel/'; var api = api_url + encodeURIComponent( channel ) + '/getInfo?callback=?'; $.getJSON( api, dfr.resolve ); return dfr.promise(); } function displayUst( embed_tag, target ) { $( target ).replaceWith( embed_tag ); } getUstInfo( 'ぬこのこ' ).then( function( info ) { displayUst( info.embedTag, '#ust' ); });
どうですか?
getしてくる部分と表示部分が、しっかり分離されていることがわかると思います。
また、promiseで一旦返しておいて、API呼び出しの答えが返ってきた時点でthenの中身が呼ばれるため、その間に並列で処理を進めることができます。
DeferredはjQuery1.5以上で利用できるようになっています。
また、DojoやMochiKitといった他のjavascriptライブラリでも同様の機能が利用できるそうです。