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

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

この記事を読むのに必要な時間:およそ 4 分

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アプリ体験に触れてみてください。

著者プロフィール

白石俊平(しらいししゅんぺい)

株式会社オープンウェブ・テクノロジー代表取締役

HTML5開発者コミュニティhtml5j.org管理人

HTML5とか勉強会主催

Web先端技術味見部 部長

読書するエンジニアの会主催

しろうと哲学部 部長

Google API Expert(HTML5)

Microsoft Most Valuable Professional 2010, 2011(Internet Explorer)