体感!JavaScriptで超速アプリケーション開発 -Meteor完全解説

第11回データ同期とリアクティブ・プログラミング

今回は、前回に引き続きMeteorのリアクティブ・プログラミングについて説明します。

Meteorアプリケーションでは、あるクライアント上で行ったデータ変更が、他のクライアントにもリアルタイムに伝わります。しかも、そのために必要なコードはごくわずかで、ほぼ自動的に処理されるといっても過言ではありません。

その仕組みは、⁠クライアント上のリアクティビティ」と、⁠publish/subscribeによる、サーバデータとクライアントキャッシュの同期」によって実現されています。

前回、クライアント上でのリアクティビティについてはお伝えしました。今回はpublish/subscribeについて解説し、リアクティビティについても復習します。

publish/subscribeによるデータ同期

Meteorでは、サーバ上のデータはクライアントにキャッシュされ、リアルタイムに同期されます。同期は自動的に、しかも双方向に行われるため、開発者が明示的に同期処理を記述する必要はありません。必要とされるのは、Meteor.publish()Meteor.subscribe()を使用し、データを同期する範囲を指定することだけです。

図1 publishとsubscribe
図1 publishとsubscribe

publishsubscribeの説明のため、まずは前提条件として、⁠従業員データ」を格納する以下のようなコレクションがあると仮定します。このコレクションは、クライアントとサーバの双方でインスタンス化されます。

var Employees = new Meteor.Collection('employees');

サーバ側で実行されるMeteor.publish()は、名前を表す文字列と、関数オブジェクトを引数に取ります。

Meteor.publish("emp", function () {
    // 60歳以上の従業員データを返す
    return Employees.find({ age: { $gte: 60 }});
});

引数として渡す関数オブジェクトは、データのコレクションを戻り値として返します。この戻り値となったコレクションが、クライアント側のキャッシュと自動的に同期されます。上の例では、60歳以上の従業員データが返されます。

対してMeteor.subscribe()はクライアント側で呼び出され、publishによって公開されたデータを「購読」⁠同期して取り込むこと)します。

Meteor.subscribe('emp');

ここに挙げた例では、サーバデータのうち「60歳以上」のデータのみが、クライアントのコレクションに同期されます。

publishsubscribeは、Meteorプログラミングにおいては非常に重要なAPIです。以降では、これらのAPIについてさらに詳しく説明します。

Meteor.publish()

Meteor.publish()の定義は以下のとおりです。

Meteor.publish(name, func)
  • name…名前を表す文字列。クライアントがサブスクライブを行う際に、この名前が使用される。
  • func…この関数は、クライアントからのサブスクライブが行われるたびに呼び出されます。

第二引数funcの目的は、クライアント側のキャッシュと同期するデータ(コレクション)を指定にすることです。そのひとつの方法としては、コレクションのカーソルMeteor.Collection.Cursorを戻り値とすることです。上記の例を再掲します。

Meteor.publish("emp", function () {
    // 60歳以上の従業員データを返す
    return Employees.find({ age: { $gte: 60 }});
});

もうひとつの方法としては、publish内でコレクションに変更を行い、その旨をクライアントに通知することです。こうした操作は、publish内のthisオブジェクトが持つ以下のようなメソッドを通じて行います。

