Backbone.js と比較しながら Knockout.js を試してみた

Backbone.js を試したから Knockout.js にも挑戦

JavaScript のクライアント MVC フレームワークは Backbone.js でいこうと思っていたんですが、Knockout.js が Ver 2.0 でかなり機能追加されて、ちょっと心変わり。もともと Knockout.js のデータバインディング機能に興味を持っていたところに、テンプレート機能が追加されて、試してみたくなりました。

念のため説明すると、Knockout.js は、.NET 開発者にはお馴染みの、データバインディングや MVVM パターンが特徴のフレームワークです。

ちなみに、Backbone.js の記事はこちら。

ToDo リストのサンプルを作ってみた

Backbone.js で作った ToDo リストのサンプルと、同等のものを Knockout.js でも作ってみました。

<!DOCTYPE html>
<html>
    <head>
        <title>Knockout Todo</title>
        <meta charset="utf-8">
    </head>
    <body>
        <h1>Knockout Todo</h1>

        <!--
        タスクリストを表示する要素。
        子要素がタスク1個を描画するためのテンプレートになる。
        -->
        <div data-bind="foreach: tasks">
            <div class="task">
                <!--通常表示するビュー。-->
                <div data-bind="visible: !editing()">
                    <input type="checkbox" data-bind="checked: completed" />

                    <!--完了していないときだけ表示する。-->
                    <span data-bind="if: !completed()"><span data-bind="text: name"></span></span>
                    
                    <!--完了時は打ち消し線を引く。完了していないときは要素を非表示。-->
                    <del data-bind="if: completed()"><span data-bind="text: name"></span></del>

                    <!--click イベントと TaskViewModel#toggleEdit メソッドをバインドする。-->
                    <a href="javascript:void(0);" data-bind="event: { click: toggleEdit }">[edit]</a>
                </div>

                <!--編集時に表示するビュー-->
                <div data-bind="visible: editing()">
                    <input type="text" data-bind="value: name"/>
                    <input type="button" data-bind="event: { click: toggleEdit }" value="編集終了"/>
                    <input type="button" data-bind="event: { click: destroy }" value="削除"/>
                </div>
            </div>
        </div>

        <!--form の submit イベントと、appViewModel の addTask メソッドをバインドする-->
        <form data-bind="event: { submit: addTask }">
            <!--keyup が発生するたびに、appViewModel.newTaskName の値を更新する-->
            <input type="text" data-bind="value: newTaskName, valueUpdate: 'keyup'" />

            <!--appViewModel.newTaskName が空のときはボタンを無効にする-->
            <input type="submit" data-bind="enable: newTaskName().length > 0" value="登録"/>
        </form>

        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
        <script type="text/javascript" src="https://github.com/downloads/SteveSanderson/knockout/knockout-2.0.0.js"></script>
        <script type="text/javascript">
            (function() {

             // タスク
             var TaskViewModel = function(name) {
                // タスク名
                this.name = ko.observable(name);

                // 完了かどうか
                this.completed = ko.observable(false);

                // 編集中かどうか
                this.editing = ko.observable(false);

                // 編集ビューの切り替え
                this.toggleEdit = function() {
                    this.editing(!this.editing());
                };

                // 削除要求コールバック
                this.requestRemove = function(task) {
                    // 何もしない
                };

                // 削除ボタンのイベントハンドラ
                this.destroy = function() {
                    if (confirm("削除していいですか?")) {
                        this.requestRemove(this);
                    }
                };
            };

            // アプリケーション
            var appViewModel = {
                // 新しいタスクの名前
                newTaskName: ko.observable(""),

                // 登録されたタスク
                tasks: ko.observableArray(),

                // 登録ボタンのハンドラ
                addTask: function() {
                    var taskName = this.newTaskName();
                    var newTask = new TaskViewModel(taskName);

                    // タスクを削除するときのコールバックを設定
                    var self = this;
                    newTask.requestRemove = function(task) {
                        self.tasks.remove(task);
                    };

                    this.tasks.push(newTask);
                    this.newTaskName("");
                }
            };

            // ビューにバインド
            ko.applyBindings(appViewModel);
            
            }());
        </script>
    </body>
</html>

そのままコピペで動くと思います。Backbone.js のときと比べて、コード量が激減。

データバインディング超便利

ビューモデルを更新したら自動でビューも更新されます。バインドは双方向なので、ビューに表示しているデータを input や textarea で変更すると、ビューモデルに反映されます。Backbone.js ではわざわざ change イベントを捕まえて更新するコードを書いていたというのに…。データバインディングJavaScript で見事に実装していてスゴイ。

ビューのデザインがしやすい

ビューモデルとビューのバインディングは data-bind 属性で指定します。foreach によるコレクションの走査も data-bind。Backbone.js みたいに script タグを使って記述する必要なし。Web ブラウザでビューだけ表示すると、それっぽく表示されるので、デザインしやすいです。

Backbone.js のルーターに相当するものが無い

Backbone.js にはルーティング機能があったので、URL とビューをマッピングでき、ブラウザの履歴にも対応できました。しかし Knockout.js には相当するものがありません。ブラウザの履歴には自力で対応するしかなさそう。

Knockout.js にはサーバーと通信する機能がない

Backbone.js だと Model クラスの fetch や save でサーバーと通信できたのに対し、Knockout.js では jQuery.ajax などを使うことになります。割り切っていて、潔いと思えなくもないです。

まとめ

Backbone.js と Knockout.js 両方使ってみましたが、今回みたいに1ページで終わるような小さいアプリケーションなら、Backbone.js よりも Knockout.js の方が生産性高いです。コードの記述量も少なくて済みます。

逆に、複数のビューを切り替える必要があるなら、今のところ手段を提供している Backbone.js の方がいいです。ただ、今後 Knockout.js.js で良い方法が提案されるかもしれません。