Backbone.Router のルートの初期化および navigate() の動作のコードを見てみる

Backbone.Router のコードでルートの初期化部分と navigate() による動作が気になったのでコード上で確認してみる.

Backbone.Router の使い方は下記の様な感じ.

var AppRouter = Backbone.Router.extend({
  initialize: function() {
    _.bindAll(this, 'fuga', 'fuga2'); // ここでは意味ないけど
  },
  routes: {
    'hoge': 'fuga',
    'hoge2/:param': 'fuga2'
  },
  fuga: function() {
    console.log('fuga');
  },
  fuga2: function(param) {
    console.log('fuga2 ' + param);
  }
});

見てたコードは Backbone.js 1.1.2

流れ概要

  • コンストラクタ
    1. routes に格納してあるオブジェクトを取得
    2. routes のキーを正規表現にしてハンドラと対応させて連想配列で格納
  • navigate()
    1. ハンドラを登録した順に確認していって最初に一致したもののハンドラを実行する

コード追跡

コンストラクタで routes の中身を取ってきて _bindRoutes() でバインドしてる.

1217   var Router = Backbone.Router = function(options) {
1218     options || (options = {});
1219     if (options.routes) this.routes = options.routes;
1220     this._bindRoutes();
1221     this.initialize.apply(this, arguments);
1222   };

_bindRoutes では routes の後ろから順に route() を呼び出す.

1355     // Bind all defined routes to `Backbone.history`. We have to reverse the
1356     // order of the routes here to support behavior where the most general
1357     // routes can be defined at the bottom of the route map.
1277     _bindRoutes: function() {
1278       if (!this.routes) return;
1279       this.routes = _.result(this, 'routes');
1280       var route, routes = _.keys(this.routes);
1281       while ((route = routes.pop()) != null) {
1282         this.route(route, this.routes[route]);
1283       }
1284     },

route() では events のキーになっていた部分を RegExp にしておく. (後述するが navigate() のタイミングで正規表現として評価される.) fragment には pushState が有効か無効かで変わるが true の場合は多分ルート以下のパスが入る. route() の呼び出しは routes の逆順で Backbone.histroy.route() では配列に unshift() で突っ込んでるので routes の前にかいてあった順が優先順位になる?

1244     route: function(route, name, callback) {
1245       if (!_.isRegExp(route)) route = this._routeToRegExp(route);
1246       if (_.isFunction(name)) {
1247         callback = name;
1248         name = '';
1249       }
1250       if (!callback) callback = this[name];
1251       var router = this;
1252       Backbone.history.route(route, function(fragment) {
1253         var args = router._extractParameters(route, fragment);
1254         router.execute(callback, args);
1255         router.trigger.apply(router, ['route:' + name].concat(args));
1256         router.trigger('route', name, args);
1257         Backbone.history.trigger('route', router, name, args);
1258       });
1259       return this;
1260     },

多分下記のところで名前付きパラメータは正規表現のグループにされる.

1288     _routeToRegExp: function(route) {
1289       route = route.replace(escapeRegExp, '\\$&')
1290                    .replace(optionalParam, '(?:$1)?')
1291                    .replace(namedParam, function(match, optional) {
1292                      return optional ? match : '([^/?]+)';
1293                    })
1294                    .replace(splatParam, '([^?]*?)');
1295       return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
1296     },

trigger が true ならば loadUrl() がよばれる

1494     navigate: function(fragment, options) {
1530       if (options.trigger) return this.loadUrl(fragment);

loadUrl() では handlers に格納されているルートのうち最初に javascript の文字列における test が true を返すハンドラに引き数を与えて呼び出す.

1477     loadUrl: function(fragment) {
1478       fragment = this.fragment = this.getFragment(fragment);
1479       return _.any(this.handlers, function(handler) {
1480         if (handler.route.test(fragment)) {
1481           handler.callback(fragment);
1482           return true;
1483         }
1484       });
1485     },

getFragment() では pushState が有効か無効かでパスを返すかハッシュを返すかを変える.

1370     getFragment: function(fragment, forcePushState) {
1371       if (fragment == null) {
1372         if (this._hasPushState || !this._wantsHashChange || forcePushState) {
1373           fragment = decodeURI(this.location.pathname + this.location.search);
1374           var root = this.root.replace(trailingSlash, '');
1375           if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
1376         } else {
1377           fragment = this.getHash();
1378         }
1379       }
1380       return fragment.replace(routeStripper, '');
1381     },