Backbone.jsのpopstate時にルートページのcallbackが実行される
Backbone.jsとjQuery pjaxを併用してフロントエンド充してるところで、思わぬ罠にハマったので記録しとく。
問題
var MyRouter = Backbone.Router.extend({ "events" : { "" : 'home', "path/to" : 'fuga' }, "home" : function () { console.log('--route:home--'); }, "fuga" : function () { console.log('--route:fuga--'); } }); $(function () { var router = new MyRouter(); Backbone.history.start({"pushState" : true}); });
こういうコードが合った時、以下のような遷移を行うとする。
/ -> /huga/hoge -> /path/to -> (ブラウザバック)
そうすると、次のような関連性でcallbackが行われる。
遷移 | URL | console.log |
---|---|---|
/ | / | --route:home-- |
/huga/hoge | /huga/hoge | |
/path/to | /path/to | --route:fuga-- |
ブラウザバック | /huga/hoge | --route:home-- |
callbackを登録してない"/hoge/huga"のパスでhomeのコールバックが呼ばれてしまう。
原因
console.logとかでchrome developerToolを使ってデータの遷移を追ってみた。
backbone.jsでpopstateされてからコールバックが実行されるまでの流れ
- popstateのイベントに登録されているのは、Backbone.History.checkUrlメソッド
- Backbone.History.checkUrlの中で呼ばれるBackbone.History.loadUrlメソッド呼ばれる
- Backbone.loadUrlはBackbone.History.getFragmentからfragmentを受け取り・上書きする
- Backbone.loadUrlはgetFragmentで書き換えたfragmentを元にcallbackを選択・実行する
どうやらBackbone.History.loadUrlメソッドがfragmentを書き換えているらしい
1170: var fragment = this.fragment = this.getFragment(fragmentOverride);
this.fragmentにブラウザバックしたときのURLが入っている。
しかし、fragmentOverrideはundefinedが代入されており、それがthis.getFragmentに渡される。
getFragment内でfragmentOverrideが == null だった場合、fragmentを書き換える。
その結果、fragment === ''という判定がくだされ、""のrouteに紐づくhomeのコールバックが実行される。
fragment == null
undefined == null はtrueとして扱われるため、このよくわからない上書きが発生する。
もし、undefinedを許可したくないのであれば(fragment === null)と型を見ればいい。
Backbone.History.getFragmentはundefinedも許容したい。
上記判定を行うとすると、fragment = ''の時だけfragmentの取得が行われる。
しかし、実は""へのアクセス時のfragmentOverrideはundefinedが格納される。
じゃあこうするしか無いな。
if (fragment === null || fragment === undefined)
・・・・あれ? ソレ結局元にもどっただけじゃんか。
解決策
前回に引き続き@silver_sが解決策を提案してくれたよ!
提案というか、発見なんだけど。こうすればいいんじゃね?と。
http://stackoverflow.com/questions/6088073/default-routes-in-a-backbone-js-controller
routesに*{なまえ}のrouteを登録するとデフォルトでrouteを設定してくれる。
こいつを設定すると、homeのtrigger実行されなくなる、と。
"routes" : { "*path" : "defaultRoute" }
ためしてみた。
var MyRouter = Backbone.Router.extend({ "events" : { "*path" : 'defaultRoute', // <- 追加 "" : 'home', "path/to" : 'fuga' }, "defaultRoute" : function () { console.log('--route:default--'); }, "home" : function () { console.log('--route:home--'); }, "fuga" : function () { console.log('--route:fuga--'); } }); $(function () { var router = new MyRouter(); Backbone.history.start({"pushState" : true}); });
/ -> /huga/hoge -> /path/to -> (ブラウザバック)
遷移 | URL | console.log |
---|---|---|
/ | / | --route:home-- |
/huga/hoge | /huga/hoge | --route:default-- |
/path/to | /path/to | --route:fuga-- |
ブラウザバック | /huga/hoge | --route:default-- |
本当だ・・・!
なんでかってーと
pushStateがtrueの時、Backbone.History.loadUrlでfragmentからcallbackが該当するものを探しだすんスよ。
その結果がcheckUrlに渡されて、callbackがあるものはtrueを返すが、trueが無かったら今度はHashから探しにいくんすよ。
Hashでもcallbackが見つからなかったら、""が実行されるらしくて。
なんで、全部のrouteにcallbackが登録されていれば、そいつ(defaultRoute)が実行される。
なんてわかりにくい・・・!