jQuery の $(document).ready(fn) と IE

IE 6 と jQuery 1.1.4 ですが,$(document).ready(fn) イベントが,ページを表示するたびに実行されるような気がします。

単純に,

$(document).ready(function () {
    alert('hoge');
});

みたいなコードを読み込む html 1 があったとして,そこからリンク等で html 2 に遷移させます。

で,html 2 からブラウザの「戻る」ボタン等で html 1 に戻ると,再度 alert が表示されます。

Firefox / Opera / Safari では,html 1 に戻っても ready イベントは再度実行はされません(すでに ready イベントが発生した後の DOM ツリーになっていますので問題はありません)。

jQuery 自体を JavaScript で遅延ロードさせているからかなぁ。もうちょっと検証してみます。

追記

遅延ロードは関係なかったです。

以降 Cache-Control については無視します。

検証用コード。

<html>
    <head>
        <title>HTML 1</title>
        <script type="text/javascript" src="jquery.js"></script>
        <script type="text/javascript">
$(function () {
    alert('ready');

    if ($('#hidden1').val() != 'loaded') {
        $('#hidden1').val('loaded');

        $('#btn1').click( function () { alert('clicked'); } );

        alert('loaded for the first time');
    }
});
        </script>
    </head>
    <body>
        <h1>HTML 1</h1>
        <p>
            <a href="html2.html">to HTML 2</a>
        </p>
        <p>
            <input type="button" id="btn1" value="押す" />
            <input type="hidden" name="dummy" id="hidden1" value="" />
        </p>
    </body>
</html>

html2.html は適宜用意するとして。

Firefox / Opera / Safari の場合,

  1. HTML 1 を開く
  2. 「ready」と alert が出る
  3. 「loaded for the first time」と alert が出る
  4. 「押す」ボタンを押すと「clicked」と alert が出る
  5. HTML 2 へのリンクをクリックする
  6. 「戻る」ボタンで戻る
  7. 「押す」ボタンを押すと「clicked」と alert が出る

IE 6 の場合,

  1. HTML 1 を開く
  2. 「ready」と alert が出る
  3. 「loaded for the first time」と alert が出る
  4. 「押す」ボタンを押すと「clicked」と alert が出る
  5. HTML 2 へのリンクをクリックする
  6. 「戻る」ボタンで戻る
  7. 「ready」と alert が出る
  8. 「押す」ボタンを押しても「clicked」と alert が出ない

どういうことかというと,IE 以外のモダンブラウザの場合,ブラウザの「戻る」等で遷移した場合,JavaScript でいじられた DOM ツリー・イベントリスナ系はそのままに戻ります。ready イベントは発生しません。

IE の場合,ブラウザの「戻る」等で遷移した場合,サーバから html をロードした時点まで巻戻り(DOM ツリー・イベントリスナも初期状態になります),再度 JavaScript を実行します。

IE のときは裏技的に?<input>タグの値を設定しておくことで挙動を変えることができますが*1,そのかわりイベントリスナ等もリセットされているのでボタンを押してもメッセージが出ません。また DOM ツリーもリセットされるので,DOM ツリーをいじって既ロードかどうかを判定させることはできません。


JavaScript 詳しくないんで間違いとか多々あるかもしれません。

*1:type = hidden じゃなくて text にするとわかりやすいかも

jQuery の $.getJSON() と IE ではまった

今つくっているサイトでは,html のエンコーディングShift_JIS にしてるんで,合わせるために JavaScriptエンコーディングShift_JIS にしてます(jQuery だけは念のため UTF-8 指定してますけど)。

で,そこで JSON データをやりとりしているんですが,全部 Shift_JIS に統一していたんで,

JSON を吐くようにしていたんですが,どうも IE 6 で挙動が安定しない。

初回の Ajax ネゴのときにはうまくいっても,そこからブラウザで「進む」「戻る」してもう一度 Ajax ネゴると文字化けしたり,初回の Ajax ネゴから文字化けどころかまったくやりとりできなかったり。

うーむ,と思ってネットをいろいろあさっていたら,

な記事を発見したんですが,元サイトがリニューアル中で何が書いてあったか不明。気になる〜。

じゃあってことで元ネタを探してみたらちょっと古い記事ですが,

というのを発見。JSON の Content-Type を text/html で返すと危ないよ,とのことですけど,頭がわるいので,この記事の前半と後半(exploit)のつながりがいまいちわからない。

