のらねこの気まま暮らし

技術系だけど、Qiita向きではないポエムとかを書くめったに更新されないヤツ

Backboneとjquery.pjaxの相性は悪い

前置き

以前こんな記事を書いた。
http://mizuki-r.hatenablog.com/entry/2013/02/02/150356

Backboneでpjaxの実装書くより、jquery-pjax使ったほうが楽だし利便性いいよね!
なんかよくわかんないバグにハマったけど試行錯誤してたらうごいたよ!
っていう内容。


お ま え は 何 を 言 っ て い る ん だ

試行錯誤しているうちになんか動くようになった。誰か説明してくれ。

実 は 解 決 し て な い。
気のせいか、別のバグでpjaxしてるように見えてしてなかったとかそんなオチ。

試行錯誤云々で動くはずがない

というのは、ソースを見ればわかる。

jquery.pjax

https://github.com/defunkt/jquery-pjax

version 1.5.1から引用

function pjax(options) {
  options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options)

  // 中略

  if (xhr.readyState > 0) {
    if (options.push && !options.replace) {
      // Cache current container element before replacing it
      cachePush(pjax.state.id, context.clone().contents())

      window.history.pushState(null, "", stripPjaxParam(options.requestUrl))
    }

    fire('pjax:start', [xhr, options])
    fire('pjax:send', [xhr, options])
  }

  return pjax.xhr
}

と、pjaxメソッドの中にベタっと埋め込まれている。

backbone.js

http://backbonejs.org/

version 0.9.10から引用
長いので、Backbone.history.navigateの部分だけ抽出

    navigate: function(fragment, options) {
      if (!History.started) return false;
      if (!options || options === true) options = {trigger: options};
      fragment = this.getFragment(fragment || '');
      if (this.fragment === fragment) return;
      this.fragment = fragment;
      var url = this.root + fragment;

      // If pushState is available, we use it to set the fragment as a real URL.
      if (this._hasPushState) {
        this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);

      // If hash changes haven't been explicitly disabled, update the hash
      // fragment to store history.
      } else if (this._wantsHashChange) {
        this._updateHash(this.location, fragment, options.replace);
        if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
          // Opening and closing the iframe tricks IE7 and earlier to push a
          // history entry on hash-tag change.  When replace is true, we don't
          // want this.
          if(!options.replace) this.iframe.document.open().close();
          this._updateHash(this.iframe.location, fragment, options.replace);
        }

      // If you've told us that you explicitly don't want fallback hashchange-
      // based history, then `navigate` becomes a page refresh.
      } else {
        return this.location.assign(url);
      }
      if (options.trigger) this.loadUrl(fragment);
    },

BackboneのpushStateは、Backbone.Router.navigateを呼び出すことでpushされる。
navigateはpushStateしつつ、routesに登録したコールバックを実行するため、navigateを使わない、という選択肢は取れない。

Backbone.Router.navigateは、Backbone.history.navigateにエリアス貼ってるだけなのでそっちみればいい。

設定や一部上書き程度でどうにかなるレベルじゃない

どちらも大きなメソッドに埋め込まれているので、仮にどちらかのpushStateを無効にできたとして、それによる弊害の方が大きくなるため、試行錯誤でパラメータ変えたり上書きしたりする程度でこの不具合は潰せない。

Backbone.history.navigateの引数でどうにかできないか

navigateは(fragment, options)を取ってる。fragmentは今回の件に関係ないので省略。
optionsは以下のパターンを想定している

  • trigger: true|false
    • trigger(Router.routesに登録されているコールバック)を呼び出すか
  • replace: tule|false
    • pushStateではなくreplaceStateを使うか

第二引数にオブジェクト({})ではなく真偽値(true|false)を渡すことも可能だが、その場合、triggerのみがtrueになる。

本当に2回pushStateされているのか

Backbone.jsとjquery-pjaxを読み込んだページで、developer toolでhistoryのAPI叩いてみれば一目瞭然。

まとめ

最初の話

BackboneのRouterでtirgger呼び出しつつpjaxしたい。
Backboneは自前でpjax実装しないといけないのでちょっと面倒。
jquery-pjaxを使いたい。

間違った話

設定しだいでBackbone.historyとjquery-pjaxを併用できるよ!

本当の話

Backbone.historyとjquery-pjaxを並行で使おうとすると、pushStateが2回走るため、同じ履歴が2つ登録してしまうという不具合を引き起こす。

当日追記

silversさんがこの問題に対する解決策をあげてくれました。
silvers++

http://ofsilvers.hateblo.jp/entry/backbone-with-jquery-pjax