コレクションエージェント(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つです。
- ディレクティブ
//= require es6-promise
を追加。 - ディレクティブ
//= require fetch
を追加。 - ディレクティブ
//= require_self
を追加。 - 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.forEach
を this.agent.objects.forEach
に直します。そして、(エラーを回避するために)7行目と8行目をいったんコメントアウトします。
以上の変更により、とりあえずタスクのリストが表示されるようになります。
タスクの「済み(done)」フラグをトグルする機能と、タスクの削除機能はまだ動きません。次回は、これらの機能に関連する部分を修正します。