Collections

4

翻訳の進捗

本章で学ぶこと:

  • Meteorの基本的な機能であるリアルタイムコレクションについて学ぶ。
  • どのようにMeteorのデータが同期するのか理解する。
  • コレクションとテンプレートと連動させる。
  • ここまでで作った試作品をリアルタイムで動くアプリにする!
  • 1章では、クライアントとサーバー間のデータを自動的に同期させる Meteor の特徴についてお話しました。

    この章では、これがどのように動いているのかもう少し詳しく見ていき、 データの自動同期を実現させる上で鍵となる Meteorコレクションについて見ていきます。

    コレクションは、永続的な特殊なデータ構造です。サーバー側のMongoDBデータベース内のデータを格納し、 リアルタイムで接続された各ユーザのブラウザと同期処理を行います。

    投稿が永続的にユーザー間で共有されるようにしたいので、 それらを保存するためにPostsと呼ばれるコレクションを作成することから始めます。

    コレクションはたいていのアプリにおいてコアとなる部分になります。 ですので常に最初にlibディレクトリの中に配置することから始めます。 まずは、libディレクトリ内にcollections/ ディレクトリを作成し、中にposts.jsを格納します。 そして次のように書き込みます。:

    Posts = new Meteor.Collection('posts');
    
    lib/collections/posts.js

    コミット 4-1

    Added a posts collection

    Varを使うか使わないか

    Meteorでは、var はオブジェクトのスコープを現在のファイル内に限定します。 ここで私たちはアプリ全体で使えるPostsコレクションを作りたいので、 var使っていないというわけです。

    データの保存について

    Webアプリケーションは、それぞれが異なる役割を埋める彼らの自由にデータを格納する3つの基本的な方法があります:

    • ブラウザのメモリ領域: JavaScript変数など、永続的ではないブラウザのメモリに格納します: 現ブラウザタブに対して局所的で、ブラウザタブを閉じると消えてしまいます。
    • ブラウザのストレージ: ブラウザでもクッキーを使用するか、 ローカルストレージを使うことで、 データをより永続的に記憶することができます。 このデータはセッション間で維持されますが、 それは現在のユーザーにローカル(ただし、タブ全体で利用可能な)だし簡単に他のユーザーと共有することはできません。
    • サーバーサイドデータベース: 複数のユーザーが使用できるようにする永続的データのための最適な場所は、古き良きデータベースにあります。 (MongoDB Meteorアプリケーションのためのデフォルトのソリューションです)

    Meteorは、すべての3つを利用して、(進めればすぐにわかります)時々ある場所から別の場所へデータを同期します。 それでも、データベースは、データのマスターコピーが含まれている「標準的な」データソースのままです。

    クライアントとサーバ

    client/server/以外のフォルダ内のコードは、両方のコンテキスト(サーバとクライアント)で実行されます。 ですので、Postsコレクションは、クライアントとサーバーの両方で使用できます。 しかし、コレクションは、各環境でかなり異なる可能性があります。

    サーバー上では、コレクションは、MongoDBのデータベースと対話し、 読み込みや変更を書き込む処理をします。 この意味で、それは、標準的なデータベースライブラリと比較することができます。

    クライアント上では、コレクションは本物の、標準的なコレクションの一部のコピーです。 クライアント側のコレクションは常に存在し、(ほぼ)透過的にリアルタイムで最新の状態に維持されます。

    コンソール vs コンソール vs コンソール

    この章では、ブラウザのコンソールを使っていきます。 これはターミナルMongo シェルとは違います。 ここでは、そのあたりについてざっくりと解説していきます。

    ターミナル

    The Terminal
    The Terminal
    • オペレーティングシステムから呼び出される。
    • サーバーサイドconsole.log()は、ここに出力される。
    • プロンプト:$
    • 別名:Shell、Bash

    ブラウザコンソール

    The Browser Console
    The Browser Console
    • ブラウザで JavaScript のコードを実行する。
    • クライアントサイドconsole.log()をここに出力する。
    • プロンプト:
    • 別名:JavaScript Console、DevTools Console

    Mongoシェル

    The Mongo Shell
    The Mongo Shell
    • ターミナルからmeteor mongo と打つと、呼び出される。
    • 作っているアプリのデータベースに直接アクセスできる。
    • プロンプト:>
    • 別名:Mongoコンソール

    ここで留意すべきことは、プロンプト文字($,,>)をコマンドで入力しなくて良いということです。 プロンプトより先の文字で始まっていないものは、それより先に行ってたコマンドが出力したものと見なすことができます。

    サーバーサイドのコレクション

    サーバーでのコレクションは MongoDB の API のような役割を果たします。 サーバーサイドのコードでPosts.insert()Posts.update()のようなMongoコマンドを書くことができます。 すると、MongoDB 内に格納されているpostsコレクションは変化します。

    MongoDB 内を見るために、2つ目のターミナルウィンドウを開きます。 (1つ目のターミナルではまだmeteorが動いている状態です。) そうしたら、アプリのディレクトリへ行きましょう。 そして、Mongo シェルを起動するためにmeteor mongoコマンドを実行します。 Mongo シェルでは、通常の Mongo コマンドを入力することができます。 (また、いつものようにctrl+cで停止することができます。)

    例として、新しい投稿を挿入しましょう。:

    meteor mongo
    
    > db.posts.insert({title: "A new post"});
    
    > db.posts.find();
    { "_id": ObjectId(".."), "title" : "A new post"};
    
    The Mongo Shell

    Mongo on Meteor.com

    *.meteor.com にアプリをホスティングする際は、 meteor mongo myAppと入力することでデプロイされたアプリのMongoシェルにアクセスすることができます。

    さらに、その状態でmeteor logs myAppと入力すること、アプリのログを見ることができます。

    MongoDB の構文は、JavaScript インターフェースを使っているため、親しみやすくなっています。 私たちはこれ以上 Mongoシェルでデータ操作をしませんが、MongoDB内に何があるのか確認するために時々のぞき見をするかもしれません。

    クライアントサイドのコレクション

    コレクションはクライアントサイドでは、より面白くなってきます。 クライアント上では、あなたが作成している実際のMongoのコレクションのローカルなブラウザ内のキャッシュが作成されます。 クライアント側のコレクションが「キャッシュ」であるというのは、 それがサーバサイドデータの一部であり、このデータへの高速アクセスを提供している。ところから述べています。

    この点を理解することは大事なことです。 というのは、これが Meteor が動作の基本だからです。 一般的に、クライアントサイドのコレクションは Mongo コレクションに格納されているすべてのドキュメントの一部分から構成されます。 (結局のところ、私たちはすべてのデータベースをクライアントに送りたいわけではありません。)

    次に、こうしたドキュメントはブラウザのメモリに保存されているので、 このドキュメントには基本的に一瞬でアクセスするということを意味しています。 つまり、データを取ってくるためにクライアントでPosts.find()を呼び出す際、 データは事前に読み込まれているのでサーバーやデータベースへのアクセスは速いのです。

    Introducing MiniMongo

    Meteor のクライアントサイドでの実装は MiniMongo と呼ばれます。 まだ完全な実装ではないので、通常の MongoDB の機能が MiniMongo で動かないことがあるかもしれません。 とはいえ、本書でカバーしているすべての機能は MongoDB と MiniMongo で同じように動きます。

    クライアントとサーバーの通信

    ここで重要な点は、どのようにクライアントサイドのコレクションが同じ名前のサーバーサイドのコレクションと同期するのかということです。 (この場合では、'posts'

    この点は詳細に説明するよりも、実際に何が起こるのか見る方が良いでしょう。

    まず2つのブラウザウィンドウを開いて、両方でブラウザコンソールにアクセスしましょう。 それから、コマンドラインで Mongo シェルを開きます。

    この時点で、我々は3つのコンテキスト全てで、先ほど作った1つのドキュメントを見つけることができるはずです。 (私たちのアプリのユーザーインターフェースはまだ他に3つのダミーポストが表示されているはずですが、今だけ無視してください。)

    > db.posts.find();
    {title: "A new post", _id: ObjectId("..")};
    
    The Mongo Shell
     Posts.findOne();
    {title: "A new post", _id: LocalCollection._ObjectID};
    
    First browser console

    新しい投稿を作りましょう。 ブラウザウィンドウの一つで、insert コマンドを実行します:

     Posts.find().count();
    1
     Posts.insert({title: "A second post"});
    'xxx'
     Posts.find().count();
    2
    
    First browser console

    当然のように、投稿はローカルのコレクションに作られました。Mongo をチェックしましょう:

    ❯ db.posts.find();
    {title: "A new post", _id: ObjectId("..")};
    {title: "A second post", _id: 'yyy'};
    
    The Mongo Shell

    ご覧のように、この投稿はクライアントとサーバーをフックするための一行もコードを書くことなく、 すべてのMongoDBをさかのぼって保存されます。 (厳密に言うと、私たちは new Meteor.Collection('posts') という一行のコードを書きました。) しかし、話はここで終わりません!。

    2つ目のブラウザーウィンドウを立ち上げて、ブラウザーコンソールに次のように入力します:

     Posts.find().count();
    2
    
    Second browser console

    先ほどの投稿がここにもあります! 私たちは更新もせず、2つ目のブラウザーと情報をやりとりもしていないのにも関わらず、 更新情報を転送するコードも書いていません。魔法のように一瞬にして、このことが起こります。 この点は、章を進めるにつれ理解できるようになります。

    何が起きたかというと、サーバー側のコレクションに クライアント・コレクションの新しい投稿によって通知されたということです。 MongoDB内の投稿配信のタスクを引き受け、すべての接続されたpostコレクションへデータを配信します。

    ブラウザーコンソールで投稿を取ってくることはそれほど役立ちません。 テンプレートに、このデータをつなげていき、単純なHTMLプロトタイプを機能的なリアルタイムWebアプリケーションにする過程をすぐに学びます。

    データベースにデータを追加する

    ブラウザコンソール上でコレクションの内容を見ると、その表示処理は 本当にやりたいことである画面上へのデータ、データへの変更と表示を行っています。 このように、静的データを単純なWebページから、動的にデータが変化するリアルタイムWebアプリケーションへ切り替えて行きましょう。

    最初にデータベースにデータを入れます。 サーバーの初回起動時に、Postsコレクションに構造化データの固定ファイルを読み込み設定する処理を行います。

    まず、データベースの中が何もないようにしましょう。 meteor resetを使うことで、データベースを削除してプロジェクトをリセットします。 当然のことながら、あなたが実際のプロジェクトに取り組み始めたら、このコマンドに対して十分注意が必要です。

    ctrl-cを押してMeteorのサーバーを止めてから、コマンドラインで動かします。

    $ meteor reset
    

    リセットコマンドは MongoDB を完全に空っぽにします。 これはデータベースが一貫性のない状態に陥る可能性が高い開発では便利なコマンドです。

    Meteorを再起動しましょう

    $ meteor
    

    データベースが空になったので、次のコードを書き込みましょう。 これでサーバーが起動して空のPostsコレクションを見つけると、3つの投稿が読み込みます。

    if (Posts.find().count() === 0) {
      Posts.insert({
        title: 'Introducing Telescope',
        url: 'http://sachagreif.com/introducing-telescope/'
      });
    
      Posts.insert({
        title: 'Meteor',
        url: 'http://meteor.com'
      });
    
      Posts.insert({
        title: 'The Meteor Book',
        url: 'http://themeteorbook.com'
      });
    }
    
    server/fixtures.js

    コミット 4-2

    Added data to the posts collection.

    このファイルはserver/ディレクトリにあるので、他のユーザーのブラウザで読み込まれることはありません。 このコードは、サーバーが起動すると瞬時に実行され、データベース上でinsertを呼び出し、Postsコレクション内に3つの投稿を追加します。

    再びmeteorでサーバーを起動すると、この3つの投稿はデータベースで読み込まれます。

    動的データ

    これでブラウザーコンソールを開くと、MiniMongo 内で読み込まれた3つの投稿を見ることができます。

     Posts.find().fetch();
    
    Browser console

    この投稿をレンダリングした HTML にするためにテンプレートヘルパーを使います。

    3章では、Meteor がどのようにデータコンテキストをSpacebarsテンプレートと結びつけるのか見てきました。 Spacebars テンプレートはシンプルなデータ構造の HTML 表示を作り出します。 私たちはまさに同じような方法でコレクションデータを結びつけます。 静的なJavaScriptのpostsDataオブジェクトを動的なコレクションに置き換えていきましょう。

    そういえば、この時点で postsData コードは削除しましょう。 現在の posts_list.js は、このようにします。

    Template.postsList.helpers({
      posts: function() {
        return Posts.find();
      }
    });
    
    client/templates/posts/posts_list.js

    コミット 4-3

    Wired collection into `postsList` template.

    Find と Fetch

    Meteorでのfind()は、リアクティブデータソースであるカーソルを返します。 その中身の記録を取り出したい場合は、カーソルを配列に変換するfetch()を使います。

    アプリ内のMeteorは明示的に配列に変換することなく、カーソルを繰り返し処理することができます。 そのような理由で、実際の Meteor コードでfetch()を見る機会はそれほど多くないでしょう。 (また、その理由から上記の例でfetch()を使いませんでした。)  

    ここでは、変数から静的な配列の投稿リストを呼び出すのではなく、 カーソルをpostsヘルパーに返しています。 (我々はまだ、同じデータを使用しているので、見た目上は違いがわからないかもしれません。)

    Using live data
    Using live data

    {{#each}}ヘルパーがPostsのすべてを繰り返し処理して、 スクリーン上で表示していることがはっきりとわかります。 サーバーサイドコレクションMongoDBから投稿を呼び出して、クライアントサイドのコレクションに引き渡します。 それから、Spacebars ヘルパーがそれらをテンプレートに引き渡します。

    さらに、もう一歩踏み込みます。コンソールからもう一つ投稿を加えましょう。

     Posts.insert({
      title: 'Meteor Docs',
      author: 'Tom Coleman',
      url: 'http://docs.meteor.com'
    });
    
    Browser console

    ブラウザーを見てみると、このようになっているはずです。

    Adding posts via the console
    Adding posts via the console

    たった今、あなたは初めて作動中のリアクティビリティを目撃しました。 SpacebarsにPosts.find()カーソルを繰り返し処理する命令をすると、 Spacebarsはそのカーソルの変化を見つけて、スクリーン上で正しいデータを表示するために、 とてもシンプルな方法で HTMLに適用します。

    DOM変化の分析

    この場合、最も簡単な変化の可能性はもう一つ<div class="post">...</div>を追加することでした。 これが本当に起こったことだと確かめたい場合は、DOM inspector を開いて、 投稿に対応する<div>要素を選択します。

    ここで、JavaScript コンソールで、もう一つの投稿を挿入します。 DOM inspector に戻ると、新しい投稿に対応する<div>が見つかりますが、 まだ選択されたままの同じ<div>も存在しています。 これはいつ要素が再レンダリングしたか、要素がそのままなのか、見分ける便利な方法です。  

    コレクションをつなげる: パブリケーションとサブスクリプション

    今までは、製品としてのアプリには向いていないautopublishパッケージが有効となっていました。 名前が意味しているように、autopublishパッケージは各コレクションの中のすべてをそれぞれつながったクライアントと共有させます。 私たちはこうしたことをしたくないので、autopublishを停止させましょう。

    新しいターミナルを開いて、次のように打ち込みます:

    $ meteor remove autopublish
    

    これはすぐに有効化されます。 今ブラウザーを見ると、すべての投稿が消えています! それは私たちがautopublishに依存していたからであり、autopublishは投稿に関するクライアントサイドの コレクションがデータベース内のすべての投稿に反映させていたからです。

    最終的には、ページネーションなどを考慮して、ユーザーが見る必要のある投稿(アカウントの事を考慮しています。)だけを送る必要があります。 とはいえ、今のところはPosts全体がパブリッシュされるように設定します。

    そうするために、publish()関数を作ります。この関数はすべての投稿を参照するカーソルを返します。

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    server/publications.js

    クライアントでは、パブリケーションにサブスクライブする必要があります。 main.js に次のようにコードを追加しましょう。

    Meteor.subscribe('posts');
    
    client/main.js

    コミット 4-4

    Removed `autopublish` and set up a basic publication.

    再びブラウザーをチェックすると、投稿が元に戻っています。ふぅ!

    結論

    それで、私たちは何を達成したのでしょうか? ええと、まだユーザーインターフェースはありませんが、私たちが作ったものは実用的なウェブアプリケーションです。 私たちはこのアプリケーションをインターネットにデプロイすることができます。 また、(ブラウザコンソールを使って)新しい投稿をおこない、世界中のユーザーのブラウザに投稿を表示することができます。