試して学ぶ Backbone.js入門5


Backbone.jsの最終回の今回は、Backbone.Router(以降、Router)について説明します。

はじめに

前回の「試して学ぶ Backbone.js入門4」
では実際に動くアプリケーションを作成しながらBackbone.Viewについて説明しました。
今回は本連載も最終回となり、Backbone.Router(以降、Router)について説明します。

今回のソースの全体はこちらで確認することができます。

Backbone.Router

Backbone.jsが提供するRouterコンポーネントは、クライアント側でのページ遷移のイベントとその後の処理(コールバック関数)をマッピングするルーティング機能を提供します。
ViewがDOMイベントの発生を監視して、対応するメソッドに関連付けるように、Rounterはrouteイベントを監視して、対応するメソッドに関連付けます。

Webアプリケーションはアプリケーションの状態をURLに表現することで、ブラウザの戻る操作に対応する、ブックマークに対応する、といったことができます。
これらのURL(permanent link)はこれまでhash fragments(#)を使うことで実現されていました(/memo/#id)。
しかし、History APIの登場によって、クライアント側でもhash fragmentsを使用せずに、”きれいな”URL(/memo/id)が使えるようになりました。

RouterはHistoryAPIをサポートしているブラウザでは”きれいな”URLを使うことができ、サポートしていないブラウザではhash fragmentsを使うという、どちらの状況にも対応したルーティング機能を提供してくれます。

それではこれからRouterの使い方について、前回のアプリケーションをRouterを使ってページ遷移可能となるようにアプリケーションを変更することで確認していきます。
今回のアプリケーションの画面は以下のようになります。

  • ホーム画面(Memoの一覧を表示するリスト画面)
    Backbone.Routerを使ったMemoアプリケーションのホーム画面
  • 新規追加画面(Memoの新規追加を行う)
    Backbone.Routerを使ったMemoアプリケーションの追加画面
  • 編集画面(選択したMemoの編集を行う)
    Backbone.Routerを使ったMemoアプリケーションの編集画面

メモ作成用のテキストエリアが幅を取るので、新規追加時、編集時のみ表示するようにし、一覧表示には表示しない、という変更をしています。
URLを確認すると分かりますが、以下のように新規追加画面を表示した状態と編集画面の状態を補助リソース用のURLになるようにしています。

  • /app -> ホーム画面(リスト表示)
  • /app/create -> 新規追加状態画面
  • /app/id/edit -> 編集状態画面

これにより戻るボタンを押した際にも適切な状態の画面に戻ることができます。

Routerの定義

それではRouterの定義方法を確認しましょう。Backbone.Router.extendsを用いて以下のように定義します。

var app = {};

//---

var AppRouter = Backbone.Router.extend({
    routes:{
        "":"home",
        "create":"add",
        ":id/edit":"edit"
    },
    initialize:function () {
        _.bindAll(this);
        this.collection = new MemoList();
        this.headerView = new HeaderView({el:$(".navbar")});
        this.editView = new EditView({el:$("#editForm"), collection:this.collection});
        this.listView = new ListView({el:$("#memoList"), collection:this.collection});
    },
    home:function(){
        this.editView.hideView();
    },
    add:function () {
        this.editView.model = new Memo(null, {collection:this.collection});
        this.editView.render();
    },
    edit:function (id) {
        this.editView.model = this.collection.get(id);
        if (this.editView.model) {
            this.editView.render();
        }
    }
});

app.router = new AppRouter();

Backbone.history.start();

今回はこのAppRouterがアプリケーションのエントリポイントとなります。
initializeでは必要なMemoListや各種Viewの生成をしています。

ルーティングの定義をするのは、routesプロパティの定義部分です。
これはViewのeventsプロパティと同様に、URLのhash fragments以下に当たる部分とそれに対応するメソッドのマッピングを定義します。
routes定義する際には、頭に”/”を付けないように注意してください。

Routerのルーティング機能を有効にするには、URLの変化を監視するために、Backbone.history.start()を呼び出す必要があります。
これはアプリケーションの開始時に1度だけ呼び出すだけです。

navigateメソッドによるURLの変更

Routerを導入したアプリケーションの変更点を見ていきましょう。変更点は以下の通りです。

  • ヘッダ部のCreateボタンを押下することでform部分を表示する
  • Memo内のeditリンクを押下した際もform部分を表示し、対象のMemoデータを反映する

この変更を実現するには、ボタンやリンクを押下した際に、URL内のhash framnents以下に当たる部分を変更し、Routerによるルーティングを行います。
このような場合、Routerのnavigateメソッドを利用するとURLを更新することができます。
実際にCreateボタンを押下した際のコードを見てみましょう。

var HeaderView = Backbone.View.extend({
    events:{
        "click .create":"onCreate"
    },
    initialize:function () {
        _.bindAll(this);
    },
    onCreate:function () {
        app.router.navigate("create", {trigger:true});
    }
});

上記はヘッダ部分のDOMに対応づけたHeaderViewのコードです。
ボタンが押下された際に、Routerのnagivateメソッドを呼び出しています。
第1引数はhash fragments以降に当たる部分の文字列、第2引数はoptionsハッシュです。
optionsハッシュでtriggerをtrueに指定すると、Routerはroute:createイベントを発生させ、対応するメソッドを呼び出します。
trigger:trueを指定しない場合、URLを更新し、HistoryにURLを
保存するだけの処理となりメソッドは呼ばれません。

var ItemView = Backbone.View.extend({
    tmpl:_.template($("#tmpl-itemview").html()),
    events:{
        "click .edit":"onEdit",
        "click .delete":"onDelete"
    },
    initialize:function () {
        _.bindAll(this);
        this.listenTo(this.model, "change", this.render);
        this.listenTo(this.model, "destroy", this.onDestroy);
    },
    onEdit:function () {
        app.router.navigate(this.model.get("_id") + "/edit", {trigger:true});
    },
    onDelete:function () {
        this.model.destroy();
    },
    onDestroy:function () {
        this.remove();
    },
    render:function () {
        this.$el.html(this.tmpl(this.model.toJSON()));
        return this;
    }
});

上記はItemViewです。onEditメソッドの所で、navigateメソッドを利用して遷移しています。

Routerのnavigateメソッドを利用せずに、HTML内にaタグのhref属性にURLを指定するようにしても同じような遷移は可能です。
しかし、後述するpushStateを有効にする、しないの切り替えなどを考えると今回のようにnavigateメソッドによる実装の方が変更の範囲を減らすことができます。

実際にHTMLを見てみましょう。

今回のHTMLの全体はこちらで確認することができます。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Backbone Router Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="../lib/css/bootstrap.css" rel="stylesheet">
    <style>
    body {
        padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */
    }
    </style>
    <link href="../lib/css/bootstrap-responsive.css" rel="stylesheet">
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="navbar-inner">
            <div class="container">
                <a href="/app" class="brand">Backbone Memo Application</a>
                <a class="create btn btn-primary pull-right">Create</a>
            </div>
        </div>
    </div>

    <div id="main" class="container">
        <h1>Memo</h1>

        <form id="editForm" class="form" style="display: none">
            <input type="text" name="title" class="input-block-level" placeholder="Title"/>
            <textarea rows="5" name="content" class="input-block-level" placeholder="Content"></textarea>
            <div class="btn-group">
              <a id="saveBtn" class="btn btn-primary">Save</a>
              <a id="cancelBtn" class="btn">Cancel</a>
            </div>
        </form>

        <div id="memoList" class="row-fluid">
        </div>
    </div>

    <script type="text/template" id="tmpl-itemview">
        <h3><%= title %></h3>
        <p><%= content %></p>
        <p><a class="edit">edit</a> <a class="delete">delete</a></p>
    </script>

    <script src="../lib/js/jquery-1.9.1.js"></script>
    <script src="../lib/js/json2.js"></script>
    <script src="../lib/js/underscore.js"></script>
    <script src="../lib/js/backbone.js"></script>
    <script src="router.js"></script>
</body>
</html>

Createボタンのaタグにhref属性を定義して、 href=”#create” と書くことで、navigateメソッドの呼び出しなしに新規追加画面を表示することができます。
しかし、これをhash fgagmentsを使わない”きれいな”URLを利用しようとすると変更が必要になります。
また、editリンクの部分はテンプレートなので、実際にはItemViewのコード内になりますが、ここでも”#”をURLに含めるか、含めないかを判断する必要が出てきます。

今回紹介したようにnavigateメソッドを使うことで、Backbone.history.startの呼び出し方を切り替えるだけで、hash fragmentsを使う場合、”きれいな”URLを使う場合どちらになってもその他のコードを変更する必要がありません。

pushStateを有効にする

HistoryAPIをサポートするブラウザではpushStateを使うことでき、hash fragments(#)を使わない”きれいな”URLを利用することできます。
実際にpushStateを有効にするには、先のBackbone.history.startの呼び出しを以下のように変更するだけです。

app.router = new AppRouter();

//Backbone.history.start();
Backbone.history.start({pushState: true, root:"/app/"});

startメソッドは引数にoptionsハッシュが指定でき、pushStateプロパティをtrueに指定するだけです。
Webアプリケーションをサーバ側のルートURL”/”に配置できない場合、Historyに対して、どこまでがrootのURLになるのか指定しないと判断できないため、そのような場合はrootプロパティを指定します。
今回の例では”/app/”の下にアプリケーションを配置しているので、このように指定しています。
hash fragmentsを利用する場合は”#”がクライアント側の遷移であることを示すため、このような指定が必要ありません。

pushStateを利用する際の注意点

アプリケーションを動作させてみると分かりますが、戻るボタンを押すと適切なアプリケーションの状態を示す画面が表示することができます。
hash fragmentsを利用する場合、pushStateを有効にする場合、どちらも同じように動作しますが、ブックマークをする際には注意が必要です。

ブックマークによってURLを保存した場合、新しいタブでブックマークを表示すると分かりますが、pushStateを有効にした”きれいな”URLを利用した場合にはサーバ側にリクエストが送信されれます。
これはブラウザがクライアント側でのルーティング用のURLであるということが分からないためです。
一般的にpushStateを使ったWebアプリケーションを作る場合、サーバ側では、指定されたURLがリクエストされた場合には、その画面状態を表示するために必要なコンテンツを返すように実装する必要があります。

一方で従来のhash fragmentsによるURLの場合、”#”以下は指定したURL内のドキュメント内を指すものであると解釈されるため、ブックマークした際の意図した状態を表示することができます。

開発するWebアプリケーションの種類によりますが、今回のような基本的に一度ページを表示したら、以降はクライアント側での画面切り替えを行い、必要なデータのみサーバから取得するようなタイプのアプリケーション(いわゆるSingle Page Application)の場合は、無理してpushStateを使わずにhash fragmentsを使ったURLを利用する方が簡単でよいと思います。

まとめ

全5回に渡ってBackbone.jsについて説明してきました。
Backbone.jsの基本となるModel, Collection, Events, View, Router を実際に動かしながら確認してきました。
JavaScriptによってWebアプリケーションを作ろうとすると、その自由さ故に、非常に複雑で見通しての悪いアプリケーションになってしまいがちです。
Backbone.jsを使うことで、アプリケーションにアーキテクチャという背骨を作り、一定のパターンで開発できることが本連載を通して感じて頂けたら幸いです。

著者: 村田賢一郎

Acroquest Technology 株式会社勤務。Javaによるミッションクリティカルな集中監視システムのフレームワーク開発、およびライフラインを支えるシステム開発に携わる。非同期処理、メッセージング、HAなどが本業である傍ら、Webによる新しいUI表現、開発手法に興味があり、あれこれ模索している。


<お知らせ>

2015年7月3日
iOS8.4がご利用いただけます(30分無料利用キャンペーン中)
2015年6月18日
キャリア各社のAndroid 5.0へのアップデート情報とバージョンアップの注意点
2015年6月10日
株式会社スクウェア・エニックス様のRemote TestKit導入事例を掲載しました
2015年6月3日
「Android M Developer Preview」インストール済みのNexus5,6,9端末をRemote TestKitで無料体験いただけます!
2015年6月2日
「Android M Developer Preview」のインストール方法解説
2015年4月27日
Remote testKitを使ってノマド実機テスト(AndroidStudioとRemote TestKit連携)
2015年3月26日
Remote TestKitでXcodeとの連携が可能になりました
> Remote TestKit Topページへ