History APIとスマホブラウザでのハマりどころ


仕事でスマホ用サイトをイジっておりまして、非同期処理でのページングを
修正するに際して、History APIを触ってました。
スマホのブラウザ周りの差分で色々とハマったので、メモしておきます。

# History APIとは
とりあえず復習しましょう。
## 概要
HTML5で追加された、履歴操作のAPI。
履歴スタックに履歴を追加（push)、現在の履歴の書換（replace)、
ブラウザバック等のアクションによる履歴移動を監視する(pop)、といったことが可能になる。
history自体は以前からあって履歴の操作は画面遷移を伴っていたが、これらのAPIの追加によって、
画面遷移を行わなくともJavaScriptからの履歴操作が可能となった。

## pushState
履歴をスタックに追加する。

```javascript:pushStateの例
history.pushState('test', null, '/hoge/fuga');
```

「test」という識別子(state)で履歴を追加、
対応するURLとして「/hoge/fuga」を設定するということになります。
履歴が追加されることにより、ブラウザ上のURLが書き換えられます。

## replaceState
現在の履歴を置き換える。

```javascript:replaceStateの例
history.replaceState('test', null, '/hoge/fuga');
```
引数はpushStateと同じですが、履歴の追加ではなく置き換えとなります。

## popState
履歴の[window.onpopstate](https://developer.mozilla.org/en-US/docs/WindowEventHandlers.onpopstate)イベントの発火により、履歴の移動を検知できます。
コールバック関数をイベントバインドしておきましょう。

```javascript:onpopstateのバインド例
window.addEventListener('popstate', function (e) {
    ...コールバック処理を記述
}
```
## history.state

pushState、replaceStateで設定したstateは、historyオブジェクト経由で取得できます。

```javascript:history.stateの例
history.replaceState('hoge', null, '/hoge/fuga');
console.log(history.state); // hogeと出力
```

# スマホブラウザでの挙動
大概のやつは普通に使えるでしょ、と軽い気持ちでやったら色々と差分があって
うぉーとなりました。
ハマった点を以下につらつらと書いておきます。
※ 手元にそれほど端末がなく、以下3機種での検証に留まってますので
参考程度の情報として捉えてください。

* SC02-B(GALAXY S) Android 2.2.1
* SC06-D(GALAXY S III) Android 4.1.2
* iPhone5S iOS 7.1.2

## そもそもHistory APIに未対応なAndroidがある
実機での検証はできていませんが、一部あるらしく、

* Android2.2以降は対応
* しかしAndroid 3.x, Android 4.0では未対応
* Android4.1以降は対応

ということになってるようです。
しかし、以下のソースだと4.1も未対応ということのようで、
http://caniuse.com/#search=history
実際のところどこまでが対応済みなのかは、はっきりとは分かってません。
※　端末固有の差分とかもありそうだし。

History APIの使用ができるか否かを判定し、処理を切り分けておくのが
無難ですね。

## history.stateが使えないブラウザがいる
history.pushStateはできるけど、history.stateは使用できない（undefinedになる)という。
Android2.2の標準ブラウザで気づいたので、以下の様なテストコードを用いて検証してみました。

```javascript:テストコード
if(history.pushState) {
    alert("history api can use");
} else {
    alert("history api can not use");
}
if(history.state === undefined) {
    alert("history.state can not use");
} else {
    alert("history.state can use");
}

```

結果は以下のとおり。

|端末|OSバージョン|ブラウザ|history.pushState|history.state|
|-----------------|---------|---|-----|-------|
|SC02-B|Android 2.2.1|Android標準|使用可|使用不可|
|SC06-D|Android 4.1.2|Android標準|使用可|使用不可|
|SC06-D|Android 4.1.2|Chrome(ver.40.0.2214.109)|使用可|使用可|
|iPhone5S|iOS 7.1.2|Safari|使用可|使用不可|
|iPhone5S|iOS 7.1.2|Chrome(ver.40.0.2214.69)|使用可|使用不可|

ブラウザ毎に、この辺りは差分が生じているようです。
http://stackoverflow.com/questions/10941553/pushstate-popstate-safari-5-not-returning-window-history-state

history.stateが使用できないと、片落ち感が否めないですね。。

## 画面ロード時にonpopstateが発火するブラウザがいる
onloadと同様のタイミングで発火する場合があることに気づき、以下の実機で再現確認しました。

|端末|OSバージョン|ブラウザ|
|-----------------|---------|---|
|SC06-D|Android 4.1.2|Chrome(ver.40.0.2214.109)|
|iPhone5S|iOS 7.1.2|Safari|
|iPhone5S|iOS 7.1.2|Chrome(ver.40.0.2214.69)|


この辺りの話ですかね。
http://lists.w3.org/Archives/Public/public-html-bugzilla/2010Oct/3102.html

初回画面ロード時はstateが設定されていないので、とりあえず以下の様な制御で
コールバック処理の実行有無は切替できそう。

```javascript:popstateコールバック制御の例
window.addEventListener('popstate',function(e){
    if (e.state) {
        ...コールバック処理を記述
    }
});
```

## pushStateしたURLがlocationオブジェクトに同期されないブラウザがいる

```javascript:テストコード
history.replaceState('hoge', null, '/hoge/fuga?hogehoge=fugafuga');
console.log(location.search); // ?hogehoge=fugafugaと出力されてほしい
```
これで、「?hogehoge=fugafuga」と取れないブラウザがあります。
location.searchだけではなくて、location.hrefとかもダメですね。
以下の実機で再現確認しています。

|端末|OSバージョン|ブラウザ|
|-----------------|---------|---|
|SC02-B|Android 2.2.1|Android標準|

どうもAndroidのバグっぽいんですよね、これ。
https://code.google.com/p/android/issues/detail?id=17471

対象を判定するには、pushStateする前のlocation情報(hrefとか）と
pushStateした後のlocation情報を突き合わせる、とかしか方法が思いつかず。
面倒ですね。

# まとめ
だらだら書いてしまいましたが、自分が実装した範囲では

```javascript:HistoryAPI使用判定例
history.pushState && history.state !== undefined;
```

これが真を返すブラウザは、History APIを使った制御をする、
という切り分けでいくことにしました。
locationオブジェクトが同期されない件は少々気になるところですが、
切り分け基準が明確にならないので、とりあえず様子見ということで。

今回は地道に実装しましたが、要件に応じて[jquery-pjax](https://github.com/defunkt/jquery-pjax)や[History.js](https://github.com/browserstate/history.js)のようなライブラリを導入した方が諸々楽できそうな気がしました。