こんにちは、ネットエージェント株式会社、研究開発部の長谷川です。
 さっそくですが、みなさんは「Advent Calendar」をご存じでしょうか? Advent Calendar と言えば、一般的には、クリスマス(12月25日)までの残り日数をカウントダウンするカレンダーを思い浮かべるかもしれませんが、ここで紹介する Advent Calendar とは、様々な業界、技術方面で活躍されているプログラマ有志が、毎日交代で1つずつ技術的なトピックスを紹介する技術系Webイベントのことです。12月1日より運用が開始され、入門者向けから実用的なもの、さらにはマニアックな研究的トピックまで、幅広い記事が日々更新されています。
 今回は JavaScript Advent Calendar の10日目の記事として、当ブログで「Internet Explorer 8 と jQuery を組み合わせた場合のクロスドメイン通信の方法」について解説したいと思います。

-----

 まずは、XMLHttpRequest によるクロスドメイン通信を解説します。現在、Mozilla Firefox や Google Chrome、Apple Safari では XMLHttpRequest Level 2 への対応が進んでおり、以下のコードを書くことでクロスドメインでの通信が可能です。

if( window.XMLHttpRequest ){
var xhr = new XMLHttpRequest();
xhr.open( "get", "http://remote.example.com/data.txt", true );
xhr.onreadystatechange = function(){
if( xhr.readyState == 4 && xhr.status == 200 ){
alert( xhr.responseText );
}
}
xhr.send( null );
}

 このように、リクエストを発行するJavaScript側は、これまで自ドメインを対象に通信していたのと全く同じコードを使用し、他ドメインへ接続できます。この際、他ドメイン側(サーバ側)では、Access-Control-Allow-Origin レスポンスヘッダによって、リモートからのアクセスの許可 or 拒否を決定します。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: text/plain; charset=utf-8

[テキストデータ]

 上記の例では「全てのアクセス元からの接続を許可」を意味する * を指定していますが、アクセス元に応じて、より細かく許可 or 拒否を設定できます。
 次の例では、example.jp から発行された XMLHttpRequest に対してのみ、リソースへのアクセスを許可しています。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://example.jp

 一方、IE8 および IE9 beta では、クロスドメインでの Ajax を実現する機能として、XMLHttpRequest Level 2 ではなく XDomainRequest という機能を実装しています。いまのところ、IE9正式版で XMLHttpRequest Level 2 をサポートするという話も聞こえてきていません。
 XDomainRequest は、サーバ側の Access-Control-Allow-Origin レスポンスヘッダに応じてリモートのリソースへのアクセスを制御するという点は XMLHttpRequest と同様ですが、呼び出し方法やプロパティなどは XMLHttpRequest とは互換性がありません。

if( window.XDomainRequest ){
var xdr = new XDomainRequest();
xdr.onerror = function(){
alert( "error" );
}
xdr.onload = function(){
alert( xdr.contentType + "\n" + xdr.responseText );
}
xdr.open( "get", "http://remote.example.com/data.txt" );
xdr.send( null );
}

 このように、IE8 以降では XMLHttpRequest とは非互換な形でクロスドメイン通信を実現する方法が実装されているため、XMLHttpRequest を使用したライブラリではクロスドメイン通信が行えません。

 次に jQuery でのクロスドメイン通信を見ていきましょう。JavaScriptのライブラリとしてもっとも普及している jQuery でも、ajax メソッドにおいて dataType に "json" や "text" 等を指定した場合には XMLHttpRequest が使用されますので、前述の通り Firefox や Chrome、Safari ではクロスドメインでの通信が可能ですが、IE8、IE9 では残念ながらできません。
 そこで、jQuery.ajax を使用してクロスドメイン通信を可能にするため、IEの XDomainRequest を XMLHttpRequest のように見せかけるラッパーオブジェクトを用意してやることにしましょう。
 実際に書くコードは以下の通りです。エラー処理やリクエスト先が同一ドメインかどうかの判定などは省略しています。

function hookXhr()
{
var xhr = function(){
this.readyState = 0; // uninitialized
this.responseText = "";
this.status = "";
this.onreadstatechange = undefined;
var xdr = new XDomainRequest();

xdr.onprogress = function(){
var f;
this.xhr.readyState = 2; // loaded
if( this.xhr && ( f = this.xhr.onreadystatechange ) ){
f.apply( this.xhr );
}
};

xdr.onload = function(){
var f;
this.xhr.readyState = 3; // interactive
if( this.xhr && ( f = this.xhr.onreadystatechange ) ){
f.apply( this.xhr );
}
this.xhr.responseText = xdr.responseText;
this.xhr.readyState = 4; // complete
this.xhr.status = "200";
if( f ){
f.apply( this.xhr );
}
};

this.open = function( method, url, async ){
return xdr.open( method, url, async );
readyState = 1;
}
this.send = function( body ){
xdr.send( body );
};
this.setRequestHeader = function( headerName, headerValue ){
};
this.getResponseHeader = function( headerName ){
if( headerName.match( /^Content\-Type$/i ) ){
return xdr.contentType;
}else{
return "";
}
};
xdr.xhr = this;
return this;
};
return new xhr();
}

 このような、XMLHttpRequestをラッピングする関数を用意しておき、jQuery.ajax の呼び出しにおいて、こちらを使用するようパラメータ xhr を与えます。

$.ajax( {
url : "http://remote.example.com/data.txt",
dataType: "text",
xhr : window.XDomainRequest ? hookXhr : undefined,
// XMLHttpRequest の生成に、hookXhr が呼び出される。
success : function( txt, status, xhr ){
alert( txt );
}
} );

 通常の ajax の使用では、あまり xhr パラメータは使用しませんが、この例のように XMLHttpRequest オブジェクトを生成する関数を指定することで、本来の XMLHttpRequest ではなく、XDomainRequest を使用したラッパーオブジェクトを使用できます。
 ちなみに、話は少しそれますが、IE9 beta および IE9 platform preview 7 においては XDomainRequest を使用した場合に responseText プロパティの末尾にゴミが付加されてしまうバグがあります。どうやら、コンテンツが utf-8 等で書かれている場合に、responseText の「文字数」としてコンテンツの「バイト数」分だけ領域を確保してしまうようで、そのため、実際の文字数より大きなメモリ領域になり、末尾にゴミがついた状態となるようです。もちろんレポートしておきましたので、IE9正式リリース版では修正されると思いますが…。

 では最後に、このラッパーを使用して、JavaScript によって JavaScript Advent Calendar およびPerlコミュニティ(JPerl)の Advent Calendar の記事タイトルを一覧表示させるサンプルページを紹介します(http://utf-8.jp/public/js2010.html)。このページでは、サーバ側の応答に Access-Control-Allow-Origin を付与させるため、Hideki Yamamura 氏 の作成された allow-any-origin.appspot.com というサイトをプロキシのように使用しています。

-----

 このように、JavaScript の進歩に伴い、これまで以上にブラウザ上だけで実現できるアプリケーションの幅が広がってきました。できることならば、ブラウザの独自実装が減り、この記事のようなバッドノウハウを考えなくても、効率的にアプリケーションを書くことができればいいですね。