コレクションエージェント(1)

2015/09/02

前回は「Todo リスト」アプリケーションの Cape.JS を 1.1 から 1.2 にアップグレードしました。

今回からは、Cape.JS 1.2 で導入された新しいクラス CollectionAgent を使ってソースコードを書き換えていきます。アプリケーションの振る舞いは変化しませんが、ソースコードの記述量が相当に減ることをお見せしたいと思います。


初めに、app/assets/javascripts/application.js を次のように書き換えます。

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require capejs
//= require bootstrap
//= require lodash
//= require es6-promise
//= require fetch
//= require_tree .
//= require_self

Cape.defaultAgentAdapter = 'rails';

修正点は4つです。

  1. ディレクティブ //= require es6-promise を追加。
  2. ディレクティブ //= require fetch を追加。
  3. ディレクティブ //= require_self を追加。
  4. JavaScript コード Cape.defaultAgentAdapter = 'rails'; を追加。

最初の2つで Chrome と Firefox 以外のブラウザに Fetch API の機能を持たせています。ディレクティブ //= require_self は、この application.js ファイルの中に JavaScript コードを記述するために必要です。

最後の1行では、CollectionAgent のデフォルトアダプターを設定しています。ここでいう「アダプター」とは、API サーバーとの通信がうまく行くように調節をしてくれる JavaScript ライブラリのことです。

私たちの「Todo リスト」のように、Ruby on Rails で実装された API サーバーと通信したければ、CollectionAgent のインスタンスを作る前にデフォルトアダプターを設定する必要があります。

この設定を行うことにより、API サーバーに対して送られる HTTP リクエストの X-CSRF-Token ヘッダに適切な値がセットされるようになります。こうしないと GET/HEAD 以外のメソッドによるリクエストが拒絶されてしまいます。

現行バージョン(v1.2.0)の Cape.JS に付属するアダプターは 'rails' だけです。Rails 以外で実装された API サーバーを使いたい場合は、アダプターを自作する必要があります。


次に、API サーバーが「タスクのリスト」として返す JSON データの構造を変更します。

現在のところ app/views/api/tasks/index.jbuilder は次のように記述されています。

json.array! @tasks, :id, :title, :done

このコードから生成される JSON データは例えば次のようなものになります。

[
  { "id": 1, "title": "猫のえさを買う。", "done": true },
  { "id": 2, "title": "粗大ゴミを捨てる。", "done": false }
]

しかし、コレクションエージェントは次のような構造の JSON データを要求します。

{
  "tasks": [
    { "id": 1, "title": "猫のえさを買う。", "done": true },
    { "id": 2, "title": "粗大ゴミを捨てる。", "done": false }
  ]
}

JSON データ全体は配列ではなくオブジェクトである必要があります。そして、そのオブジェクトにコレクションエージェントの「リソース名」と一致するキーがあり、そのキーの値が配列でなければなりません。この例では "tasks" がリソース名です(詳しくは後述)。

そこで app/views/api/tasks/index.jbuilder を次のように書き直します。

json.tasks do
  json.array! @tasks, :id, :title, :done
end

正確に言えば、JSON データがオブジェクトで、そのキーがコレクションエージェントのリソース名である、というルールは規約に過ぎません。必要であれば、開発者は設定により変更できます。Cape.JS は Ruby on Rails の「設定より規約(Convention over Configuration)」というパラダイムを受け継いでいます。


続いて、コレクションエージェントのクラスを定義します。クラス名は TaskCollectionAgent としましょう。Rails のモデル名とは異なり、名前は自由に決めて構いません。

app/assets/javascripts ディレクトリに新規ファイル task_collection_agent.es6 を次のような内容で作成してください。

class TaskCollectionAgent extends Cape.CollectionAgent {
  constructor(client, options) {
    super(client, options);
    this.basePath = '/api/';
    this.resourceName = 'tasks';
  }
}

コレクションエージェントのクラスは Cape.CollectionAgent クラスを継承します。コンストラクタでは、いくつかのプロパティを設定します。basePath プロパティは Ajax リクエストの URL のベースとなる文字列です。デフォルト値は '/' です。私たちの「Todo リスト」アプリケーションでは、/api/ ディレクトリ以下のパスにアクセスしますので、このように設定します。

Rails のルーティング用語で言えば、basePath プロパティは名前空間(namespace)に相当します。

resourceName プロパティは前述の「リソース名」を表します。この値は、コレクションエージェントが Ajax リクエストの URL を生成する際に basePath プロパティの値と組み合わせて使われます。また、既に述べたように API サーバーから返ってきた JSON データから配列を取り出すためのキーとしても使われます。


TaskCollectionAgent クラスはまだコンストラクタを記述しただけですが、もうすでにタスクのリストをサーバーから取得する能力を有しています。

テキストエディタで app/assets/javascripts/todo_list.es6 を開いてください。現行の init() メソッドは次のように記述されています。

  init() {
    this.ds = new TaskStore();
    this.ds.attach(this);
    this.editingTask = null;
    this.ds.refresh();
  }

これを次のように書き換えてください。

  init() {
    this.agent = new TaskCollectionAgent(this);
    this.editingTask = null;
    this.agent.refresh();
  }

コレクションエージェントにはデータストアと異なり、attach() メソッドがありません。その代わり、コンストラクタの第1引数としてコレクションエージェントの“顧客(client)”となるコンポーネントを指定します。

また、データストアの場合は開発者が refresh() メソッドを実装する必要がありましたが、コレクションエージェントには既製の refresh() メソッドがあります。

app/assets/javascripts/task_store.es6 を見ると refresh() メソッドが次のように記述されています。

  refresh() {
    $.ajax({
      type: 'GET',
      url: '/api/tasks'
    }).done(data => {
      this.tasks = data;
      this.propagate();
    });
  }

TaskCollectionAgent クラスのインスタンスメソッド refresh() は、これとほぼ同等の機能を持ちます。ただし、タスクの配列は tasks プロパティではなく objects プロパティに格納されます。


app/assets/javascripts/todo_list.es6 の書き換えを続けます。次は render() メソッドです。現行の記述は次の通り:

  render(m) {
    m.ul(m => {
      this.ds.tasks.forEach(task => {
        m.li(m => this.renderTask(m, task));
      });
    });
    if (this.editingTask) this.renderUpdateForm(m);
    else this.renderCreateForm(m);
  }

これを次のように書き換えます。

  render(m) {
    m.ul(m => {
      this.agent.objects.forEach(task => {
        m.li(m => this.renderTask(m, task));
      });
    });
    // if (this.editingTask) this.renderUpdateForm(m);
    // else this.renderCreateForm(m);
  }

変更点は3箇所。3行目の this.ds.tasks.forEachthis.agent.objects.forEach に直します。そして、(エラーを回避するために)7行目と8行目をいったんコメントアウトします。

以上の変更により、とりあえずタスクのリストが表示されるようになります。

画面キャプチャ


タスクの「済み(done)」フラグをトグルする機能と、タスクの削除機能はまだ動きません。次回は、これらの機能に関連する部分を修正します。