added(collectionName, id, fields)
…コレクション名、ドキュメントのID(_idフィールド⁠⁠、追加されたドキュメント(JavaScriptオブジェクト)を引数に取り、ドキュメントが追加されたことをクライアントに通知します。
changed(collectionName, id, fields)
…コレクション名、ドキュメントのID(_idフィールド⁠⁠、変更されたドキュメント(JavaScriptオブジェクト)を引数に取り、ドキュメントが変更されたことをクライアントに通知します。
removed(collectionName, id)
…コレクション名、ドキュメントのID(_idフィールド)を引数に取り、ドキュメントが削除されたことをクライアントに通知します。

また、publish内のthisオブジェクトは、以下のようなメソッドも保持しています。

userId
…ログインユーザのID。ログインされていなければnull。
ready()
…クライアントに対し、初期データの送信が終わったことを伝える。Meteor.subscribe()のコールバックが呼び出される。
onStop(func)
…パブリッシュが停止した際に呼び出されるイベントハンドラを登録する。
error(error)
…クライアントにエラーを通知する。クライアントからのサブスクライブは停止し、Meteor.subscribe()のコールバックが呼び出される。
stop()
…パブリッシュを停止する。

また、funcは引数を取ることもできます。funcに引数として渡される値は、クライアントがMeteor.subscribe()で渡したものになります。よくある例としては、クライアントが検索条件などを指定してサブスクライブを行う場合に、検索条件を渡す用途で使用されます。

Meteor.subscribe()

Meteor.subscribe()は、パブリッシュされたデータをクライアント側から「サブスクライブ(購読⁠⁠」するために、クライアント側で呼び出されます。Meteor.subscribe()の定義は以下のようになっています。

Meteor.subscribe(name [, args...][, callbacks])
name
…サブスクリプションの名称publishで指定した名前)
args
Meteor.publish()に渡す引数。任意の数の引数を渡すことができます。
callbacks
…関数、もしくはJavaScriptオブジェクトを指定出来ます。JavaScriptオブジェクトを指定する場合、onReady(初期化完了時に呼び出される)onError(エラー時に呼び出される)というコールバックプロパティを持つことができます。関数を指定した場合は、onReadyを指定したのと同様になります。

また、戻り値は以下のメンバーを持つJavaScriptオブジェクトです。

stop()
…サブスクリプションを停止することができます。
ready()
…このサブスクリプションが「準備完了」状態になっているかどうかpublishの節におけるready()メソッドの説明を御覧ください)を返します。リアクティブ・データソースであり、この値が変化すると、リアクティブ・コンテキストの再評価が行われます(リアクティブ・データソースやリアクティブ・コンテキストについては前回の記事を御覧ください)

Meteor.autorun()を用いてsubscribeを自動的に再実行する

Meteor.autorun()は、前回の記事でも紹介しました。autorun()は任意の関数を受け取ります。その関数は即座に実行され、同時に関数内部で使用されているリアクティブ・データソースが記憶されます。そのデータソースに変化があると、autorun()に渡された関数は自動的に再実行されます。

// 内部のリアクティブ・データソースが変化すると、関数が再実行される。
// この例では、Session内の"roomId"の値が変化すると、再実行されます。
Meteor.autorun(function () {
  Meteor.subscribe("counts-by-room", Session.get("roomId"));
});

Meteor.autorun()内でMeteor.subscribe()を使用していると、再実行時にサブスクリプションの再構築が行われます。例えば、検索条件を指定したサブスクライブを行なっている場合などに、検索条件が変化したらサブスクリプションの再構築を行う、と言った用途に使用出来ます。

サンプル1: 各種APIの利用例

以上で紹介したさまざまなAPIを利用している簡潔なコード例として、まずはMeteorのドキュメントから引用します。この例のポイントは、Meteor.publish()の内部でthis.addedthis.ready()などのAPIを利用している点です。この例はチャットアプリケーションを想定しており、チャットルームごとの人数を保持するために「counts」というコレクションを定義しています。

// サーバ: コレクションの現時点でのサイズをパブリッシュする
Meteor.publish("counts-by-room", function (roomId) {
  var self = this;
  var count = 0;
  var initializing = true;
  var handle = Messages.find({roomId: roomId}).observeChanges({
    added: function (id) {
      count++;
      if (!initializing)
        self.changed("counts", roomId, {count: count});
    },
    removed: function (id) {
      count--;
      self.changed("counts", roomId, {count: count});
    }
    // movedやchangedはサイズ変更を伴わないので無視
  });

  // まずaddedコールバックを呼び出し、初期値をクライアントに送信する
  initializing = false;
  self.added("counts", roomId, {count: count});
  // 初期値を送信し終わったことをクライアントに通知
  self.ready();

  // クライアントからのサブスクライブが終了した際に呼び出される
  self.onStop(function () {
    handle.stop();
  });
});

// クライアント: カウントを保持するコレクションを定義する
Counts = new Meteor.Collection("counts");

// クライアント: 現在のチャットルームごとの参加人数をサブスクライブする
Meteor.autorun(function () {
  Meteor.subscribe("counts-by-room", Session.get("roomId"));
});

// クライアント: コレクションからカウントを取得する
console.log("Current room has " +
            Counts.findOne(Session.get("roomId")).count +
            " messages.");

サンプル2: 実際に動作するサンプル

また、実際に動作させることのできる単純なサンプルとして、連載第9回で使用した従業員データベースに機能を追加したものをご紹介します。

前回のサンプルとの違いは、従業員の年代別に絞り込みを行えるようにしたことです。一覧表の上部に、年代ごとに絞り込みを行える選択リストを置きました。このリストでは、⁠絞り込みを行わない」⁠10代」⁠20代」...「60代以上」を選択することができます。

図2 年代絞り込みリストを追加
図2 年代絞り込みリストを追加

サンプルコードは以下のリンクからダウンロードすることができます。

以下に、今回のサンプルのポイントを示します。

まず、今回追加した選択リストは、以下のようなHTMLとなっています。option要素の値が、そのまま検索条件のパラメータとして利用されます。

<select id="filter">
  <option value="">年代で絞り込む</option>
  <option value="0">10歳以下</option>
  <option value="10">10代</option>
  <option value="20">20代</option>
  <option value="30">30代</option>
  <option value="40">40代</option>
  <option value="50">50代</option>
  <option value="60">60歳以上</option>
</select>

そして、このリストの選択状態が変化したら、以下のイベントハンドラが呼び出されます。セッションに、選択された値をセットします。

'change #filter': function(e, template) {
    var selected = e.target.options[e.target.selectedIndex].value;
    // (1) セッションに選択値をセット
    Session.set('filter', selected);
},

クライアント側のコードで、最大のポイントがここです。Meteor.autorun()を使用してサブスクライブを行っていますが、autorun()に渡した関数の内部でセッションの値にアクセスしています。したがって、⁠リストの選択状態が変化する→セッションに選択値がセットされる→サブスクライブし直す」という流れが実現できていることになります。

// (2) 従業員データをサブスクライブする
Meteor.autorun(function() {
    Meteor.subscribe('emp', Session.get('filter'));
});

そして、サーバ側のコードを見てみます。クライアントがサブスクライブしている'emp'というコレクションは、以下のようにパブリッシュされています。

// (3) 全ての従業員データを公開する
Meteor.publish("emp", function (filter) {
    // クライアントから送信された検索条件に応じて、処理が変わる
    // 検索条件の指定なし
    if (!filter) {
        return Employees.find(); // everything
    }
    var minAge = parseInt(filter);
    switch (minAge) {
    // 60歳以上
    case 60:
        return Employees.find({ age: { $gte: 60 }});
    default:
        return Employees.find({ age: { $gte: minAge, $lt: minAge + 10 }});
    }
});

たったこれだけのコード追加で、検索条件に応じてデータの絞り込みを行い、かつ、それらをリアルタイムに監視するアプリケーションができてしまいます。実際にサンプルコードを動作させて、いかにMeteorのリアクティブ・プログラミングが強力かを体感してみてください(ブラウザウィンドウを2枚開いて動作させて見ることをオススメします⁠⁠。

まとめ

前回と今回で、Meteorのリアクティブ・プログラミングについて解説しました。リアクティビティはMeteorにとって最大のウリのひとつです。ぜひそのパワーを理解し、新時代のWebアプリ体験に触れてみてください。

おすすめ記事

記事・ニュース一覧