かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

HISTORY API Safariでウィンドウ ロード時にpopStateイベントが呼ばれてしまう。

pushState, popStateを使ってURLの履歴を追加したり、ブラウザバックに対応したりする事ができるので画面遷移をせずにAjaxとかで画面を切り替えるサイトを作ることができます。

で、記事の概要をクリックされたら、Ajaxで記事内容を取ってきて全面モーダルで表示。その際にpushStateでURLを記事のURLに変更、モーダルが閉じられたら元のURLに戻すようなサイトを作成していました。
久しぶりにサイトの修正があり確認しに行った所、Safariでページがロードされた時にpopstateで登録してたイベントが実行されている現象に遭遇しました。

$(function() {
  var w = window,
      d = document;
  if(w.history && w.history.pushState) {
    $(d).on('click', '#push', function(evt) {
      console.log('>> pushstate');
      evt.preventDefault();
      var q = w.location.search.substring(1),
          res = {count: 0},
          url;
      q = q.split("&");
      for(var i=0, l=q.length; i<l; i+=1) {
        var parm = q[i].split('=');
        res[parm[0]] = parm[1];
      }
      url = '?count=' + (res['count'] - 0 + 1);
      history.pushState('', '', url);
    });
    
    $(w).on('popstate', function() {
      console.log('<< popstate');
      history.go(0);
      history.replaceState('back', '', w.location.pathname);
    });
  }
});

#pushの要素をクリックする度にurlのパラメーターの?count=の値が増えて、ブラウザバックするとパラメーターなしのURLになるっていうすごい意味の分からないコードです!

これをSafari (9.0.3)で開くと初回ロード時にpopstateイベントが呼びだされ延々画面がリロードされますwww
手持ちのChrome (48.0.2564.116)、Firefox (44.0.2)、Vivaldi (1.0.344.37) では初回ロード時にpopstateイベントが呼ばれている事はありませんでした。
SafariなのでiOSのブラウザでも同様の現象が発生しているかもしれません。

自分でpopStateしたものかどうかチェックする事で初回ロード時かどうか判別する。

popstateイベントで呼び出される関数の引数でpushStateした時のstateを取得する事ができるようです。
これを利用すると意図してpushStateされたものかどうか判断ができるので、初回ロード時は処理を行わないという事ができそうです。
先のコードを修正します。

// 略
    $(d).on('click', '#push', function(evt) {
      /* 中略 */
      // 第1引数で state を指定する
      history.pushState('count', '', url);
    });

    $(w).on('popstate', function(evt) {
      console.log('<< popstate', evt.originalEvent.state);
      // state がなければ何もしない
      if(!evt.originalEvent.state) {
        return;
      }
      history.go(0);
      history.replaceState('back', '', w.location.pathname);
    });
// 略

これで、Safariでウィンドウのロード時にpopstateの処理が実行されることは無くなりましたが、 stateが設定されていないと、上手く動作しないので自分で意図してpushState()replaceState()したか解るように第一引数にキチンとstateオブジェクトを指定しなければなりません。

たぶんデフォルトだとevt.originalEvent.statenullだと思うので、stateオブジェクトを指定せずに空文字列のままにしておき、null出ない時で判別することもできなくはないと思いますが、stateオブジェクトを指定しておくほうが幸せになれるんじゃないかなぁ―と思います。


[参考]

オブジェクト指向JavaScriptの原則

オブジェクト指向JavaScriptの原則