ともあれ,text/javascript というのは obsolete ですか。でも RFC がどうこういっても実際の実装はブラウザによりけりですよね。と思いもちょっとたぐってみると,これもちょっと昔の記事ですが

このサイトを合わせて見ると,Opera 8.5 だと text/x-javascript がベターぽくて,(当時の)Safari のことを考えると charset 指定はしたほうがいいっぽい。

ということで,x-javascript にしようかとも思ったんですが,結局,文字化けがらみだし〜ということで

  • Content-Type: application/json; charset=UTF-8

にしてみました。実質文字コードを変えただけです。


一応 IE で安定したっぽいし,Opera 9.23 だとこれでも OK でした。Safari 2.x でも OK。

結論。コンテンツを Shift_JIS に統一していても少なくとも JSON データの charset は UTF-8 のほうが無難?

ちなみに jQuery を読解してみたら,json のパースは eval を使っているぽい。JSONP でなければまぁ eval でも構わないというかサーバアプリサイドの責務にしちゃってもいいかなと私も思います。

おまけ:やはり jQueryIE ではまったところ

<select id="hoge">
    <option>hahaha</option>
    <option>fufufu</option>
</select>

みたいな html で,

$('#hoge').val('fufufu');

とすると,Firefox / Opera / Safari だとうまく選択されるのに,IE だとブランクになってしまいます。

面倒でも

<select id="hoge">
    <option value="hahaha">hahaha</option>
    <option value="fufufu">fufufu</option>
</select>

みたく value を指定しておかないとだめでした。

YUI Compressor が \uXXXX を文字に変換して困った

いやそんな圧縮のあれこれについて話をしたかったんじゃなかったんでした。

jQuery の 1.1.4 を YUI Compressor 2.2 で minify して組み込んでテストしてたら,IE のテスターから

ライン: 2
文字: 15579
エラー: 文字セットの範囲が不正です。
コード: 0

というエラーが出るんですけど……と報告をうけたのです。

んーと思って調べたら,もともとのコード,

var chars = jQuery.browser.safari && parseInt(jQuery.browser.version) < 417 ?
                "(?:[\\w*_-]|\\\\.)" :
                "(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",

ってところが,Compressor かけると,

var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?
"(?:[\\w*_-]|\\\\.)":"(?:[\\w■-■*_-]|\\\\.)",

になってたのでした(表記できない文字は■で代用)。

とりあえず使ってたのはそこだけだったんで,手書きで直したんですが,気になって jQuery 1.2.1 の Official minified をダウンロードして見てみたら,きちんと \uXXXX 表記のままでした。

んーなんでなんだろう。そんなオプションなさそうですし。

JavaScript ファイルの圧縮・再訪

JavaScript ファイルの圧縮と一口にいってもおおまかに次の3種類があります。

  • コンテンツの圧縮(gzip);ブラウザ機能による伸長
  • コンテンツの圧縮(compress);JavaScript による伸長
  • コンテンツの縮小(minify)

それぞれについて説明します。

コンテンツの圧縮;ブラウザ機能による伸長

ブラウザが圧縮コンテンツをサポートしている場合,リクエストヘッダの Accept-Encoding に gzip(や deflate)という値を渡します。この場合,サーバは圧縮されたコンテンツを渡すことができます。

一番簡単で安全なのは mod_deflate を使うことです*1。基本動作として*2リクエストヘッダの Accept-Encoding に gzip が指定されている場合のみ,モジュールが圧縮して伝送してくれます。

フィルタモジュールなので,CGIPHP)等による動的生成コンテンツに圧縮もかけられます。

ただし,デメリットもあります。

私見では今時これくらいはデメリットに入らないかと思います。


これらのデメリットが受容できないか,mod_deflate なんてうちのサーバに入ってないよって場合,静的コンテンツであれば,あらかじめ gzip で圧縮して置いておくこともできます。この際,適切な Content-Type と Content-Encoding を返してやることが望ましいです。

たとえば,JavaScript ファイル foo.js を gzip 圧縮して foo.jsz というファイル名にした場合,

# .htaccess

AddType application/x-javascript jsz
AddEncoding gzip jsz

みたくすればいいです。

このままだと,ブラウザが gzip 伸長に対応していることが前提になってしまいます。たとえば jQuery とかバリバリ使ってモダンブラウザしか相手にしないぜ〜ってことであれば静的 gzip 圧縮でもいいかも。

