History.js ではまったこと

[History.js](https://github.com/browserstate/history.js) は，ブラウザヒストリ・ステート用 API (pushState 等) のブラウザ間非互換性を解消したり，(IE 9 以下など) pushState に対応していないブラウザでも hash を利用して擬似的に state の遷移ができるようにしたり，と便利なライブラリです。

ですが，いろいろとはまったことがありました。

## IE (等，エミュレーション実装) で戻った時にステートが反映されない

まず，以下のような状況で困りました。

1. History.js を利用したページ (A) をロード
2. pushState(B)
3. pushState(C)
4. リンク等により History.js を利用していないページ (D) に遷移
5. ブラウザの「戻る」ボタン等で戻る

正しい挙動はステート C になること (で，もちろん Chrome や Firefox ではそうなる) なのですが，IE 9 で D から戻ると A になってしまいます。

そこからさらに戻ると，きちんと？ステート B になります。

### 原因

この原因は，つぎのようなコーディングをしていたことにありました。

```javascript
$(document).ready(function () {
    $(window).on('statechange', function (ev) {
        // popState なイベントハンドラ
    });
});
```

DOM ready な状況で `statechange` イベントをハンドリングしようとしても，(別画面からの) 「戻る」遷移では発火されないのですね。

このようにする必要があります。

```javascript
// DOM ready ではなく，スクリプト読み込み時に，バインドを行う
$(window).on('statechange', function (ev) {
    // popState なイベントハンドラ
});
```

> 追記 2013-04-19 10:30
> 正確には DOM ready で `statechange` が発火されるんですが， History.js (の HTML4 用モジュール) を先に読み込んでいると，そいつの DOM ready handler より後で `statechange` handler を登録しても間に合わない，ということみたいです。
> ですので， History.js をよりあとで読みこむようにすると，後述するような面倒なコーディングをしなくていいのかも。

statechange ハンドラで DOM をいじりたい場合もあるでしょうが (というかほとんどの場合？），この場合は，statechange ハンドラではフラグ立てだけしておいて，DOM ready イベント時に (も)，擬似的にハンドラを呼ぶ必要があります。

```javascript
// わかりやすいようにあえてフラットに書いてます

var dom_ready           = false,
    initial_statechange = false,
    initial_state;

$(window).on('statechange', function (ev) {
    if (dom_ready) {
        on_pop_state(History.getState());
    } else {
        // DOM ready 前
        initial_state       = History.getState();
        initial_statechange = true;
    }
});

$(document).ready(function (ev) {
    dom_ready = true;

    if (initial_statechange) {
        initial_statechange = false;
        on_pop_state(initial_state);
    }
});

function on_pop_state(state) {
    // ......
}
```

### おまけ (`$(document).ready(fn)` について)

jQuery で DOM ready イベントハンドラはまぁ普通に

```javascript
$(function () {
    // ......
});
```

って書けばいいんですが，なんとなく抵抗感があって，

```javascript
$(document).ready(function () {
    // ......
});
```

という形式で書いています。

以前は (他のイベントハンドラと形式をあわせて)

```javascript
$(document).on('ready', function () {
    // ......
});
```

のように書いていたんですが，これ jQuery 1.8 以降では obsolete 扱いとなっていて，このように書いてはいけません。

どのように困るかというと，たとえば DOM ready イベントハンドラからさらに DOM ready イベントハンドラを (この obsolete 形式で) 登録しようとした場合 (わざとそうしようとしなくても，結果的にそうなることはよくあります), その2つ目のハンドラが呼ばれません (正しい形式で書くときちんと即座に呼ばれます)。くわしくは [.ready() | jQuery API Documentation](http://api.jquery.com/ready/) を参照してください。

## エンコードされた URL が文字化けする

たとえば，

```javascript
//         http://example.com/日本語
var url = 'http://example.com/%e6%97%a5%e6%9c%ac%e8%aa%9e';
History.pushState(null, null, url);
```

のようにすると，`statechange` イベントでうけとったステートオブジェクトの url プロパティが `http://example.com/%e6%97%a5%e6%9c%ac%e8%aa%9e` ではなく `http://example.com/日本語` になります。

で，IE でこの URL にアクセスしようとするとなぜかそこから CP932 にエンコードされたりして，もともとアクセスしたかった URL とは異なる URL になってしまいます。

これ，どうも [History.js](https://github.com/browserstate/history.js) が無駄にアグレッシブに `unescape()` しまくるからみたいなんですね。なので，手元のプロダクトではとりあえず，こんな感じにしています。

```javascript
var url = 'http://example.com/%e6%97%a5%e6%9c%ac%e8%aa%9e';
History.pushState(null, null, encodeURIComponent(url));
```

これたまたま今手元の環境で動いているだけで，どんな環境でも動くとは限らないです。

(今考えたら State data オブジェクトのほうに入れたらいいんじゃないかなと思いますが，前述のブラウザで別画面から戻る場合に問題が発生しそうですね)

このバグは GitHub issues でも何回か討議されてたりしますが， [History.js](https://github.com/browserstate/history.js) にきちんと手を入れないといけない部分だと思います。

### 追記 2013-04-16 16:30

だいたいこんな感じのパッチをあてればいい感じみたいです。
https://github.com/dayflower/history.js/commit/4c60734bf5d7ec41016d204f7f9e479205ea5ad7

Chrome 25, Firefox 20, IE 9 あたりでは確認済。
IE 9 だと非 latin な URL の場合，URL ハッシュが冗長になった気もしますが。

同じような内容のプルリクがもうあがっているので自分から Pull Request を送ったりはしません。ちょっと否定的？なコメントもあがってたりしたのであれがマージされるかはあやしいですが。Fork が多いのはみんな手元で直して使っているのかな？