Sep 03, 2005

IEでXMLHttpRequestを使えるようにする

もうあんまり需要無いような気もするけど。
IEで「new XMLHttpRequest」と書けるようにするラッパを書いてみた。

XMLHttpRequest for IE
http://la.ma.la/misc/js/ie_xmlhttp.html

結構前にIE7(JavaScriptの方)の人が似たようなのを作ってたのですが、
http://dean.edwards.name/weblog/2004/11/ie7-xml-extras/

ふつうに使う分にはこれでも問題はないです。

if(typeof ActiveXObject == "function" && typeof XMLHttpRequest == "undefined"){
    XMLHttpRequest = function(){
        return new ActiveXObject("Microsoft.XMLHTTP")
    }
}


IEでXMLHttpRequestが未定義だったら、XMLHTTPオブジェクトを返すコンストラクタを定義してやる。
これだけでいい。ただ、これだとXMLHttpRequest.prototypeをいじったりすることができない。
IEのActiveXObjectっていうのはちょっと特殊で、メソッドなんかがJavaScriptのfunctionではない。

var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
alert(typeof xmlhttp.open); // unknown


こうなる。xmlhttp.openはfunctionではなくて謎オブジェクト。
FirefoxやOperaのXMLHttpRequest#openは、ちゃんとfunctionになっているのだが、
IEの場合は関数ではないので、関数型言語としてのJavaScriptの機能が使えない。

----

XMLHttpRequest for IEは何なのかというと、
IEでもXMLHttpRequestのメソッドをfunctionオブジェクトとして扱うことができるようになるラッパです。
あとついでにonloadイベントハンドラを定義してあるので記述が楽になります。

functionalでアスペクト指向なコードを書く際に、役に立ちますが、
functionalでアスペクト指向なコードを書かない場合は書き方が共通になって嬉しい程度で、
その辺はすでにprototype.jsとかあるので大してメリットはないと思われる。

ものすごくシンプルな例はこんな感じ。

var XP = XMLHttpRequest.prototype;
XP.open_old = XP.open;
XP.open = function(){
    alert("open called!");
    return this.open_old.apply(this,arguments)
}


XMLHttpRequest.prototypeを書き換えてやることで、通信をトレースできるようにしたり、機能を拡張したり制限を加えたりすることができるようになる。もちろんクロスブラウザで動く(safariは知らないけど)。

例えば、通信するときには必ず前回のリクエストから1秒のウェイトを入れるようにする、とか、
デバッグ用に通信状態をステータスバーに流すとか、
他ドメインのURLを開こうとした場合はプロキシCGI経由のリクエストにするとか、
UserAgentを強制的に書き換えるとか、キャッシュが利かないようにユニークなURLにするとか。

全般にわたる振る舞いを効率的に記述することができるようになる。
----

工夫したのはこのあたり。

var methods = "open,abort,send,setRequestHeader,getResponseHeader,getAllResponseHeaders".split(",");
var make_method = function(name){
    XMLHttpRequest.prototype[name] = function(){
        var params = new Array(arguments.length);
        for(var i=0;i<params.length;i++) params[i] = "_"+i;
        return Function(
            params.join(","),
            ["return this.__request__.",name,"(",params.join(","),")"].join("")
        ).apply(this,arguments);
    }
};
for(var i=0;i<methods.length;i++) make_method(methods[i]);

本来のXMLHTTPオブジェクトをthis.__request__に保存していて、
引数の数に応じて関数を動的に生成して、本来のXMLHTTPオブジェクトに処理を丸投げする。

this.open("GET",url)
→(function(_1,_2){this.__request__.open(_1,_2)}).apply(this,arguments)

this.open("GET",url,async)
→(function(_1,_2,_3){this.__request__.open(_1,_2,_3)}).apply(this,arguments)

this.open("GET",url,async,user,pass)
→(function(_1,_2,_3,_4,_5){this.__request__.open(_1,_2,_3,_4,_5)}).apply(this,arguments)

こんな具合に変換される。

ふつうにラッパーを書くときは、
function wrapper(){
    return original.apply(this,arguments)
}

こんな具合にやるんだけど、オリジナルのxmlhttp.openは上記のとおり、そもそも関数ではないのでxmlhttp.open.applyが存在しない。
eval使わないと無理かな、とも思ったんだけどFunctionコンストラクタ使うときれいに書けた。

Functionコンストラクタは文字列から関数を生成する。
add = new Function("a","b","return a+b");
そして、こう書いてもOK。
add = new Function("a,b","return a+b");

これはJavaScriptの方言ではなくてちゃんとECMA準拠。
http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/15-3_Function_Objects.html#section-15.3.2.1

まあ、あんまり使わないと思うんだけど。
Posted at 15:42 | WriteBacks (24) | Edit
Edit this entry...

wikieditish message: Ready to edit this entry.
















A quick preview will be rendered here when you click "Preview" button.