yutaponのブログ

javascript界隈の興味あるネタを備忘録的に残しておく場所

Backbone.jsに入門してみる【Router編】

久しぶりのBackbone.js入門。
Backbone.jsガイドブックを見ながらまとめてますが、
後半は試行錯誤のたまもの。

Backbone.Routerとは

Backbone.RouterはサーバーサイドMVCフレームワークでいうところのCにあたる部分、でもありますし、VCでもあります。
Backbone.ViewがDOMを監視するのに対して、Backbone.Routerはブラウザのハッシュ(URL欄)を監視します(厳密に言うとhashChangeイベントとpopStateイベントを監視)。
Backbone.Routerにハッシュとそれに対応する操作を設定しておくことで、
アプリケーション全体のコントローラみたいなふるまいをします。

Backbone.Routerの使い方

何はともあれサンプルコードを。

// @file router_sample.js

var Router = Backbone.Router.extend({
    routes : {  // ハッシュフラグメントと対応するメソッド名の組を設定する
        ''           : 'index',
        'mypage'     : 'mypage',
        'blog(/:id)' : 'blog'
    },

    index : function index() {
        // ハッシュなしでアクセスされたときの処理を書く
        console.log('index', arguments);
    },

    mypage : function mypage() {
        // #mypageでアクセスされたときの処理を書く
        console.log('mypage', arguments);
    },

    blog : function blog(id) {
        // #blog(または#blog/123など)でアクセスされたときの処理を書く
        console.log('blog', arguments);
    }
});

var router = new Router();

// DOMの生成が完了してからstart()させる
$(function () {
    Backbone.history.start();  // ブラウザのhashChangeの監視を開始する
});

これが基本形です。
まずはroutersにハッシュと対応するコールバックの組をオブジェクトで渡します。
ブラウザのURLがhoge.com#mypageに変化すると、mypageプロパティに設定されている
コールバックが実行されるといった具合です。

もしくはこんな形でも同様の処理になります。

var Router = Backbone.Router.extend({
    routes : {  // ハッシュフラグメントと対応するコールバックの組を設定する
        ''           : 'index',
        'mypage'     : 'mypage',
        'blog(/:id)' : 'blog'
    }
});

var router = new Router();

// routeイベントを購読してコールバックを設定する
router.on('route:index', function () {
    console.log('index', arguments);
});

router.on('route:mypage', function () {
    console.log('mypage', arguments);
});

router.on('route:blog', function (id) {
    console.log('blog', arguments);
});

$(function () {
    Backbone.history.start();
});

Router側の設定はこれでいいとして、View側でページ遷移させるときには
Backbone.history.navigate()または、Backbone.Routerのインスタンスのnavigate()を使います。

mypageからblogに遷移したいときは、

Backbone.history.navigate('blog/10', true);

という感じです。
第二引数にtrueを渡すと遷移が実行されますが、falseを渡すとハッシュ(URL)だけが変わります。

Backbone.history.start()のオプションについて

pushState: true

Backbone.history.start()にオプションとして { pushState: true } を渡すと
ブラウザのハッシュを #〜〜 が付かないURLを使えるようになります。
そもそもpushState/popStateというのはAjaxで画面遷移してもブラウザに履歴として残すことができるようにするAPIのことです。
HTML5から入ったAPIなので、古いブラウザとかIEとかで動作しないそうです(スマホ向けなら大丈夫)。

root : '/hoge/'

rootオプションはアプリケーションのルートがドメイン直下ではない場合に指定します。
たとえば example.com/hoge というURLでアプリケーションを開始したい場合は

Backbone.history.start({ pushState: true, root: '/hoge/' });

といった感じで指定します。


Backbone.Routerの使い方 応用編

これまで紹介した書き方はネット上に散々書かれていることなので、もう少し突っ込んでみます。

基本形で紹介したコード、新しいページを作りたくなったら修正箇所が多くてメンテナンス性が低いですよね。
ということでまずはルーティングの設定を別ファイルとして切り出しましょう。
また、別ファイルを読み込むためにRequireJSも使ってみましょう。

ここからは前に書いた記事で使っているディレクトリ構成を例に進めます。
RequireJSの導入から使い方(Bowerにも触れてみる) - yutaponのブログ

