2010-02-25

JSONPを使いたいが、Operaの挙動がおかしい

http://www.example.com/が、以下のような内容を返すとする。

callback({
    "foo" : "bar"
}) ;

このとき、script要素を動的に生成することによって、same originを回避しつつ、しかもJSONデータをJavascriptから利用することが可能になる。

function callback(data) {}

var jsonp = document.createElement("script") ;
jsonp.setAttribute("type", "application/javascript") ;
jsonp.setAttribute("src", "http://www.example.com/") ;

document.head.appendChild(jsonp) ;
// callback()が呼ばれる。

このテクニックは、JSONPと呼ばれている。

実際のコードでは、HTML5のdocument.headに対応してないブラウザのため、別の方法でhead要素へのDOMを取得しているが、それは話の本題ではないので、省略する。

ここまではいい。しかし、このscript要素は、無駄であるので、できれば、消したい。

document.head.appendChild(jsonp) ;
// callback()が呼ばれる。
document.head.removeChild(jsonp) ;

わたしは予想では、これは動くはずであった。なぜならば、JavascriptによるDOM操作は、暗黙のうちに非同期になってくれたりはしないからだ。ひとたび、appendChild()が呼ばれたならば、DOMに追加され、読み込みが終わり、関連するすべての処理がおわるまで、Javascriptのコードは実行を続けることができないはずである。

これは、時として、ユーザーをブロックする原因にもなるが、この場合は、都合がいい仕様である。

Chrome, Safari, Firefoxでは、私の期待通りに、問題なくcallback()が呼ばれた。

しかし、Operaでは、動かなかった。

Operaでは、callback()は呼ばれるものの、その引数であるdataが、nullになっていた。removeChild()を呼び出さなければ、nullではなく、ちゃんと、JSONオブジェクトを参照しているのだ。

相変わらず、Operaの実装は分からない。最適化のために、script要素がDOMに追加された場合の、その中身の実行のタイミングを遅らせるなどの、妙なことをしているのではあるまいか。

実は、もう一つ、removeChild()を呼び出した場合の不都合が存在した。それは、ブラウザの「戻る」機能を使って、ページを戻した場合、動的に追加したscript要素内のコードが実行されないのだ。これは、すべてのブラウザに当てはまる。

結局、removeChild()を呼び出すのは、諦める事にした。script要素が存在することで、CPU時間やメモリなどのリソースの消費が、目に見えるほど増えることはない。

3 comments:

edvakf said...

昔から appendChild した script が同期というのは保証されてなかったと記憶しています。
試してみると、Firefox ではいつも同期、Safari と Chrome では src がキャッシュされてる場合は同期で、そうじゃない場合は非同期 (つまり removeChild した場合は callback が呼ばれない) で、Opera はいつも非同期みたいでした。

IE 以外では script.onload が使えます。
http://d.hatena.ne.jp/os0x/20080827/1219815828

余談ですが、document.write にもおもしろい話があります。
http://webreflection.blogspot.com/2009/12/documentwriteshenanigans.html

江添亮 said...

そうか、onloadを使えばよかったのか。
さっそく改良。

江添亮 said...

いまどきdocument.writeがどうしたと思ったら、これはすばらしい。