データストア

2015/06/22

前回は、Cape.JS コンポーネントの中から jQuery の ajax メソッドを呼んでデータベースを更新する方法について解説しました。

今回のテーマは、Cape.JS のデータストアです。Cape.JS で Web 開発を行う上で非常に重要な概念です。


次に示すのは todo_list.es6 のソースコードの全体です。

class TodoList extends Cape.Component {
  init() {
    $.ajax({
      type: 'GET',
      url: '/api/tasks'
    }).done(data => {
      this.tasks = data;
      this.refresh();
    });
  }

  render(m) {
    m.ul(m => {
      this.tasks.forEach(task => {
        m.li(m => {
          m.class({ completed: task.done });
          m.label(m => {
            m.onclick(e => this.toggleTask(task));
            m.input({ type: 'checkbox', checked: task.done }).sp();
            m.span(task.title);
          });
        });
      });
    });
  }

  toggleTask(task) {
    $.ajax({
      type: 'PATCH',
      url: '/api/tasks/' + task.id,
      data: { task: { done: !task.done } }
    }).done(data => {
      if (data === 'OK') {
        task.done = !task.done;
        this.refresh();
      }
    });
  }
}

まだ完成には程遠いのですが、すでに39行もあります。少しソースコードを分割したいところです。

分離するとすればどの部分でしょうか。私なら init メソッドと toggleTask メソッドにある $.ajax メソッドの呼び出しを他のクラスに移します。Cape.JS のコンポーネントは MVC モデルの「V」すなわちビューに当たるものなので、データの取得や更新に関わるコードを持たない方がいいのです。

ここで登場するのがデータストアです。MVC モデルの「M」に当たります。

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

class TaskStore extends Cape.DataStore {
  constructor() {
    super();
    this.tasks = [];
  }

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

  toggleTask(task) {
    $.ajax({
      type: 'PATCH',
      url: '/api/tasks/' + task.id,
      data: { task: { done: !task.done } }
    }).done(data => {
      if (data === 'OK') {
        task.done = !task.done;
        this.propagate();
      }
    });
  }
}

データストアとは、その名の通り、データの格納庫です。Cape.DataStore クラスを継承して定義してください。データストアがインスタンス化される際に呼ばれる constructor() メソッドの中では、必ず super() メソッドを呼んでから、データの初期化を行ってください。

TaskStore クラスの refresh() メソッドは、TodoList クラスの init() メソッドの内容と同じです。ただし、1箇所だけ違いがあります。this.refresh()this.propagate() に変わっています。

また、TaskStore クラスの toggleTask() メソッドは、TodoList クラスの同名メソッドのコピーです。ただし、this.refresh()this.propagate() に変わっています。

データストアの propagate() メソッドの役割については後述します。


続いて、TodoList クラスのソースコードを次のように変更します。

class TodoList extends Cape.Component {
  init() {
    this.ds = new TaskStore();
    this.ds.attach(this);
    this.ds.refresh();
  }

  render(m) {
    m.ul(m => {
      this.ds.tasks.forEach(task => {
        m.li(m => {
          m.class({ completed: task.done });
          m.label(m => {
            m.onclick(e => this.ds.toggleTask(task));
            m.input({ type: 'checkbox', checked: task.done }).sp();
            m.span(task.title);
          });
        });
      });
    });
  }
}

toggleTask() メソッドは全体を削除します。

init() メソッドの中身はまったく別のものとなりました。

    this.ds = new TaskStore();
    this.ds.attach(this);
    this.ds.refresh();

1行目で TaskStore クラスのインスタンスを作り、自身の ds プロパティにセットしています。続いて、2行目で TaskStore オブジェクトの attach() メソッドに対して自分自身を引数として渡しています。

この2行目が今回のお話しの核心です。

コンポーネントが自分自身をデータストアに結び付ける(attach)ことにより、データストアの持つデータが変化したときに通知を受け取れるようになります。

データストアが結び付けられたコンポーネントにデータの変化を通知することを、伝播(propagation)と呼びます。

さきほど TaskStore を説明したとき propagate() メソッドの説明を後回しにしました。このメソッドはまさにこの伝播を行います。

伝播の結果として、コンポーネントの refresh() メソッドが呼ばれます。つまり、データが変化するとコンポーネントが再描画されるのです。

データストアには複数のコンポーネントを結び付けることができます。その場合、データストアはそれらのコンポーネントに対して順にデータの変化を通知していきます。別の見方をすれば、コンポーネント群がデータストアを「observe」しているとも言えます。この見方によれば、データストアが持つデータが変化したらコンポーネントが自分自身を再描画する、ということになります。

init() メソッドの3行目では、次のように書いています。

    this.ds.refresh();

コンポーネントの refresh() メソッドではなく、データストアの refresh() メソッドを呼んでいる点に留意してください。つまり、TaskStore オブジェクトにおいて次のコードが実行されることになります。

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

Ajax コールが行われ、データストアのデータ(this.tasks)が更新され、「伝播」が始まります。その結果として、コンポーネントの refresh() メソッドが呼ばれ、1回目の描画が行われます。

少しややこしいかもしれません。ソースコードを丹念に追って、処理の流れを理解してください。


TodoList クラスの render() メソッドにも変更があります。次の抜粋をよく見てください。

    m.ul(m => {
      this.ds.tasks.forEach(task => {
        m.li(m => {
          m.class({ completed: task.done });
          m.label(m => {
            m.onclick(e => this.ds.toggleTask(task));
            m.input({ type: 'checkbox', checked: task.done }).sp();
            m.span(task.title);
          });
        });
      });
    });

まず、2行目で this.tasks.forEachthis.ds.tasks.forEach に変わっています。コンポーネント自身がデータを持つのをやめて、その役割をデータストアに委譲したので、このように変更しました。

また、6行目で this.toggleTask(task)this.ds.toggleTask(task) に書き換えました。toggleTask() メソッドをデータストアに移した結果です。データストアに移された toggleTask() メソッドでは最後に this.propagate() が呼ばれ、データの変化がコンポーネントに「伝播」してきます。


今回の変更によってアプリケーションの振る舞いは変化していません。前回同様に Rails アプリケーションを起動し、各タスクのチェックボックスをクリックして、タスクの done カラムの値がデータベース上で変化することを確かめてください。

次回は、タスクを新規追加するフォームを表示する方法について解説します。