public/javascripts/以下にいくつかのファイルを追加して、ディレクトリ構成はこんな感じにしました。

$ tree -L 3
.
├── app.js
├── conf
│   └── routes.js
├── core
│   └── router.js
└── module
    ├── blog
    │   └── index.js
    │   ├── model
    │   └── view
    ├── index
    │   ├── index.js
    │   ├── model
    │   └── view
    └── mypage
        └── index.js
        ├── model
        └── view



まずは設定ファイル。適当にこんな感じで作ってみた。
このファイルは conf/routes.jsに作成しました。

// @file routes.js

define([
    'module/index/index',
    'module/mypage/index',
    'module/blog/index'
], function () {
    return {
        index : {
            url : '',
            controller : require('module/index/index')
        },
        mypage : {
            url : 'mypage',
            controller : require('module/mypage/index')
        },
        blog : {
            url : 'blog(/:id)',
            controller : require('module/blog/index')
        }
    };
});



Backbone.Routerの初期化は core/router.js にて設定することにしました。

// @file router.js

define(['../conf/routes'], function (confRoutes) {
    'use strict';

    var Router = Backbone.Router.extend({

        routes : function routes() {
            var _routes = {};

            _.each(confRoutes, function (val, key) {
                var url = val.url;
                var controller = val.controller;

                _routes[url] = key;

                this._setRouteCallback(key, controller);
            }, this);

            // console.log('_routes', _routes);
            //  => {"": "index", mypage: "mypage", blog(/:id): "blog"}

            return _routes;
        },

        _setRouteCallback : function _setRouteCallback(moduleName, Controller) {
            this.on('route:' + moduleName, function () {
                // TODO: すでにインスタンス生成されていたら生成しないようにする
                var controller = new Controller();

                // コントローラにはshow()メソッドが実装されていること
                controller.show && controller.show.apply(controller, arguments);
            });
        }
    });

    return Router;
});



アプリケーションの起動は app.js にて行います。

// @file app.js

requirejs.config({
    baseUrl : '/javascripts',  // モジュール読み込みのbaseUrlを指定する

    paths : {
        jquery : [
            '//code.jquery.com/jquery-2.1.0.min',      // 先に読み込される
            '/bower_components/jquery/dist/jquery.min' // 失敗したら次に読み込みされる
        ],

        underscore : [
            '//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore-min',
            '/bower_components/underscore/underscore'
        ],

        // '/'または'http'から始まると絶対パスで参照する
        backbone : '/bower_components/backbone/backbone'
    },

    shim : {
        underscore : {
            exports : '_'
        },
        // Backbone.js ver1.1.1 からAMDに対応したので下記の設定はいらなくなった
        // backbone : {
        //     deps : [underscore', 'jquery'],
        //     exports : 'Backbone'
        // }
    }
});


define([
    'backbone',
    'core/router'
], function (Backbone, Router) {

    var router = new Router();

    Backbone.history.start();  // hashChangeの監視を開始
});

最初はハッシュが '' (空文字) なので module/index/index.js に処理が移ります。
module/MODULE_NAME/index.js はモジュールのコントローラViewとしてふるまうことを期待しています。

このコントローラViewで、ModelやCollectionの生成と、それに対応するViewを生成することにします。

module/index/index.js はこんな感じになってます。

// @file index.js <module/index>

define([
    'backbone'
], function (Backbone) {
    'use strict';

    var IndexView = Backbone.View.extend({
        render : function render() {
            this.$el.html('Index View');
        },

        /**
         * hashChangeで呼ばれる
         */
        show : function show() {
            this.render();

            $('body').html(this.$el);
        }
    });

    return IndexView;
});

module/mypage/index.js, module/blog/index.js も現状はほぼ同様のコードです。

こういう感じで組んでいけばいいんじゃないかなっていうコードを載せてみました。
ModelやViewの生成は書いてませんが、各モジュールのコントローラが取りまとめて
インスタンスの生成をすれば管理しやすいと思います。

というか、素のBackbone.jsだとここからスケールさせていくと管理が難しくなっていくので何かのフレームワークに乗っかるってのも一案。

おわりに

サンプルアプリを試行錯誤しながら作ってみたけど、これでいいのかよくわからん。
運用したことのないコードなので、後半は参考程度に。