March 29, 2007

Ajaxでdocument.writeするJavaScriptへの対策

最近サイトをAjax化、つまり非同期通信をすることによってアクセス時の体感速度を向上させようとしています。JavaScriptライブラリのサイズを小さくするという内容の記事にも書いたとおり、Ajaxの非同期通信は有名ライブラリであるprototype.jsを利用して実現しようと考えていましたが、ここでJavaScript、特にdocument.writeをしてくるJavaScriptを非同期で扱う際にサイトが壊れるという現象に遭遇しました。そこで今回はその対策についておこうと思います。

少し長くなりそうなので、サイトをご覧の方は『続き』からどうぞ。

まず、サイトが壊れるという現象ですが、外部JavaScriptファイルとしてext.jsというのがあったとします。その内容はdocument.writeをするだけのもので、以下のとおりだとします。

document.write("hoge");

これをAjaxの非同期ではなく従来の同期的な方法、つまりHTMLファイルにscriptタグを記述して読み込んだとします。

<html>
<head></head>
<body>
ada

<script type="text/javascript" src="ext.js"></script>
</body>
</html>

何のことはない、問題なくadaとhogeが表示されます。しかしこのスクリプトをprototype.jsを使って非同期で読み込む、つまり以下のようなHTMLから呼び出すとどうなるかというと

<html>
<head>
<script type="text/javascript" src="prototype.js"></script>
</head>
<body>
ada

<script type="text/javascript">
<!--
new Ajax.Request(
    'ext.js',
    {
        method: 'get',
        onComplete: function(res){}
    });
//-->
</script>

</body>
</html>

IE 6では思ったとおりの動作、つまりadaとhogeが表示されるのですが、Firefox 1.5.0.7ではなぜか一瞬adaのみが表示されたあとhogeだけ表示された画面になりました。なお、prototype.jsのAjax::Requestですが、レスポンスのMIMEがtext/javascriptの場合、そのスクリプトを自動的にevalするという仕様になっています。詳しくはAjax.Requestのもったいない使い方をどうぞ。

このようになるのは非同期通信による影響だと考えられます。つまりHTMLが閉じた後にdocument.writeされても、document.writeの引数は呼び出し元のHTML内には取り込まれずに、文章全体を上書きをしてしまっていると考えられます。おそらくIEの動作よりはFirefoxの方が定義的には正しい動作ではないのでしょうか。

ここで諦めてしまっては元も子もないので、対策を考えました。まず、prototype.jsを改造してAjax::Requestの自動evalを阻止するオプションを設けました。ここではprototype.jsの1.5を前提としいます。prototype.jsの938行目付近

    if ((this.getHeader('Content-type') || 'text/javascript').strip().
        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
        this.evalResponse();

を次のようにしました。

    if (! this.options['noautoeval']){
    if ((this.getHeader('Content-type') || 'text/javascript').strip().
        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
        this.evalResponse();
    }

これでnoautoeval: trueとした場合のみ自動evalが働かなくなります。自動evalをしなくなった変わりに非同期読み込みが完了した時点での処理を充実させ、document.writeを実質無効化します。ext.jsを呼出すHTMLは以下のようにしました。

<html>
<head>
<script type="text/javascript" src="prototype.js"></script>
</head>
<body>
ada

<div id="placeholder"></div>
<script type="text/javascript">
<!--
new Ajax.Request(
    'ext.js',
    {
        method: 'get',
        noautoeval: true,
        onComplete: function(res){
document._write = document.write
var html = [];
document.write = function(s){html.push(s);}
eval(res.responseText);
document.write = document._write;
$('placeholder').innerHTML = html.join('');
        }

    });
//-->
</script>
</body>
</html>

document.writeを一時的に乗っ取り、html配列へdocument.writeの引数を格納しています。そのあとplaceholderというidのdiv要素へそれを流し込んでいます。

こうすることでAjaxの非同期通信化においてもdocument.writeをしてくるJavaScriptを扱うことができるようになりました。ちなみに関数乗っ取りのアイデアはdocument.write()の実行タイミングをずらす方法を見てはっとさせられました。関数オブジェクトってやっぱり便利ですね。

最後に、document.writeをしてくるJavaScriptはこのサイトの再度バーにあるBlogPeopleやDrecomなど、現実に結構な数があると思います。このようなスクリプトは確かに設置が楽ですが、Ajaxの非同期通信のようにちょっと込み入ったことをしようとすると、とたんに問題になります。この記事がそういったスクリプトへの対策として有効に機能してくれれば幸いです。

14:02 fenrir が投稿 : 固定リンク | | このエントリーを含むはてなブックマーク | この記事をdel.icio.usでブックマーク | トラックバック
このエントリーのトラックバックURL: http://fenrir.naruoka.org/mt/mt-tb.cgi/555
コメント

なるほど。
原因とその対策の説明が非常に役に立ちました!!!
助かりました!ありがとうございます!

Posted by: akira : May 31, 2011 01:07 PM

>akiraさん
お役に立てたようで何よりです。

Posted by: fenrir : June 1, 2011 07:21 AM
コメントする









名前、アドレスを登録しますか?
(次回以降コメント入力が楽になります)
  • 匿名でのコメントは受け付けておりません。
  • 名前(ハンドル名可)とメールアドレスは必ず入力してください。
  • メールアドレスを表示されたくないときはURLも必ず記入してください。
  • コメント欄でHTMLタグは使用できません。
  • コメント本文に日本語(全角文字)がある程度多く含まれている必要があります。
  • コメント欄内のURLと思われる文字列は自動的にリンクに変換されます。