Archive for the ‘Spine.js’ Category:

CakePHP + Spine.jsで作るJavaScript Web Application

Wednesday, March 28th, 2012

前回の記事「CakePHP+Backbone.jsで作成したJavaScript Web Application」の内容を、比較のためにSpine.jsでも試してみました。
CakePHP側は何も変更していないので、Spine.js側だけを説明します。それから、Spine.jsはCofeeScriptにも対応していますが、ここではJavaScriptで作成しています。ソースコードは前回のソースに追加する形でGitHubで公開しています。

1.Spine.jsでのModelの定義

Spine.jsはControllerやViewも持っていますが、今回はModelだけを使っています。

まず、nameとaddressを属性として持つCustomerモデルを以下のようにして作成します。

var Customer = Spine.Model.sub();
Customer.configure('Customer', 'name', 'address');

CoffeeScriptですと「class Customer extends Spine.Model」ですので、これでModelクラスを継承して新しいモデルクラスを作成しているということになります。ですが、Spine.jsのややこしいのは、このCustomerクラスには、複数のCustomerが含まれるコレクションとしての機能も持っているということです。

Backbone.jsでは、モデルとコレクションのクラスを定義し、そのインスタンスを生成するという、オブジェクト指向らしい手順を踏んでいました。ですが、Spine.jsでは、Customerクラスがコレクションで、Customerクラスのインスタンスが個々のCustomerを表しています。困ったのは、この後、このCustomerをクラスと呼ぶべきか、それともコレクションと呼ぶべきかです。迷ったのですが、ひとまずはクラスと呼ぶこととします。ですが、Customerクラスは、コレクションであり複数のCustomerインスタンスを持っていることは気にとめておいてください。

次に、Ajaxによる永続化の設定をします。

Customer.extend(Spine.Model.Ajax);
Customer.extend({
    url : '/cakephp_service/customers'
});

Ajaxによる永続化を利用することと、Ajaxリクエストを送信するURLを指定します。
次に、Backbone.jsでも同じことをしましたが、CakePHPからのJSONをパースできるように、特別なパース処理を定義します。

Customer.extend({
    fromJSON : function(objects) {
        var value, _i, _len, _results;
        if (!objects) {
            return;
        }
        if (typeof objects === 'string') {
            objects = JSON.parse(objects);
        }
        if (Spine.isArray(objects)) {
            _results = [];
            for (_i = 0, _len = objects.length; _i < _len; _i++) {
                if (objects[_i].Customer != undefined) {
                    value = objects[_i].Customer;
                } else {
                    value = objects[_i];
                }
                _results.push(new this(value));
            }
            return _results;
        } else {
            if (objects.Customer != undefined) {
                return new this(objects.Customer);
            }
            return new this(objects);
        }
    }
});

ちょっと長いですが、元々のソースであるSpine.Model.fromJSON関数から、「.Customer」で取得する処理を数行追加しただけです。

2. イベント

当然ながらSpine.jsもAjaxの永続化は非同期で行われますが、Backbone.jsのようにfetch()にコールバック関数を指定できない仕様のようです。代わりに、イベントを使う必要があります。(ちなみに、Backbone.jsにも同じような機能はありますが、前回の記事では使っていません)

Spine.jsのイベントとしては、以下のようなものがあります。

イベント名 発生するタイミング
save 保存(生成(create)もしくは更新(update))が発生した場合
update 更新が発生した場合
create 生成が発生した場合
destroy 削除が発生した場合
change 生成(create)/更新(update)/削除(destroy)のどれかが発生した場合
refresh すべてのデータが無効化され置き換わった場合
error バリデーションに失敗した場合

ひとまず、表示を更新できるようにするためrefreshイベントにCustomerの一覧を表示するように、コールバック関数を設定します。

Customer.bind("refresh", function() {
    var tr = $('#list-table>tbody>tr');
    if (tr.length > 0) {
        tr.remove();
    }
    Customer.each(function(customer) {
        var tbody = $('#list-table>tbody');
        tbody.append('<tr><td>' + customer.id + '</td><td>' + customer.name
                + '</td><td>' + customer.address + '</td></tr>');
    });
});

3. アクションの定義

3.1 Customerの追加(addアクション)

以下が、Spine.jsで新しいCustomerを一件追加する処理です。

$(function() {
    $('#add-button').click(function(e) {
        Customer.create({
            name : $('#add-name').val(),
            address : $('#add-address').val()
        });
    });
});

Backbone.jsと異なるのは、Customerクラス(というかコレクション)のcreateを呼び出すだけで、Customerインスタンスの生成、Cusomterクラスへの追加、そして、Ajaxによる永続化まで行われているということです。Backbone.jsではsave()を呼び出すまで永続化が行われませんが、Spine.jsではcreateを呼び出すだけで直ぐに行われます。当然ながら、新しいCustomerを追加するには常にcreateを呼び出すしかありません。なので、インスタンスは生成するが、永続化はしないということができません。

3.2 Customerの一覧の取得(indexアクション)

Customerの一覧の取得は以下の通りとなります。すでにrefreshイベントに画面の更新処理を入れているので、結果を受け取った後にrefreshイベントが発生し、画面が更新されます。

$(function() {
    $('#list-button').click(function(e) {
        Customer.fetch();
    });
});

3.3 指定したIDのCustomerの取得(viewアクション)

指定したIDのCustomerの取得は、find(id)を使い、以下の通りとなります。ここで初めてCustomerのインスタンスが出てきます。

$(function() {
    $('#update-read_button').click(function(e) {
        var customer = Customer.find($('#update-read_id').val());
        $('#update-name').val(customer.name);
        $('#update-address').val(customer.address);
    });
});

3.4 Customerの属性の更新(editアクション)

属性を変更し、save()を呼び出せば、更新が行われます。

$(function() {
    $('#update-update_button').click(function(e) {
        var customer = Customer.find($('#update-read_id').val());
        customer.name = $('#update-name').val();
        customer.address = $('#update-address').val();
        customer.save();
    });
});

3.5 Customerの削除(deleteアクション)

Customerクラスのdestroy(id)を呼び出せば、指定したIDのCustomerが削除されます。また、Customerインスタンスにdestroy()があるのでそちらも使えます。

$(function() {
    $('#delete-button').click(function(e) {
        Customer.destroy($('#delete-id').val());
    });
});

4. まとめ

おそらく、Spine.jsの思想としては、JavaScript上のモデルは常にサーバと同期され、同時にViewとも同期されるべきと考えているようです。そのためか、モデルのcreateを呼び出すだけで、すぐにAjaxリクエストが送られるようになっています。fetchにコールバックが指定できず、イベントでしか更新できないのも、ViewとModelが常に同期されているようにするためと考えられます。さらに、モデルクラスがコレクションになっているのも、モデルクラスが一つ存在し、それがクライアント側でのモデルのリポジトリのように動作し、サーバおよびViewと同期するようにさせたいためではないかと思われます。

対して、Backbone.jsは、サーバとの同期タイミングは開発者が制御できますし、fetchメソッドにコールバック関数を指定できViewが更新できるタイミングを制御できます。Spine.jsの考えが常にうまくいけば、こちらの方が楽そうですが、うまくいかなった場合の懸念があります。

Spine.jsで納得できないのが、モデルクラスがコレクションにもなっているという設計です。なんともわかりずらいとしか言いようがありません

ということで、私は、現時点ではBackbone.jsがおすすめです。