History.js は,ブラウザヒストリ・ステート用 API (pushState 等) のブラウザ間非互換性を解消したり,(IE 9 以下など) pushState に対応していないブラウザでも hash を利用して擬似的に state の遷移ができるようにしたり,と便利なライブラリです。
ですが,いろいろとはまったことがありました。
IE (等,エミュレーション実装) で戻った時にステートが反映されない
まず,以下のような状況で困りました。
- History.js を利用したページ (A) をロード
- pushState(B)
- pushState(C)
- リンク等により History.js を利用していないページ (D) に遷移
- ブラウザの「戻る」ボタン等で戻る
正しい挙動はステート C になること (で,もちろん Chrome や Firefox ではそうなる) なのですが,IE 9 で D から戻ると A になってしまいます。
そこからさらに戻ると,きちんと?ステート B になります。
原因
この原因は,つぎのようなコーディングをしていたことにありました。
$(document).ready(function () {
$(window).on('statechange', function (ev) {
// popState なイベントハンドラ
});
});
DOM ready な状況で statechange
イベントをハンドリングしようとしても,(別画面からの) 「戻る」遷移では発火されないのですね。
このようにする必要があります。
// 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 イベント時に (も),擬似的にハンドラを呼ぶ必要があります。
// わかりやすいようにあえてフラットに書いてます
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 イベントハンドラはまぁ普通に
$(function () {
// ......
});
って書けばいいんですが,なんとなく抵抗感があって,
$(document).ready(function () {
// ......
});
という形式で書いています。
以前は (他のイベントハンドラと形式をあわせて)
$(document).on('ready', function () {
// ......
});
のように書いていたんですが,これ jQuery 1.8 以降では obsolete 扱いとなっていて,このように書いてはいけません。
どのように困るかというと,たとえば DOM ready イベントハンドラからさらに DOM ready イベントハンドラを (この obsolete 形式で) 登録しようとした場合 (わざとそうしようとしなくても,結果的にそうなることはよくあります), その2つ目のハンドラが呼ばれません (正しい形式で書くときちんと即座に呼ばれます)。くわしくは .ready() | jQuery API Documentation を参照してください。
エンコードされた URL が文字化けする
たとえば,
// 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 が無駄にアグレッシブに unescape()
しまくるからみたいなんですね。なので,手元のプロダクトではとりあえず,こんな感じにしています。
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 にきちんと手を入れないといけない部分だと思います。
追記 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 が多いのはみんな手元で直して使っているのかな?