Chaplinでのroutes登録のイベント取り回し関係をまとめました


Chaplinjsで渡されるroutesについて、イベントのpub/subの挙動を真面目に読んでいなかったところを読んだのでまとめました。

端的にいうと、イベント(route:match)をpublishするための登録フロー(Router -> Route -> Backbone.history)とsubscribeするための登録フロー(Dispatcher -> Route)があり、監視されているURLの変更(popstateやhashchangeなど)でイベントが発火 -> Controllerのロードとactionの実行、になっています。

Application

Applicationは内部で下記のinitを実行します。
* initDispatcher
* initLayout
* initMediator
* initRouter

publishのフロー

initRouter

initRouterの中ではApplicationに渡されたroutesoptionsを受け取ります

@router = new Router options

として、routerを初期化し、Router#matchというメソッドを受け、渡されたroutesを実行します。

routesは下記のようにpatterntarget(に加え必要であればoptions)を受け、URLの変更に対して適切なControllerのactionを対応付けます。

initRouterの中では

routes? @router.match

が実行され、それぞれのmatch pattern, targetが実行されます。

matchでは、

route = new Route pattern, controller, action, options

として、routeオブジェクトが作成され、その後

Backbone.history.handlers.push {route, callback: route.handler}

としてhistory handlerに登録されます。
最後にrouteが返却されますが、routesの中では特になにも処理されていません。

routesの中で記述したルーティングが処理される機会は、このinitRouterだけになります。

Backbone.history

では、このルーティングはどのように制御されているかというと、すべてはBackbone.historyの中だけで処理されます。

Backbone.historyは、startメソッドが実行されると、hashChangeだけなのか、pushStateを見るのか、などのフラグ判別の処理の末、

      if (this._hasPushState) {
        addEventListener('popstate', this.checkUrl, false);
      } else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
        addEventListener('hashchange', this.checkUrl, false);
      } else if (this._wantsHashChange) {
        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
      }

というように、

  1. popstate
  2. hashchange
  3. setInterval の3つの方法でURL変化の監視が開始されます。

URLの変化に対してhistory.checkUrlが実行されます。
この中で、setIntervalに対応するために実際にURLが変化しているかどうかのチェックが行われ、通過すると、history.loadUrlが実行されます。

Backbone.history.loadUrl

ここでようやく、Backbone.history.handlersに登録しておいたrouteが評価されます。

      return _.any(this.handlers, function(handler) {
        if (handler.route.test(fragment)) {
          handler.callback(fragment);
          return true;
        }
      });

handlersの中から、現在のfragment(ざっくりURL)にマッチするものが存在するかがRoute#testでチェックされ、マッチしたcallbackが実行される、という挙動になります。

ここでのcallbackは、前述のとおり、Chaplin.RouterBackbone.history.handlers.push {route, callback: route.handler}として登録されています。

Chaplin.Route#handlerでのイベントpublish

さて、Route#handlerはどうなっているでしょうか?

例えば /users/33/posts/123?fields=comments のような fragment が渡されると、query も含めてパースされ最終的には

routes = {path, @action, @controller, @name, query}

というオブジェクトをつけて

    @publishEvent 'router:match', route, actionParams, options

として、イベントが発行されます。

subscribeのフロー

Chaplin.Dispatcher

Chaplin.Dispatcherは初期化時にroute:matchイベントを@dispatchで待ち受けます。

もろもろのチェックが通ると、Dispatcher#loadController(name, handler)が発火します。
loadControllerでは、ついに動的なcontrollerのロードが行われます。
Applicationの初期化時にoptionsで渡したcontrollerSuffixcontrollerPathが加味され、controllerのファイルパスが作成されます。

ここでのhandlerは

(Controller) =>
      @controllerLoaded route, params, options, Controller

となっており、ロードされたコントローラに対して、ルーティングの結果得られたパラメタなどが渡されます。

loadControllerの中では、作成されたファイルパスに対して、define.amdまたはrequireでcontrollerモジュールが呼び出されhandler(というかcontrollerLoaded)が実行されます。

Dispatcher#controllerLoadedとDispatcher#executeAction

その後は、controllerLoadedでControllerのインスタンスが作成され、executeActionに引き渡され、その中でactionが実行される事になります。

そこで、ようやく各種のControllerで定義したメソッドのところに届くことになります

class SomeController extends Chaplin.Controller
  someAction: (params, routes, options)->

まとめ

長くなりましたが、順序としては

  1. Applicationの初期化
  2. subscribe処理
    1. route:matchのsubscribe
    2. イベント受け取り時のroute, params, optionsを元に、動的にcontrollerのロード/インスタンス化/Controller#Actionの実行
  3. publish処理
    1. Application#initRouter
    2. routes(Router#match)により、Backbone.history.handlersへの追加
    3. Backbone.history.startによるURL変化の監視
    4. handlers内のmatchするhandlerのcallbackの実行
    5. Route#handlerでのroute:matchイベントのpublish

となっていることを読み解きました。