それ以外に対応するためには mod_rewrite 等を利用して,非対応ブラウザでは非圧縮コンテンツを返すように設定する必要があります(参考⇒prototype.jsを10KBにする方法の続き(.htaccessをスマートに使う) | 亜細亜ノ蛾)。わたし的には mod_rewrite を使うくらいなら mod_deflate 使えよと思いますが,世の中には mod_rewrite ならインストールされてるんだけど,というサーバも少なくないかも。

ちなみに Web で調べて見たところ Safari についてちょっと情報が錯綜してますが,

  • Safari 1.x は Accept-Encoding を送らない。gzip 圧縮された JavaScript ファイルにも対応してないっぽい。
  • Safari 2.x は Accept-Encoding を送る。ので,gzip 圧縮された JavaScript ファイルにも対応している。

というのが私の試した結果です。

コンテンツの圧縮(compress);JavaScript による伸長

これについてはかつて書きました(⇒JavaScript の圧縮 - daily dayflower)ので詳しく説明しません。

静的辞書法等でファイルを圧縮しておき,ブラウザに読み込まれたときに JavaScript で展開する,というものです。

デメリットとしては,

  • 一見難読化されているようだけど,元のコードに簡単にもどせるのでその部分は期待できない
  • ファイルを読み込んだ後に JavaScript で展開するため,クライアントサイドで実際の処理を開始するまでにラグが発生・負荷が上昇する。

があります。ので,今日日流行っていません。

コンテンツの縮小(minify)

今アツイのがこれ。

  • 基本機能: コメントの削除
  • 基本機能: 余分な空白,改行,セミコロンの削除
  • 応用機能: ローカル変数名の短縮・難読化

どのような実装があるのかというと,

など,いろんな言語であります。

前者2つはいずれも Rhino*3をパーサとして利用しているので,文法的に不要なものを厳密に判断しやすい…つまり圧縮率が高いです。その代わり,重いためオンライン圧縮(リアルタイム圧縮)には向いていません。

後者2つは…きちんとコードを読んでないのですが,正規表現等による簡易パーサを利用しているので改行等不要な部分が結構残ってしまうらしいです。また上であげたローカル変数名等については原理上対応しにくいです*4。その代わり安全性が高く(ロジックが変わりにくい),軽快なためオンライン圧縮に向いています。

YUI Compressor 作者の Julien Lecomte 氏によると,Dojo Compressor はローカル変数名の短縮をやりすぎるのでやや危険,YUI Compressor は minify と同じくらい安全だよ,とのことですが(⇒Introducing the YUI Compressor | Julien Lecomte's Blog),まあ本人の言なのでなんとも。


で,ロジックからすると静的辞書法などに比べると断然圧縮率は落ちるのですが,なぜアツイかというと,これと最初にあげた gzip 圧縮を絡めることができて,そうすると圧縮率を補えるからです。

File File size Gzipped file size
Original jQuery library 62,885 bytes 19,758 bytes
jQuery minified with JSMin 36,391 bytes 11,541 bytes
jQuery minified with Packer 21,557 bytes 11,119 bytes
jQuery minified with the YUI Compressor 31,822 bytes 10,818 bytes
http://www.julienlecomte.net/blog/2007/08/21/gzip-your-minified-javascript-files/

まあ YUI Compressor + Gzip が一番小さくなったのは結果的にそうなったというだけでアレなんですが,

  • 元々 63KB あった jQuery (1.1系列と思われる)が
  • gzip 非対応ブラウザでも 32KB に
  • gzip 対応ブラウザだと 11KB に!

という風にとらえることができると思います。

もちろん世の中がすべて gzip 対応ブラウザだと何もしなくても 20KB で済むのですが,余計なコメント等がないと,ブラウザ側の JavaScript パーサもやや処理が軽くなるんじゃないかという期待もあります。

まとめ

  • minifing + mod_deflate だとみんな幸せ


おまけ。mod_deflate がないけど mod_rewrite を使ってあれこれするのも…という向きには,自分で gzip 圧縮するスクリプトを書くという手もあります。たとえば⇒ http://www.julienlecomte.net/blogfiles/gz.phps


あと,圧縮とは関係ないですが,複数の小さな JavaScript ファイルを読み込むより,まとめて一つのファイルにしたほうが転送は早い気がします。たとえ Keep-Alive でも。Firebug の Net タブを見ていてそう思いました。

追記 2007/09/21

2007年09月21日 miya2000 javascript mod_deflate って毎回圧縮するんでしょうか?

http://b.hatena.ne.jp/miya2000/20070921#bookmark-5944766

基本的に毎回圧縮します。

もちろん,サーバ側 Expires ヘッダやクライアント側 If-Modified-Since ヘッダの絡みでリクエストが発生しない場合がありますが,サーバにリクエストが届けば都度都度圧縮します(そういえば ETag はどうなってるんだろう)。キャッシュ的な機能はありません。

上にも書いたように,巨大なサイトを運営しているのでなければ気になるほどのサーバ負荷ではないと思ってます(実験したわけではなくてただの予想)。もし気になるようであれば,静的なファイルに限った話ですが相当な未来の Expires ヘッダを吐くようにして,js ファイル名にタイムスタンプを加えるとか,「<script src="hogehoge.js?1190265347" ...」みたくすればいいんじゃないかと思います。動的コンテンツじゃないと面倒ですが。

*1:適切な設定が必要ですが

*2:SetEnvIf 等である種の環境変数を指定することでこのへんの挙動は変わります⇒[http: //httpd.apache.org/docs/2.2/mod/mod_deflate.html:title]

*3:Java による JavaScript 実装

*4:packer のように独自記法を導入すれば不可能ではないと思いますが

jQuery の submit() ではまった

jQueryAjax でフォームのバリデーションをして,オッケーなら submit するようなコード書いてたんですが,

$('#button_submit').click(
    function () {
        $.ajax({
            type: 'POST',
            url:  'バリデータ',
            success:
                function () {
                    $('#form_main').submit();
                    return;
                }
        });

        return false;
    }
);

コードはあくまで雰囲気程度です。

いろんなブラウザでテストしてうまくいってたので本番にもデプロイしたんですが,上司の使っている IE 6 でだけなぜかうまくいかなかった罠。他の人の IE なら OK なのに。

submit()

Trigger the submit event of each matched element. This causes all of the functions that have been bound to thet submit event to be executed.

Note: This does not execute the submit method of the form element! If you need to submit the form via code, you have to use the DOM method, eg. $("form")[0].submit();

いまひとつわかんないんですが,たぶん,$(...).submit() だと submit イベントハンドラだけ実行するだけだから,ほんとに submit したけりゃ

                    $('#form_main')[0].submit();

とかしろってことかいな。んで,そのようにしたらうまくいきました。

なぜ環境依存がおきたのかは未だに謎。つーか他の環境でうまくいっていたことのほうが不思議な気がします。英語 or JS 得意な人に教えてほしいです。

JSONのセキュリティの、疑問、の、回答案

東京で舘野にも聞いたんだけど、JSONとクロスドメインのデータ漏洩の問題がよく分からない。

http://d.hatena.ne.jp/brazil/20070328/1175076777

データ漏洩/セキュリティではなくって,たとえば,すっごく便利な JSON(P) サービスを自社用に作ったけど,無制限で他所から使われるとマシンリソースとか食われて不愉快!ってシチュエーションが思い当たりました。

C++ スタイルのコメントを除去

JavaScript のコメントは C++ と同じようなスタイルなんですが,// や /*〜*/ を単純に正規表現で除去しようとすると,クォーテーションされた文字列リテラル内にコメントを含んでいる場合にうまくいきません。

んで,苦し紛れに考えたコード。

my $text = shift;

my $cppcom = qq{ //   .*? \\n  };
my $c__com = qq{ /\\* .*? \\*/ };

my $dquot = qq{ " (?: \\\\\\\\ | \\\\. | [^\\\\"]+ )* " };
my $squot = qq{ ' (?: \\\\\\\\ | \\\\. | [^\\\\']+ )* ' };

my @tokens
    = split qr{ ( $cppcom | $c__com | $dquot | $squot ) }xmso, $text;

my $f = 0;
foreach my $token (@tokens) {
    if (! $f) {
        $token =~ s/ \s+ / /gxmso;
    }
    elsif ($token =~ m' \A /[/*] 'xmso) {
        $token = q{};
    }
    $f = 1 - $f;
}

return join q{}, @tokens;

コメントを除去しつつ余分なスペース/タブ/改行を畳むようにしてます。

これでほんとに等価なのかしらん。我ながらスマートなコードじゃないし!そもそもコード中に正規表現リテラルがある場合を考慮してないし!

クォーテーション内の正規表現が間違ってたのでなおしました。