Discover Meteor

Building Real-Time JavaScript Web Apps

紹介

1

私がこれから言うことをちょっと想像してみてください。あなたは今自分のコンピューターに向かって、同じフォルダーを2つの異なるウインドウで開いています。

そして今、2つのウインドウのうちの1つをクリックして、ファイルを削除します。削除したファイルは別のウインドウからも消えましたか?

消える事を知っていたら、実際にこれを試す必要はありません。私たちがローカルのファイルシステムで何かを修正をするとき、リフレッシュやコールバックの必要なしに修正内 容は更新されます。いや、出来てしまうのです。

ところで、同じシナリオをWEB上で行っときの事について考えてみましょう。例えば、同じワードプレスの管理サイトを二つのブラウザで開いていたとします。そして、どちらか一方を新規更新したとします。デスクトップ上とは違い、どんなに長く待っても、自らリフレッシュしない限り、もう1つのウインドウのサイトには更新内容が反映されません。

長年私たちはWEBサイトとは、短く、別々のバーストで通信する物だという考えに慣れてきました。

しかし、Meteorは、Webをリアルタイムで反映させ続ける状態を保つ事に挑戦し続ける、フレームワークとテクノロジーの新しい時代の波の1つです。

Meteorて何?

MeteorとはリアルタイムでWebアプリを構築できるNode.jsの上に構築されたプラットフォームです。あなたのアプリのデータベースとそのユーザーインターフェイスの間のサイト、その両方がシンクし続けている状態を保させる状態を作ります。

Node.js上で構築されているので、Meteorはクライアント上そしてサーバー上ではJavaScriptを使用します。さらに重要な事は、Meteorは両方の環境の下、コードをシェアする事が出来るのです。

このような結果は、Webアプリの開発中に起こりうる落とし穴や、日常起こる煩わしさなどを取り除き、とてもパワフルにそしてとてもシンプルに管理することができます。

なぜMeteor?

なぜ他のWebフレームワークでなくMeteorを学ぶべきなのか?Meteorの様々の機能については少し置いておいて、私たちは1つの事に集約されると信じています:Meteorは簡単に学べる。

もう他のどんなフレームワークより、Meteorはわずかな時間があれば、リアルタイムでWebアプリの取得とWeb上での公開が可能になるのです。もしあなたがかつてフロントエンドのディベロッパーだったとしたら、すでにJavaScriptには慣れているはずですよね、そしてまた新たに言語を習得必要はありません。

Meteorは、あなたが必要としているフレームワークかも知れませんし、または違うかもしれません。しかし、帰宅後もしくは週末の数時間のコースでやれるならば、これが自分に相応しいかどうかをトライしてみる価値はあると思いませんか?

なぜこの本?

過去六ヶ月の間、 Telescopeや、誰でも独自のニュースサイトを作れるソーシャルニュースサイト( Redditまたは、みんなが投稿したリンクに票を入れられる(Hacker news)などのオープンソースのMeteor Appの製作をしてきました。

アプリ構築について山のように学びました、しかし、常に向き合う問題に対して答えを見つけるのは容易ではありませんでした。沢山の異なるソースを集結してつなぎ合わせなければならず、多くの場合、独自の可決策を考えなければなりませんでした。だからこの本では、私たちのこの教訓を共有し、最初から本格的なMeteorアプリを構築する手順を、順に追って説明し、簡単なステップーバイースッテプのガイドを作りたかったのです。

私たちが構築しているアプリはTelescopeのバージョンよりも簡素化したバージョンで、Microscopeと呼んでいます。構築している間、Meteorアプリの構築の中に入る、ユーザーアカウント、Meteorコレクション、ルーティン、など複数の異なる事柄全てに対応します。

そしてこの本を読み終わった後に、もっと掘り下げて勉強したい思った時でも、Telescopeと同じパターンに従っているので簡単にTelescopeのコードを習得できるでしょう。

この本は誰のため?

この本を書く上での私たちのゴールの1つは、親しみやすく、理解しやすいという点を守るということです。それによりあなたが、Meteor, Node.js、MVCフレームワークや、一般的なサーバー側のコーディングの経験がなくてもついていて来れるようにと言う思いがあるです。

またその一方では、基本的なJavaScriptのシンタックスやコンセプトがよく理解されていることが前提となっています。そうは言っても、多少jQueryやブラウザーのディベロッパーコンソールを操作した経験があるだけで大丈夫であろうと判断します。

作家に関して

もしあなたが、私たちの事を、どこの誰でなぜ信頼すべきなのかと躊躇しているのであれば、こちらが私たちの情報です。

Tom Coleman (トム・コールマン)は、クオリティーとUXに焦点を当てているPercolate StudioWeb開発のショップの一部です。彼はAtmosphereのパッケージのリポジトリをメンテナンスしている一人で、また数あるMeteorのオープンソースプロジェクトの陰の ブレインの一人でもあります。

Sacha Greif (サーシャ・グリーフ)はHipmunkRubyMotion などのスタートアップで、プロダクト、webデザイナーとして働いた経験があります。また彼は Telescopeや、(Telescopeをベースとしている)Sidebarの開発者であり、 またFolyoの創設者でもあります。

章 & サイドバー

私たちはこの本を初心者でもプログラマーでも使えるようにしたいと思いました、この本では章を2つのカテゴリーに分けています: 通常の章(番号1から14)そしてサイドバー(番号.5)。

通常の章は、深すぎない程度に、またあなたのやる気を損なわせず、それでいて、可能な限り重要なステップを説明しながらアプリの構築をカバーします。

一方、サイドバーではMeteorの深いところまで掘り下げて、実際のところどうなっているかを追求します。

例えば、あなたが初心者であるなら、最初はサイドバーを飛ばして、Meteorに慣れてきたところでもう一度戻って読むということも出来ます。

コミット & リアルタイムでのインスタンス化

プログラミングを勉強しているときに、いきなりコードが今まで通りのように動作せずデーターが一致しなくなる事ほど最悪な事はありません。

これを避けるために、私たちはa GitHub repository for Microscopeを設置し、数行のコードが更新される度にGitのコミットへ直接リンクされるようにしました。更には、それぞれのコミットが、特定のコミットのあるアプリに対してもリアルタイムでインスタンス化をしてリンクをさせます、それによりローカルにある自分のファイルとそれを比較する事が出来ます。

コミット 11-2

Display notifications in the header.

しかし、私たちがこれらのコミットを用意したから問いって”Git チェック”から次へ行く必要はありません。アプリのコードをマニュアルでタイプする事に時間をかけるて勉強した方が遥かにいいですということは覚えていて下さい。

その他のリソース

Meteorについての特にもっと詳細が知りたい場合はofficial Meteor documentationを参考にするのが一番良いと思われます。 ////

Gitが必要ですか?

GITのバージョンコントロールに慣れているならこの本に忠実に従う必要はないという事を強くお薦めします。

早くついてきたいのであれば、Nick FarinaのGit Is Simpler Than You Thinkをお薦めします.

GITの初心者であれば、コマンドラインを使わなくてもレポスを管理しないでクローンしてくれるGitHub for Mac アプリをお薦めします。

連絡方法

-連絡したい事があればメールでhello@discovermeteor.comまで連絡ください。 - 本の内容に誤字や間違いを見つけた場合、submitting a bug in this GitHub repoまでご連絡ください。 - Microscopeのコードに問題を発見したら、submit a bug in Microscope’s repositoryまでご連絡ください。 - 最後にその他質問がある場合は、アプリのサイドパネルにコメントを残してください。

Getting Started

2

第一印象は重要です。Meteorのインストールプロセスは比較的痛みを伴いません。

大抵の場合最初にターミナルウインドウを開けて、以下をタイプするだけでMeteorがインストールできます。

$ curl https://install.meteor.com | sh

これでMeteorをあなたのシステム上に実行させ、Meteorを使う準備ができます。

Not インストールしてはいけない場合

ローカルにMeteorをインストールできない(もしくはしたくない場合)、ここをNitrous.io.

を確認する事をお勧めします。Nitrous.io はアプリを実行させ、ブラウザー上でコードを編集させてくれるサービスです。 そして、短いガイドをa short guideに描いたのでセットアップするのに便利だと思います。

簡単なアプリの作成。

Meteorをインストールしたので、簡単なアプリを作成してみましょう。まず, Meteorのコマンドラインのツール'meteor'を使います。

$ meteor create microscope

このコマンドはMeteorをダウンロードして、基本的な物をセットアップしてくれ、Meteorプロジェクトの準備をしてくれます。これが終わったら、microscope/ディレクトリーをみて、次の項目が構成されているか確認してください。

microscope.css  
microscope.html 
microscope.js   

Meteorがあなたの為に作ったアプリは簡単なパターンのお手本を示すボイラープレートアプリケーションです。

私たちのアプリが沢山の事をしなくても、実行はできます。アプリを実行するにはタームなるに戻って次をタイプします:

$ cd microscope
$ meteor

次にあなたの画面のブラウザーをhttp://localhost:3000/ (もしくは http://0.0.0.0:3000/と同様にします。) に合わせてください、そしてこのような物を見てください。

Meteor's Hello World.
Meteor’s Hello World.

コミット 2-1

Created basic microscope project.

おめでとうございます!初めてMeteorでアプリを実行する事ができました。ところで、アプリを一旦止めて、アプリが起動しているターミナルタブに持ってきて、ctrl+cを押してください。

そして、Gitを使っているなら、これはgit initで初めてリポするいいタイミングですよ。

バイバイMeteorite

MeteorがMeteoriteと呼ばれる外部の管理パッケージに頼っていた時期がありました。Meteor Ver0.9.0以来、Meteor自体に吸収されてからにMeteoriteはもう必要なくなりました。

もしあなたがこの本、もしくはMeteor関連のマテリアルをブラウズしている間に、Meteoriteの mrtのコマンドラインのユーティリティに関連するものに出くわしたら、 安全に通常のmeteorに置き換えればいいです。 So if you encounter any references to Meteorite’s mrt command line utility throughout this book or while browsing Meteor-related material, you can safely replace them by the usual meteor.

パッケージを追加する

MeteorのパッケージシステムをBootstrapのフレームワークを私たちのプロジェクトに追加して使えるようになりました。

これは、Meteorのコミュニティーメンバーの Andrew Mao (the “mizzao” in mizzao:bootstrap-3 は著作者のユーザーネームパッケージです。)が私たちに情報を更新してくれていることを除いては、 Bootstrapを通常通りに、マニュアルでそのCSSとJavaScriptファイルを追加していくのと何ら変わりないです。

私たちがそこに居る間、Underscore パッケージを同じく追加します。 UnderscoreはJavaScriptユーティリティライブラリーで、JavaScriptのデーター構造を操作する段階になったときにとても便利です。

これを書いている現時点では、underscoreパッケージはまだMeteorの"公式"パッケージの一部に過ぎず、そのため著者者がいないのです:

$ meteor add mizzao:bootstrap-3
$ meteor add underscore

Bootstrap 3を使いしていくとうことは念頭に置いておいてください。この本にある数々のスクリーンショットは Boostrap 2上で実行されている古いバージョンのMicroscopeで撮ったので若干違いが出るかと思います。

コミット 2-2

Added bootstrap and underscore packages.

パッケージのノート

Meteorのパッケージを文脈でつづれというのであれば、明確に挙げれます。Meteorは5種類の基本タイプを兼ね備えています:

-Meteorのコア部は、異なるMeteor platform packagesに分割されています。Meteorアプリのどれにもに含まれており、それらを全く気にする必要は全くありません。

  • 通常のMeteorパッケージは、“isopacks”として認知されております。同一構造のパッケージ(クライアントとサーバー両方の上で動く事が出来るという意味)、 **ファーストーパーティー パッケージ** の'accounts-uiappcacheなどはMeteorのコアチームcome bundled with Meteorによってメンテナンスされています。

-サード-パーティーパッケージは、単なるisopackで、Meteorのパッケージサーバーをアップロードされている他のユーザーによって開発されました。これはAtmosphere もしくは meteor searchコマンドでブラウズが出来ます。

-Local packages はあなたが自分でカスタマイズできるパッケージで、/packagesのディレクトリーに入れておけます。

  • NPM packages (Node.js パッケージモジュール) は Node.js パッケージのことです。Meteorと一緒にBoxの外では作動し合いませんが、以前のパッケージタイプであれば、使用可能であるかもしれません。

Meteorアプリのファイル構成

コーディングを始める前に、私たちはプロジェクトのプロパティーを設定する必要があります。まずは綺麗に構築ができるようmicroscopeディレクトリを開けmicroscope.htmlと、 microscope.js, そして microscope.cssを削除します。

次に、4つのルートディレクトリを /microscope: /client, /server, /public, /lib, そして /collectionsの内側に作成します。

その次に、空のmain.htmlmain.js の両方のファイルを/clientの中に作成します。今のこの作業がもしもアプリを破壊しているのであっても 心配しないでください。次の章でこれらのファイルを埋めていきます。

このいくつかのディレクトリは特別であることをここで述べさせていただきます。コーディングを実行していくのにあたり、Meteorにはいくつかのルールがあります:

  • /server ディレクトリにあるコードはサーバー上のみ実行します。
  • /client ディレクトリにあるコードはクライアントの上のみ実行します。
  • 他のものはみんなクライアントとサーバー上の両方で実行します。
  • フォントや画像などの静的なアセットは/publicディレクトリに行きます。

それから、Meteorがあなたのファイルをロードする順番をどう決めているかを知ることはとても役に立ちます。 - /libにあるファイルは何よりも先にロードされます。 - どの`main.` ファイルは何よりも*後にロードされます。 - その他のものはファイル名に基づいてアルファベット順にロードされます。

これらのルールがMeteorにはあるものの、もしあなたが望まないのでれば、このファイル構成の定義は強制されません。なので、この構成は私たちが提案するものであり、決定事項ではありません。

これについてもっと詳しく知りたければ、official Meteor docsを一読することをお勧めします。

MeteorはMVC?

Ruby on Railなど、他のフレームワークからMeteorに移って来ているなら人、Meteorアプリは MVC (Model View Controller)パターンを採用しているのかと疑問に思っているかもしれません。

簡単に言うと答えはノーです。Railと違ってMeteorはあなたのアプリに対して定義づけされた構成を課してはいません。ですので、この本の中ではアクロニム(頭文字)について深く考えなくても、自分が一番理解ができるよう簡単にコードをレイアウトします。

No public?

はい、うそついてました。Microscopeは静的アセットを使わないためpublic/ ディレクトリは実際には必要ないのです。しかし、他のほとんどのMEteorアプリは少なくとも何枚かの画像を含んでいるため、私たちはこれを含むのことは必要だと考えました。

ところで、もう一つ、隠れ.meteor ディレクトリについて気づかれたのではないでしょうか。Meteor自身のコードがここに格納されていますので、これに手を加えることは大体悲惨な結果を迎えることになります。事実このディレクトリは全く見る必要のないものです。唯一、使われているMeteorのバージョンと、あなたのスマートパッケージのリストでそれぞれが使われている ’.meteor/packages.meteor/release`ファイルは例外となります。パッケージ追加したりとMeteorのリリースを変えたりするときはこれらのファイルの変更を確認することは役に立ちます。

アンダーバー 対 キャメル方式

古めかしいアンダーバー(my_variable) 対 キャメル方式(myVariable)の論争について一つ言及するとしたら、常に同じ様式を取っていればどちらを使おうと全く問題ありません。

この本では、JavaScriptで通常使われている様式のキャメル方式を使っています。(ところで、表記はJavaScriptですよ、java_scriptではないですよ!).

ここでの唯一のルールは、ファイル名はアンダーバー(my_file.js)で表記し、CSSのクラスはハイフン(.my-class)で表記するという点です。理由としては、アンダーバーはファイルシステムで一番採用されている様式であり、CSSシンタックスではすでにハイフン(font-family, text-align, etc.)が取り入れられているからです。

CSSの扱いについて

この本はCSSについての本ではありません。ですので、スタイリングについて詳しく述べ、あなたの足を引き止めことを避ける為、最初から全部のスタイルシートを作成しました。

CSSはMeteorによってMinify化(ファイル圧縮)を自動でロードするようになっています。そうすれば違う他の静的アセットが /publicでなく/clientの中に行きます。それでは今、client/stylesheets/ ディレクトリをd作成して、このstyle.css ファイルを中に入れてください。

.grid-block, .main, .post, .comments li, .comment-form {
  background: #fff;
  -webkit-border-radius: 3px;
  -moz-border-radius: 3px;
  -ms-border-radius: 3px;
  -o-border-radius: 3px;
  border-radius: 3px;
  padding: 10px;
  margin-bottom: 10px;
  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
  -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); }

body {
  background: #eee;
  color: #666666; }

.navbar {
  margin-bottom: 10px; }
  /* line 32, ../sass/style.scss */
  .navbar .navbar-inner {
    -webkit-border-radius: 0px 0px 3px 3px;
    -moz-border-radius: 0px 0px 3px 3px;
    -ms-border-radius: 0px 0px 3px 3px;
    -o-border-radius: 0px 0px 3px 3px;
    border-radius: 0px 0px 3px 3px; }

#spinner {
  height: 300px; }

.post {
  /* For modern browsers */
  /* For IE 6/7 (trigger hasLayout) */
  *zoom: 1;
  position: relative;
  opacity: 1; }
  .post:before, .post:after {
    content: "";
    display: table; }
  .post:after {
    clear: both; }
  .post.invisible {
    opacity: 0; }
  .post.instant {
    -webkit-transition: none;
    -moz-transition: none;
    -o-transition: none;
    transition: none; }
  .post.animate{
    -webkit-transition: all 300ms 0ms;
    -webkit-transition-delay: ease-in;
    -moz-transition: all 300ms 0ms ease-in;
    -o-transition: all 300ms 0ms ease-in;
    transition: all 300ms 0ms ease-in; }
  .post .upvote {
    display: block;
    margin: 7px 12px 0 0;
    float: left; }
  .post .post-content {
    float: left; }
    .post .post-content h3 {
      margin: 0;
      line-height: 1.4;
      font-size: 18px; }
      .post .post-content h3 a {
        display: inline-block;
        margin-right: 5px; }
      .post .post-content h3 span {
        font-weight: normal;
        font-size: 14px;
        display: inline-block;
        color: #aaaaaa; }
    .post .post-content p {
      margin: 0; }
  .post .discuss {
    display: block;
    float: right;
    margin-top: 7px; }

.comments {
  list-style-type: none;
  margin: 0; }
  .comments li h4 {
    font-size: 16px;
    margin: 0; }
    .comments li h4 .date {
      font-size: 12px;
      font-weight: normal; }
    .comments li h4 a {
      font-size: 12px; }
  .comments li p:last-child {
    margin-bottom: 0; }

.dropdown-menu span {
  display: block;
  padding: 3px 20px;
  clear: both;
  line-height: 20px;
  color: #bbb;
  white-space: nowrap; }

.load-more {
  display: block;
  -webkit-border-radius: 3px;
  -moz-border-radius: 3px;
  -ms-border-radius: 3px;
  -o-border-radius: 3px;
  border-radius: 3px;
  background: rgba(0, 0, 0, 0.05);
  text-align: center;
  height: 60px;
  line-height: 60px;
  margin-bottom: 10px; }
  .load-more:hover {
    text-decoration: none;
    background: rgba(0, 0, 0, 0.1); }

.posts .spinner-container{
  position: relative;
  height: 100px;
}

.not-found{
  text-align: center;
}
.not-found h2{
  font-size: 60px;
  font-weight: 100;
}

@-webkit-keyframes fadeOut {
  0% {opacity: 0;}
  10% {opacity: 1;}
  90% {opacity: 1;}
  100% {opacity: 0;}
}

@keyframes fadeOut {
  0% {opacity: 0;}
  10% {opacity: 1;}
  90% {opacity: 1;}
  100% {opacity: 0;}
}

.errors{
  position: fixed;
  z-index: 10000;
  padding: 10px;
  top: 0px;
  left: 0px;
  right: 0px;
  bottom: 0px;
  pointer-events: none;
}
.alert {
          animation: fadeOut 2700ms ease-in 0s 1 forwards;
  -webkit-animation: fadeOut 2700ms ease-in 0s 1 forwards;
     -moz-animation: fadeOut 2700ms ease-in 0s 1 forwards;
  width: 250px;
  float: right;
  clear: both;
  margin-bottom: 5px;
  pointer-events: auto;
}
client/stylesheets/style.css

コミット 2-3

Re-arranged file structure.

CoffeeScriptのノート

この本では、純粋なJavaScriptを書いていきます。しかし、一方でCoffeeScriptを好むのであれば、Meteorはカバーします。CoffeeScript パッケージを追加して、準備オッケーです。:

meteor add coffeescript

Deployment

Sidebar 2.5

完璧になるまで静かにプロジェクトを進める人もいれば、待てずに出来次第世に公開する人もいます。

もしあなたが前者であれば、とりあえずローカルでの開発をするでしょう、そうであればこの章を飛ばしてもらって構いません。一方で、Meteor アプリをオンラインでデプロイするやり方を学びたいのであれば、ここでカバーできます。

いくつかの方法にてMeteorアプリをデプロイするのを学びます。Microscopeもしくは他のMeteorアプリで作業をしていても、開発中のどのタイミングでもそれそれ使っていただけます。

Introducing Sidebars

ここはサイドバーの章です。 サイドバーは、他の本の部分から離れてもっと一般的なMeteorのトピックについて深く検証していきます。

ですから、もしMicroscopeの構築を優先にしたいのであれば、今の時点ではここを飛ばして、後からここに戻ってきても全く問題ありません。

Meteorのデプロイ

Meteorのサブドメイン(i.e. http://myapp.meteor.com) をデプロイするのは一番簡単な方法で、最初に試してみたほうがいいでしょう。この方法は、ステージングサーバーを早く設定するときや、初期段階で他の人にあなたのアプリを見せる時に役立ちます。

Meteorのデプロイはとてもシンプルです。ターミナルを開けて、Meteorアプリのディレクトリを開き、以下を入力します:

$ meteor deploy myapp.meteor.com

もちろん"myapp" の部分は自分がつけたい名前に変更する必要があります。使われていない名前にしたほうが良いでしょう。

もしこれがあなたにとって最初のデプロイアプリであれば、Meteorのアカウントを早速取得してください。そして全てがうまくいったら、数秒後にhttp://myapp.meteor.com.であなたのアプリにアクセスすることができます。

ホストしているインスタンスのデーターベースに直接アクセスしたり、アプリのカスタムドメインの環境の設定をするなどの情報についてはthe official documentation をご参照ください。

モジュールのデプロイ

モジュールModulus は Node.js appsをデプロイするのに最適な選択です。すでに数多くの人がMeteorアプリの制作を その上で行っており、Meteorを公式にサポートする数少ないPaaS (platform-as-a-service) プロバイダーの中の一つです。

Demeteorizer

モジュールのオープンソースのツールでディメテオライザーdemeteorizer と呼ばれており、あなたのMeteorアプリを 標準のNode.js アプリに変換してくれます。

アカウントの作成creating an accountから始めてください。モジュール上で我々のアプリをデプロイすると、モジュールのコマンドラインツールをインストール必要が出てきます:

npm install -g modulus

そして、その時に以下と一緒に認証されます。:

modulus login

これでモジュールプロジェクトを作成できます。(モジュールのWebのダッシュボード上でも同じようにこれができることをメモしておいてください。):

modulus project create

次のステップは、私たちのアプリ用にMongoDBデーターベースを作成することです。MongoDBデーターベースをModulus itselfMongoHQ、 もしくは、他のクラウドのMongoDBのプロバイダーとも一緒に作ることができます。

私たちのMongoDBデーターベースを一度作ったら、MONGO_URL`をモジュールのWeb UIから私たちのデーターベース(Dashboard > Databases > Select your database > Administration へ行く)から取得できます。そして私たちのアプリの環境設定をするのに使ってください。このように:

modulus env set MONGO_URL "mongodb://<user>:<pass>@mongo.onmodulus.net:27017/<database_name>"

やっと私たちのアプリをデプロイする時がきました。入力するのと同じようにとても簡単です。

modulus deploy

これで、完璧にモジュールに私たちにのアプリをデプロイすることができました。ログへのアクセスや、カスタムドメインの設定、またSSLのなどについては the Modulus documentation をご参照ください。

Meteor Up

日常的に新しいクラウドのあり方が出てきますが、それらは頻繁に独自の問題や限界などと面しています。そこで、今日現在では、制作中のMeteorのアプリケーションは自分のサーバーにデプロイしておくのが最善の方法といえるでしょう。ただ一つ言えるのは、特にクオリティの高い開発を目指しているのであれば、自分でデプロイするのはそれほど簡単ではないということです。

Meteor Up (or mup for short) is another attempt at fixing that issue, with a command-line utility that takes care of setup and deployment for you. So let’s see how to deploy Microscope using Meteor Up.

Before anything else, we’ll need a server to push to. We recommend either Digital Ocean, which starts at $5 per month, or AWS, which provides Micro instances for free (you’ll quickly run into scaling problems, but if you’re just looking to play around with Meteor Up it should be enough).

Whichever service you choose, you should end up with three things: your server’s IP address, a login (usually root or ubuntu), and a password. Keep those somewhere safe, we’ll need them soon!

Initializing Meteor Up

To start out, we’ll need to install Meteor Up via npm as follows:

npm install -g mup

We’ll then create a special, separate directory that will hold our Meteor Up settings for a particular deployment. We’re using a separate directory for two reasons: first, it’s usually best to avoid including any private credentials in your Git repo, especially if you’re working on a public codebase.

Second, by using multiple separate directories, we’ll be able to manage multiple Meteor Up configurations in parallel. This will come in handy for deploying to production and staging instances, for example.

So let’s create this new directory and use it to initialize a new Meteor Up project:

mkdir ~/microscope-deploy
cd ~/microscope-deploy
mup init

Sharing with Dropbox

A great way to make sure you and your team all use the same deployment settings is to simply create your Meteor Up configuration folder inside your Dropbox, or any similar service.

Meteor Up Configuration

When initializing a new project, Meteor Up will create two files for you: mup.json and settings.json.

mup.json will hold all our deployment-related settings, while settings.json will contain all app-related settings (OAuth tokens, analytics tokens, etc.).

The next step is to configure your mup.json file. Here is the default mup.json file generated by mup init, and all you have to do is fill in the blanks:

{
  //server authentication info
  "servers": [{
    "host": "hostname",
    "username": "root",
    "password": "password"
    //or pem file (ssh based authentication)
    //"pem": "~/.ssh/id_rsa"
  }],

  //install MongoDB in the server
  "setupMongo": true,

  //location of app (local directory)
  "app": "/path/to/the/app",

  //configure environmental
  "env": {
    "ROOT_URL": "http://supersite.com"
  }
}
mup.json

Let’s walk through each of these settings.

Server Authentication

You’ll notice that Meteor Up supports password based and private key (PEM) based authentication, so it can be used with almost any cloud provider.

Important note: if you choose to use password-based authentication, make sure you’ve installed sshpass first (refer to this guide).

MongoDB Configuration

The next step is to configure a MongoDB database for your app. We recommend using MongoHQ or any other cloud MongoDB provider, since they offer professional support and better management tools.

If you’ve decided to use MongoHQ, set setupMongo as false and add the MONGO_URL environmental variable in mup.json’s env block. If you decided to host MongoDB with Meteor Up, just set setupMongo as true and Meteor Up will take care of the rest.

Meteor App Path

Since our Meteor Up configuration lives in a different directory, we’ll need to point Meteor Up back to our app using the app property. Just input your full local path, which you can get using the pwd command from the terminal when located inside your app’s directory.

Environment Variables

You can specify all of your app’s environment variables (such as ROOT_URL, MAIL_URL, MONGO_URL, etc.) inside the env block.

Setting Up and Deploying

Before we can deploy, we’ll need to set up the server so it’s ready to host Meteor apps. The magic of Meteor Up encapsulates this complex process in a single command!

mup setup

This will take a few minutes depending on the server’s performance and the network connectivity. After the setup is successful, we can finally deploy our app with:

mup deploy

This will bundle the meteor app, and deploy to the server we just set up.

Displaying Logs

Logs are pretty important and Meteor Up provides a very easy way to handle them by emulating the tail -f command. Just type:

mup logs -f

This wraps up our overview of what Meteor Up can do. For more infomation, we suggest visiting Meteor Up’s GitHub repository.

These three ways of deploying Meteor apps should be enough for most use cases. Of course, we know some of you would prefer to be in complete control and set up their Meteor server from scratch. But that’s a topic for another day… or maybe another book!

Templates

3

 Meteor での開発を簡単にするため、私たちはアウトサイドインアプローチを用いることになります。 要するに、私たちは最初にイマイチな HTML と JavaScript で外側の骨組みを作り、 それから後でアプリが内側で動くように繋いでいきます。

つまり、この章では /client ディレクトリの内側で何が起きるのかに関心を払うだけとなります。

では、/client ディレクトリの中に main.html という新しいファイルを作って、次のようなコードを書き込みましょう:

<head>
  <title>Microscope</title>
</head>
<body>
  <div class="container">
    <header class="navbar navbar-default" role="navigation">
      <div class="navbar-header">
        <a class="navbar-brand" href="/">Microscope</a>
      </div>
    </header>
    <div id="main" class="row-fluid">
      {{> postsList}}
    </div>
  </div>
</body>
client/main.html

これは アプリにメイン部分のテンプレートになります。 ご覧のように、{{> postsList}}タグ以外はすべて HTML です。 {{> postsList}} タグは、これから見ていく postsList テンプレートの挿入場所となります。 では、いくつかテンプレートを作っていきましょう。

Meteor テンプレート

基本的に、ソーシャルニュースサイトは投稿のリストによって構成されています。 私たちはまさにそのようにテンプレートを作っていきます。

では、/client の中に /templates ディレクトリを作りましょう。 私たちは /views ディレクトリの中にすべてのテンプレートを置くことになります。  /views ディレクトリ内を整理したいので、投稿に関連したテンプレート用に、/views 内に /postsディレクトリ を作ります。

ファイル検索機能

Meteor は素晴らしいことにファイルを探してくれます。 /client ディレクトリ内のどこにコードを入れようと、Meteor はコードを見つけだし確実にコンパイルします。  つまり、JavaScript や CSS にインクルードパスを記述する必要はありません。

また、同じディレクトリにすべてのファイルを置くこともできます。 さらには、同じファイルにすべてのコードを置くこともできます。

しかし、Meteor はすべてのコードを小さくした1つのファイルにコンパイルしてしまうので、 ファイル内をきちんと整理して、きれいなファイル構造にすると良いでしょう。

これから2つのテンプレートを作っていきます。  client/views/posts の中に posts_list.html を作ります

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}
  </div>
</template>
client/templates/posts/posts_list.html

次に post_item.html を作ります。

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
    </div>
  </div>
</template>
client/templates/posts/post_item.html

テンプレート要素の name="postsList" 属性に注目してください。 この name はテンプレートの場所を Meteor に把握させるために使います。 (実ファイル名 は関連しないことに注意してください)。

ここで Meteor のテンプレートシステム、Spacebars について紹介します。 Spacebars はシンプルな HTML に、3つ付け加えます: 内部テンプレート(inclusions) とブロックヘルパーです。

内部テンプレート では {{> templateName}} 構文を使い、  構文記述箇所を同じ名前(今回は postItem)のテンプレートと置き換えるよう Meteor に指示します。

{{title}}と記述し、カレントオブジェクトのプロパティを呼び出すか、  テンプレートマージャーで定義されている後述するテンプレートヘルパーの値を返します。

最後に、ブロックヘルパーはテンプレートのフローをコントロールする特別なタグで、 {{#each}}…{{/each}}{{#if}}…{{/if}} のように使います。

他の機能について

Spacebars について更に学びたい場合は、Spacebars ドキュメンテーションを参照ください。

この知識が備わると、ここでどんなことが起こっているのか簡単に理解することができます

 最初に postsList テンプレートについてですが、{{#each}}…{{/each}} ブロックヘルパーを使い posts オブジェクトに繰り返し処理を行っています。 繰り返し毎に、postItem テンプレートをインクルードしています。

あれ?posts オブジェクトはどこからやって来たんだろう?  いい質問です。 それは実際のところ、テンプレートヘルパー からやって来ており、動的に書き換わる値と考えて貰えばいいかと思います。

postItem テンプレート自体は簡単です。三つの式を使っています。: {{url}}{{title}}は 文書のプロパティを返し, {{domain}} はテンプレートヘルパーを呼び出します。

テンプレートヘルパー

ここまで私たちは Spacebars について取り組みました。 Spacebars はいくつかのタグを散りばめられた HTML です。 PHP のような他の言語と違って(あるいは、JavaScript が含まれた通常の HTML ページでさえ)、 Meteor はテンプレートと他のロジックを分離させます。 テンプレート自体が分離させるわけではありません。

テンプレートをうまく使うには、ヘルパーが必要です。 このヘルパーは料理ををウェイター(テンプレート)に渡す前に、生の食材(データ)をとってきて、調理をするシェフのようなものと見なすことができます。

言い換えると、テンプレートの役割は変数を表示をすることや変数をループすることに限定されますが、 ヘルパーはそれぞれの変数に値を割り当てる役割を果たしています。

コントローラー?

テンプレートヘルパーを含むファイルは、一種のコントローラとしての考えることはできるかもしれません。 しかし、あいまいな言い方ですが(少なくてもMVCにおける)コントローラとは、 わずかに異なる役割を持っています。

ですので我々は用語による分類をやめることに決めました。 このテンプレートに添えるjavaScriptコードについて話す場合は、 単純に、「テンプレートのヘルパー」もしくは「テンプレートのロジック」ということです。

物事をシンプルに保つために、我々は、テンプレートの後に ヘルパーを含むファイル命名規則を採用しますが(訳間違い?命名規則が違うような) .jsの拡張子を持ちます。それでは、/client/templates/posts内にposts_list.jsを作成してみましょう。 最初のヘルパーを作成しましょう。

var postsData = [
  {
    title: 'Introducing Telescope',
    author: 'Sacha Greif',
    url: 'http://sachagreif.com/introducing-telescope/'
  },
  {
    title: 'Meteor',
    author: 'Tom Coleman',
    url: 'http://meteor.com'
  },
  {
    title: 'The Meteor Book',
    author: 'Tom Coleman',
    url: 'http://themeteorbook.com'
  }
];
Template.postsList.helpers({
  posts: postsData
});
client/templates/posts/posts_list.js

正しくできたら、ブラウザでは次のように表示されているでしょう。

Our first templates with static data
Our first templates with static data

私たちはここで2つのことをしています。 まず1つめに postsData 配列の中でダミーの試作データを設定しています。 通常、データはデータベースからやって来るのですが、 この点は次の章で学びますので、今は静的データを使って「ごまかして」います。

2つめに、私たち Meteorの Template.postsList.helpers()関数を使って、 postsData 配列を返すだけの posts を呼び出すテンプレートヘルパーに定義しています。

覚えているでしょうか? postsList テンプレートで使うpostsです。 ~~~html ~~~

client/templates/posts/posts_list.html

postsヘルパーを定義すると、私たちのテンプレートはpostItemテンプレートに内に含まれる各オブジェクトをpostsData配列を反復処理して 渡すことができるようになりますので、このテンプレートは、 使用可能になったことを意味します。

コミット 3-1

Added basic posts list template and static data.

domain ヘルパー

同様に、postItemテンプレートのロジックを保持するために、 post_item.jsを作成します:

Template.postItem.helpers({
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  }
});
client/templates/posts/post_item.js

This time our domain helper’s value is not an array, but an anonymous function. This pattern is much more common (and more useful) compared to our previous simplified dummy data example.

Displaying domains for each links.
Displaying domains for each links.

domain ヘルパー はURLを取得し、JavaScriptマジックを使って、 ドメイン名を返します。しかしどうやってurlからドメイン部分を取得するんでしょう?

その答えはposts_list.htmlテンプレートにあります。 {{#each}} ブロックヘルパーは配列から繰り返し処理だけではなく、 ブロック内部のthisに反復オブジェクトを設定するという処理も行っていたのです。

つまり{{#each}}タグの間で投稿情報を順次 this に 割り当てていたのです。 このテンプレートヘルパー(post_item.js)が含まれることで、 できることを拡張してきます。

今まさになぜ、this.urlが投稿情報のURLをちゃんと返せるか。 それ以上に、post_item.html テンプレート内で、{{title}} , {{url}}を使ってthis.titlethis.urlを返すことで、 正しい値が返るかが理解できたかと思います。

コミット 3-2

Setup a `domain` helper on the `postItem`.

JavaScript マジック

Meter固有の方法ではありません。 このjavaScriptマジックを簡単に説明すると、 まず、メモリ上にHTML要素である空のアンカー (a 要素) を作成します。

href属性に投稿情報のURLを渡します。 (先ほど示したようにthisにはオブジェクトが正しく設定されています)

最後にa 要素には hostnameプロパティを使ってURLから ドメイン名だけを取得します。

ここまで理解ができたら、ブラウザで 投稿 のリストを見ることができるでしょう。 このリストは単なる静的データなので、まだ Meteor のリアルタイムに関する機能を使っていません。 次の章ではデータを変化させる方法を見て行きましょう!

Hot Code リロード

ファイル変更の際に手動でブラウザをリロードをする必要がないことに お気づきでしょうか。

これは Meteor がプロジェクトディレクトリ内のすべてのファイルを読み込んで、  変更点を見つけると自動的にブラウザを再読み込みするためです。

Meteor のホットコードリロードはとてもスマートで、コードとアプリの2状態をリフレッシュします!

Using Git & GitHub

Sidebar 3.5

GitHub は Git というバージョン管理システムを基にした、オープンソースプロジェクト向けのソーシャルリポジトリです。(???) GitHub の基本的な役割は、コードを共有してプロジェクトのコラボレーションをしやすくすることです。 また、GitHub は素晴らしい学習ツールでもあります。 この補足事項では、Discover Meteor を理解する上での GitHub の使い方を学びます。

この補足事項では、Git や GitHub について知らない読者の方を想定しています。 もしあなたがどちらも使いこなせるのなら、この章まで飛ばしても大丈夫です。

コミットをする

git リポジトリの基本的なワーキングブロック(???)は、コミットです。 コミットとは、一定時間のコード(???)の状態のスナップショットと考えることができます。

Microscope の完成したコードを(give?) の代わりに あらゆる面でスナップショットを撮って、オンラインの GitHub 上で見ることができます。(???)

たとえば、前章でのコミットは次のようになります:

A Git commit as shown on GitHub.
A Git commit as shown on GitHub.

ここで見ているのは、postitem.js ファイルの “diff”(“difference”) です。 これはコミットによって取り込まれた変化です。 この場合、ゼロから postitem.js ファイルを作ったため、背景が緑色に表示されています。

後々本書で出てくる例を見比べてみましょう

Modifying code.
Modifying code.

今回は修正した行の背景だけが緑色になっています。

もちろん、コードを加えたり修正するだけでなく、コードを削除することもあります。

Deleting code.
Deleting code.

これが GitHub の最初の使い方です。何か変更されたのか、一目瞭然です。

コミットされたコードを見る

Git のコミット表示は このコミットに(include?)された変化を表示していますが、  変化していないファイルを見たいという時は、  コードが(at this stage of the process?)で(???)

またしても GitHub が(comes through?)します。  コミットページで、Browse code ボタンをクリックしましょう。

The Browse code button.
The Browse code button.

これで(specific?=特定の?)コミットを示すリポジトリにアクセスしました。 

The repository at commit 3-2.
The repository at commit 3-2.

GitHubは私たちがGitHubを見ている時に、多くの視覚的なヒントを伝えません。 しかし、「普通」の(master view?)を比較して、ファイル構造が違っていることが一目でわかります。

The repository at commit 14-2.
The repository at commit 14-2.

ローカルコミットへのアクセス

これまでオンラインの GitHub で、どのようにコミットされたコードを見るのか学習しました。 一方で、ローカル環境で同じことしたいときはどうしたらよいのでしょうか? たとえば、現在のプロセスで想定通りにアプリが動くかどうか確かめるための特定のコミットで、ローカル環境でアプリを動かしたいといましょう。 (???)

これをするために、git コマンドライン(utility?)を使って本書での最初の一歩を進みましょう。  まず第一に、Git がインストールされているか確認します。 それから次のようにして Microscope リポジトリをクローン(言い換えると、ローカルにコピーをダウンロード)します。   ~~~bash $ git clone git@github.com:DiscoverMeteor/Microscope.git github_microscope ~~~

この github_microscope は(at the end?)では、アプリをクローンして入れておくローカルディレクトリの名前です。  すでに microscope ディレクトリが存在している(Assuming?=場合は?)、 他の名前を(pick?=選びます?)(GitHub リポジトリと同じ名前を使う必要はありません)。   git コマンドライン(utility?)を使い始めるために、リポジトリ(into?)cd しましょう。

$ cd github_microscope

私たちは GitHub からリポジトリをクローンしたので、 アプリのすべてのコードをダウンロードしました。つまり、私たちは( last ever?=最後に?)コミットされたコードを見ています。

ありがたいことに、( other ones?)に影響を及ぼさずに 時間の流れをさかのぼって 特定のコミットを(“check out” ?)する方法があります。では、試してみましょう。

$ git checkout chapter3-1
Note: checking out 'chapter3-1'.

あなたは'detached HEAD'状態になっています。
 周りを見渡して、実験的に変化させて、(them?)をコミットします。
 すると、この状態でどのようなコミットも
 他の checkout をすることで、どのようなブランチにも 影響を与えずに、
 捨てることができます。

コミットを保持するために新しいブランチoこを作りたいという場合、
 再び checkout コマンドで -b を使って行います。

  git checkout -b new_branch_name

HEAD is now at a004b56... Added basic posts list template and static data.

私たちは Git によって(“detached HEAD”?)状態を知ることができます。 Gitが関係する限り、私たちは過去のコミットを見れるだけで、修正することはできません。 これは水晶の玉で過去を調べる魔法使いのようなものだと考えることができます。

(Git には 過去のコミットを変えるコマンドがあります。   これは時間の流れをさかのぼって、蝶を踏みつけるタイムトラベラーのようなものです。   しかし、この点はこの短い紹介の範囲を超えてしまいます。)

なぜ(chapter3-1?)をタイピングできるのかというと、  私たちは(correct chapter marker?)で Microscope のすべてのコミットを( pre-tagged?)したからです。  (this weren’t the case?)、最初にコミットのハッシュか(unique identifier?)を見つける必要があります。 

またしても、 GitHub は私たちの生活を(???)しやすくしてくれました。  (as shown here?=この図のように?)、青いコミットヘッダーボックスの右下の隅にコミットハッシュを見つけることができます。  

Finding a commit hash.
Finding a commit hash.

タグの代わりに ハッシュを使ってみましょう。

$ git checkout c7af59e425cd4e17c20cf99e51c8cd78f82c9932
Previous HEAD position was a004b56... Added basic posts list template and static data.
HEAD is now at c7af59e... Augmented the postsList route to take a limit

最後に、水晶の玉を見ることをやめたくなって、現在に戻りたくなったらどうするのでしょうか?  私たちは Git に(master branch?)をチェックしたいと伝えます。

$ git checkout master

Historical Perspective

これはもうひとつのよくある状況です: あなたがファイルを見ていると、今まで見たことのない変化に気づきました。  (The thing is,?=要するに?)、あなたはいつファイルを変更させたか覚えていません。  (right one?)を見つけるまで、一つ一つコミットを見ていく (???)  しかし、 GitHub の History 機能でもっと簡単にする方法があります。

最初に、GitHub でリポジトリの ファイルにアクセスして、“History” ボタンを見つけます。

GitHub's History button.
GitHub’s History button.

このファイルに影響を与えたすべてのコミットを整ったリストで(have?)します。

Displaying a file's history.
Displaying a file’s history.

The Blame Game

締めくくりに、Blame を見ていきましょう。  

GitHub's Blame button.
GitHub’s Blame button.

(neat view?)は 誰がファイルとコミットを変更したのか行ごとに、表示します。  (言い換えると、うまくいかなくなった時に誰のせいかということがわかります。)  

GitHub's Blame view.
GitHub’s Blame view.

Git と GitHub はかなり複雑なツールです。そのため、1つの章ですべてのことをカバーすることは見込めません。  実際のところ、これで私たちは やっと Git と GitHub でできることを少しだけ学ぶことができました。 (???)  とはいえ、本書の残りを理解していく上でいくらか役立つとわかるでしょう。(???)

Collections

4

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.

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

結論

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

パブリケーションとサブスクリプション

Sidebar 4.5

パブリケーションとサブスクリプションは Meteor において、最も基本的で重要なコンセプトの1つです。 しかし、まだ始めたばかりでは理解することが難しいものです。

そのため多くの誤解の原因ともなっています。 その誤解の中には Meteorは安全でないといったものやMeteorのアプリは大量のデータを扱えないといたものがあります。

最初にこうしたコンセプトに困惑してしまう理由の多くは、Meteor がもたらす「マジック」によるものです。 Meteor のマジックは最終的にはとても役立つものですが、裏側で何をしているのかわかりません。(マジックってそういうものですからね) 何か起きているのか理解するために、化けの皮を剥ぎ取りましょう。

昔々

しかしはじめに、 Meteor が存在しなかった2011年の古き良き時代を振り返ってみましょう。 例えば、あなたが Rails でアプリを作っていたとしましょう。 ユーザーがサイトにやってくると、クライアントでは(つまり、あなたのブラウザーでは)、リクエストをサーバーにあるアプリに送ります。

このアプリの最初の仕事はユーザーが見る必要のあるデータが何であるか理解することです。 これは検索結果の12ページであったり、メアリーのユーザープロフィール情報や、ボブの最近の20個のツイートといったものかもしれません。 これは基本的にあなた要求した本を探すために、本屋の店員さんが通路を歩いて見て回ることのように考えることができます。

正しいデータを選び出したら、アプリの2つ目の仕事はデータを良いもの変換します。 例えば、人間が読むことのできるHTML等です。(APIの場合はJSONに変換します。)

本屋でたとえると、これは購入した本をラッピングしてバックの中にしまうことです。 これが有名な Model-View-Controller モデルでの"View"の部分です。

最終的に、アプリは HTML コードを取ってブラウザーへ送信します。 これで Rails で作られたアプリの仕事は終わって、すべてのことが手元から離れました。 そのため、Railsで作られたアプリは次のリクエストが来るまで待っている間にビールを飲んでくつろぐことができます。

Meteorのやり方

Meteor がなぜ特別なのか比較して おさらいをしましょう。 これまで見てきたように、Meteorの鍵となるイノベーションはRailsのアプリがサーバーでのみ動いているのに対して、 Meteorアプリはクライアント(ブラウザー)でも動くクライアントサイド(ブラウザで動く)のコンポーネントがる点です。

データベースの一部分をクライアントにプッシュする
データベースの一部分をクライアントにプッシュする

これはまるで本屋の店員さんがあなたの欲しい本を見つけるだけでなく、家までついて来て夜中に読んでくれるようなものです。(ゾッとする話ですけどね。)

この構造はMeteorをクールなものにしています。本屋のチーフのように、 Meteorがデータベースをどこからでも呼び出すのです。 簡単に言うと、Meteorはあなたのデータベースから一部分を取ってきて、それをクライアントにコピーをします

これには2つの大きな意味合いがあります。 1つ目は、クライントにHTMLコードを送る代わりに、 Meteorアプリが実際の生データを送って、クライアントが対処するようにするという点。 (data on the wire) 2つ目は、サーバーからの応答を待つことなくデータのアクセスや編集さえ瞬時にできてしまう点です。 (latency compensation)

パブリッシュ

アプリのデータベースはプライベートから機密データに至る何万ものドキュメントを入れることができます。 そのため、セキュリティやスケーラビリティの観点からもクライント上のデータベースへすべてをミラーするわけにはいきません。

どの部分のデータをクライントに送るかMeteorに命令する方法が必要で、パブリケーションを通してこれを実現します。

Microscopeにもどりましょう。これがデータベースに入っているアプリの投稿のすべてです。:

データベース内のすべての投稿
データベース内のすべての投稿

Microscopeにはそのような機能はないのですが、罵詈雑言にフラグを立てることを想定します。 データベースでは、それらを維持したいですが、ユーザーに提供(すなわちクライアントに送信)すべきではありません。

私たちの最初のタスクは クライントにどんなデータを送りたいのかMeteorに指示を与えることです。 フラグが立っていない投稿だけをパブリッシュしたいということをMeteorに指示します。

フラグがついた投稿を除く
フラグがついた投稿を除く

これが対応するコードで、サーバー内にあります。:

// on the server
Meteor.publish('posts', function() {
  return Posts.find({flagged: false});
});

こうすることで、クライアントがフラグ付き投稿にアクセス可能な方法がないことが保証されます。 まさにこれがMeteorアプリをセキュアにする方法です: 単にクライアントにアクセスしてほしいデータをパブリッシュするだけです。

DDP

基本的に、パブリケーション/サブスクリプション システムについては サーバーサイドのコレクション(ソース)をクライアントサイドのコレクション(ターゲット)に、 データを転送するじょうごのようなものだと考えることができます。

じょうご上で話しているプロトコルはDDPと呼ばれます。(Distributed Data Protocolの略) DDPについてさらに学習するには、Matt DeBergalis(Meteor発起人の一人)によるリアルタイムカンファレンスか、Cris Matherのスクリーンキャストで、もう少し詳細にこの概念を説明されています。

サブスクライブ

フラグの立っていないすべての投稿をクライアントでも閲覧できるようにしたいとしても、 一度に幾千もの投稿をすぐに送ることはできません。

私たちは、クライアントが任意の特定の瞬間に必要なデータがどの部分かを指定するための方法を必要とし、 それはまさにサブスクリプションの出番です。

サブスクライブしているすべてのデータはMinimongoによってクライアント上でミラーされます。 MinimongoはMongoDBのクライアントサイド実装です。

たとえば、私たちはボブ・スミスのプロフィールのページを見ているとしましょう。 そして、彼の投稿だけを表示したいとします。

クライアント上でボブの投稿だけをミラーするようにサブスクライブする
クライアント上でボブの投稿だけをミラーするようにサブスクライブする

最初に、引数をを取るためにパブリケーションを改造します。:

// on the server
Meteor.publish('posts', function(author) {
  return Posts.find({flagged: false, author: author});
});

そして、アプリのクライアント側のコードでそのパブリケーションに サブスクライブする時にそのパラメータを定義します: アプリのクライアントサイドのコード内でパブリケーションにサブスクライブするときに引数を定義します。

// on the client
Meteor.subscribe('posts', 'bob-smith');

これがMeteorアプリをスケーラブルなクライアントサイドにする方法です。: すべての利用可能なデータをサブスクライブする代わりに、ちょうど、今必要な部分を選択し、 ピックアップします。このようして、サーバー側のデータベースはどんなに大きくとも関係なく、ブラウザのメモリの過負荷を避けることができます。

Finding

ボブの投稿は多数のカテゴリーに散在しています。(たとえば、“JavaScript”や ”Ruby”、”Python”など) まだ、メモリ上のボブの全ての投稿を読み込みたいかもしれませんが、 今現在は“JavaScript"カテテゴリーでの投稿だけを表示したいとします。ここから“finding”が登場です。

クライアント上のドキュメントの一部を選択
クライアント上のドキュメントの一部を選択

サーバーで行ったのと同様に、 データの一部分を選択するためPosts.find()関数を使いましょう。:

// on the client
Template.posts.helpers({
  posts: function(){
    return Posts.find({author: 'bob-smith', category: 'JavaScript'});
  }
});

今や、パブリケーションとサブスクリプションの役割が何であるか把握しているので、より深く掘り下げて、 共通する実行パターンを確認しましょう。

Autopublish

Meteorのプロジェクトをゼロから(つまり、meteor createを使って)作っているとしたら、 自動的にautopublishパッケージが有効となっています。 出発点として、autopublishが正確に何をしているのか、お話しましょう。

autopublishの目標は、あなたのMeteorアプリをコーディング始めるのが非常に簡単にすることです。 それは自動的にパブリケーションとサブスクリプションを調整し、サーバーから全データをクライアントにミラーリングすることによって行われます。

Autopublish
Autopublish

どうやって動いているのでしょうか? サーバー上に'posts'という名前のコレクションがあるとします。 autopublishは自動的にMongoDB内のpostsコレクションを見つけ全ての投稿をクライアント上で'posts'という名前のコレクションに送ります(コレクションは1つだと想定します)。

そのため、autopublishを使うなら、パブリケーションについて考える必要はありません。 データはユビキタスで、物事がシンプルです。 もちろん、すべてのユーザーのマシン上で、アプリのデータベースの完全なコピーをキャッシュするのは明白な問題があります。

こうした理由から、autopublishは開発を始めたときだけに適しています。 その時はまだパブリケーションについて検討しないで良いからです。

すべてのコレクションをパブリッシュする

一旦autopublishを削除したら、クライアントから全てのデータが消去されたことがすぐにわかるでしょう。 元に戻す簡単な方法は、autopublishが行うことを単に真似て、 コレクションを丸ごとパブリッシュすることです。例えば:

Meteor.publish('allPosts', function(){
  return Posts.find();
});
全コレクションをパブリッシュする
全コレクションをパブリッシュする

すべてのコレクションをパブリッシュしているわけですが、少なくともどのコレクションをパブリッシュするか、しないかを制御しています。 この場合は、Commentコレクションではなく、Postsコレクションをパブリッシュしています。

コレクションの一部分をパブリッシュする

次の制御レベルでは、コレクションの一部分だけをパブリッシュします。 たとえば、ある著者に関連したpostsだけなら:

Meteor.publish('somePosts', function(){
  return Posts.find({'author':'Tom'});
});
コレクションの部分パブリッシュ
コレクションの部分パブリッシュ

舞台裏

もしあなたがMeteor publication documentationを読んだなら、

おそらく、クライアント上のレコードの属性を設定するadded()ready()を使用しての話に圧倒され、 二重に苦労するでしょう。ですがMeteorアプリを作る上でそれらを使うことはありません。

その理由はMeteorはとても重要で便利なものを提供しているからです: _publishCursor()メソッド

これまでにこれが使われているのを見ましたか?おそらく直接は見ていませんが、あなたはパブリッシュ関数でカーソルを返しています。(例えばPosts.find({'author':'Tom'})) まさにこれをMeteorが使っているのです。

MeteorがsomePostsパブリケーションがカーソルを返したところを見かけたら、 _publishCursor()を呼び出して自動的にカーソルをパブリッシュするとあなたは推測します。

_publishCursor()が行うことは次のようなことです:

  • サーバーサイドのコレクションの名前をチェックします。
  • カーソルからマッチする全てのドキュメントを呼び出して、 それをクライアントサイドの同じ名前のコレクションに送ります。(これをするために.added()を使います)
  • ドキュメントが加えられたり、削除されたり、変更したときは、 クライアントサイドのコレクションにその変更をおくります。 (カーソルでは.observe()を使い、そして.observe().added().changed()removed()を使います)

そのため、上記の例では、 クライアントサイドのキャッシュでユーザーの興味のある投稿(トムが書いたもの)だけが 見れるようにすることができます。

一部のプロパティをパブリッシュする

どのように投稿の一部分だけをパブリッシュするのか見てきました。 しかしもっと薄くスライスすることができます。 どのようにして特定のプロパティだけをパブリッシュするのか見ていきましょう。

先ほどのように、 find()を使って、カーソルを返しますが、今回は特定のfieldを除外します。:

Meteor.publish('allPosts', function(){
  return Posts.find({}, {fields: {
    date: false
  }});
});
一部のプロパティをパブリッシュする
一部のプロパティをパブリッシュする

もちろん、2つのテクニックを併用することもできます。例えば、トムが著者の全投稿を日付を削りつつ返すようにしたい場合、このように書きます。:

Meteor.publish('allPosts', function(){
  return Posts.find({'author':'Tom'}, {fields: {
    date: false
  }});
});

まとめ

あらゆるコレクションの全てのドキュメントのあらゆるプロパティをパブリッシュすることから始まって、(autopublishも含めて) コレクションの一部のドキュメントの一部のプロパティの一部だけをパブリッシュする方法を見ていきました。

Meteorでのパブリケーション処理の基本をカバーしました。 このシンプルなテクニックは 大部分のケースで使えます。

時々、パブリケーションをつなげる、リンクする、マージする必要が発生するかもしれません。 こうしたことは後の章でカバーしていきます!

Routing

5

今のところ、投稿の一覧表ができたので(最終的にはユーザーが投稿するようにします)、 ユーザー同士で投稿を議論できる個別の投稿ページが必要です。

このページをパーマリンクを通じて、アクセスできるようにしたいと思います。  パーマリンクとは http://myapp.com/posts/xyz の形をした URL で、それぞれの投稿は固有のものです。( xyz の部分は MongoDB の _id 識別子です)

ブラウザーの URL バーの中に何があるのか調べてそれに対応して正しいコンテンツを表示するために、何らかのルーティングが必要となります。

Iron Router パッケージを入れる

Iron Router は Meteor アプリのルーティングに特化したパッケージです。

Iron Router はルーティング(パスの設定)に役立つだけではなく、  設定をしたパスにアクションを割り当てるフィルターも処理します。  さらに、どのパスがどんなデータにアクセスするのか制御するサブスクリプションも扱います。  (注釈:Iron Router の一部は Discover Meteor の著者の一人であるトム・コールマンによって開発されました。)

最初に、Atmosphere からパッケージをインストールしましょう。

$ mrt add iron-router
Terminal

このコマンドで iron-router パッケージをアプリにダウンロードしてインストールをしたので、すぐに使える状態となります。  パッケージを使えるようになる前に Meteor アプリを再起動する必要があるかもしれません。 (ctrl+c でプロセスを中止して、再び起動するために meteor を入力します。)

ルーティング用語集

この章では、ルーティングに関する様々な機能を学びます。  もしあなたが Rails などのフレームワークの経験があるなら、  ルーティングの概念についてすでに知っていることでしょう。  もしそうでなければ、ここでちょっとした用語集を用意しました。

  • ルート:ルートとは、ルーティングの基本的な構成要素です。     アプリが URL を見つけたら、どこに行って、何をしたらよいかといった基本的な命令がセットになったものがルートです。

  • パス:パスとはアプリ内での URL のことです。パスは静的な(/termsofservice) や動的な(/posts/xyz)、 (/search?keyword=meteor)といったクエリパラメータもあります。

  • セグメント:パスの様々なパーツのことで、前のスラッシュ(/)で区切られています。

  • フック:フックとはルーティングプロセスの前や後、あるいはルーティングプロセス中に実行させるアクションです。      代表的な例として、ページを表示する前にユーザーが適切な権利を持っているか照合することが挙げられます。

  • フィルター:フィルターとは、一つ以上のルートをグローバルに定義するフックです。  

  • ルートテンプレート: 各ルートはテンプレートを指し示す必要があります。  テンプレートを指定していない場合、ルーターはデフォルトでルートと同じ名前のテンプレートを探します。

  • レイアウト:レイアウトとは、デジタルフォトフレームのようなものだと捉えることができます。  レイアウトはカレントテンプレートをラップするすべての HTML コードを組み入れ、  テンプレートが変化しても同じ状態を保ちます。  

  • コントローラ:時々、多くのテンプレートが同じパラメータで再利用できることに気づくでしょう。  コードを重複させるのではなく、ルーティングロジックをすべて含んだ1つのルーティングコントローラで  ルートを継承することができます。

Iron Router に関する詳細は、GitHub 上のドキュメンテーションで全文を見ることができます。

Routing: テンプレートに URL を割り当てる

今まで、{{>postsList}} のようにハードコードされたテンプレートのインクルードを使ってレイアウトを作ってきました。 そのため、アプリの内容は変化できますが、ページの基本的な構造は常に同じです: つまり、ヘッダーと投稿の一覧表です。

Iron Router が HTML の タグの中をレンダリングしたものを引き継ぐことで、私たちはこの骨組みから抜け出すことができます。  そのため、私たちは通常の HTML ページのようにタグの中を自ら定義をしません。  その代わりに、{{> yield}} テンプレートヘルパーを持つ特別なレイアウトテンプレートをルーターに指定します。

この {{> yield}} ヘルパーは特別な動的な範囲を定義します。  動的な部分ではカレントルートに対応するあらゆるテンプレートに自動的にレンダリングします。 (私たちは今後からこの特別なテンプレートを「ルートテンプレート」と指定します。)

Layouts and templates.
Layouts and templates.

ではレイアウトを作って {{> yield} } ヘルパーを追加してみましょう。 はじめに、main.html から HTML の タグの中身を layout.htmlの中のテンプレートに移動させます。

すると、スリムになった main.html は現在このようになっています。

<head>
  <title>Microscope</title>
</head>
client/main.html

新しく作った layout.html は現在、アプリの外側のレイアウトを組み入れています。

<template name="layout">
  <div class="container">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="brand" href="/">Microscope</a>
    </div>
  </header>
  <div id="main" class="row-fluid">
    {{yield}}
  </div>
  </div>
</template>
client/views/application/layout.html

postsList テンプレートを yield ヘルパーの呼び出しに差し替えたことに気づいたことでしょう。  また、その変更後はスクリーン上になにもないことにお気づきかと思います。  これはまだルーターに / URL で何を処理するか指定していないためです。  そのため、(it=ルーター?)は空っぽのテンプレートを出力しています。

はじめに、postsList テンプレートに/ URL ルートを割り当てて、以前と同じ状況にします。。  ルートのプロジェクトで /lib directory を作って、その中に router.js を作ります。

Router.configure({
  layoutTemplate: 'layout'
});

Router.map(function() {
  this.route('postsList', {path: '/'});
});
lib/router.js

これで2つの大事なことを行いました。  1つ目は、私たちが作ったデフォルトのレイアウトをすべてのルートで使うことをルーターに指定しました。  2つ目は、postsList という名前の新しいルートを定義して、/ パスを割り当てました。

The /lib folder

/lib フォルダ内に置いたものは、何よりも前に最初に読み込まれることが保証されています。 (スマートパッケージは例外です) そのため、/lib フォルダはヘルパーコードを置く場所として最適です。ヘルパーコードは常に有効とする必要があるためです。

しかし、注意点が1つあります。 /lib フォルダは /client 、/server 内のどちらにもないので、 /lib フォルダの中のコードはどちらの環境でも有効化されるということに気をつけましょう。

Named Routes

ここで少しあいまいな点をクリアにしましょう。  私たちはルートに postsList と名付けましたが、postsList という名前のテンプレートもあります。  すると、どんなことが起こるでしょうか。

デフォルト状態だと、 Iron Router はルートと同じ名前のテンプレートを探します。  実際には、ルートの名前に基づいてパスを探します。  ルートの定義にパスのオプションを与えたカスタムパスを定義していない場合、  テンプレートはデフォルトで /postsList の URL でアクセスできるということです。

そもそも、なぜでルートに名前をつける必要があるのでしょうか。  それはルートに名前を付けることで、簡単にアプリ内のリンクを作る Iron Router の機能を使うためです。   最も役立つ機能は {{pathFor}} という Spacebars ヘルパーです。これはあらゆるルートの URL パスの構成要素を返します。

メインホームリンクが投稿のリストを返すようにしたいので、 静的な / URL を指定する代わりに、Spacebars ヘルパーを使います。 最終的な結果は同じとなりますが、ルーターでルートのパスを変えたとしてもヘルパーは常に正しい URL を出力するため、 私たちはフレキシビリティを得ることができます。

<header class="navbar">
  <div class="navbar-inner">
    <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
  </div>
</header>

//...
client/views/application/layout.html

コミット 5-1

Very basic routing.

Waiting on Data

もし現在の状態のアプリをデプロイしたら(あるいは上記のリンクを使ってインスタンスをローンチしたら)、  投稿が現れる前のわずかな間はリストの中身がないことに気づくことだと思います。  これはページが最初に読み込まれるときは posts サブスクリプションがサーバーから投稿データを取ってくるまでの間に表示する投稿がないためです。

ユーザーに何が起きているのかという視覚的なフィードバックを与えることができれば、よりよいユーザーエクスペリエンスとなって、ユーザーは少しの間待ってくれることでしょう。

幸運なことに、Iron Router にはそうするための簡単な方法があります。 waitOn サブスクリプションです。

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.map(function() {
  this.route('postsList', {path: '/'});
});
lib/router.js

ひとつひとつ見ていきましょう。 最初に、loading template(これからすぐに作ります。)の名前をルーターに与えるために、私たちは Router.configure() ブロックを修正しました。 loading template はアプリがデータを待っている間にリダイレクトを行います。

次に、posts サブスクリプションを返す waitOn関数を加えました。 最後に、私たちはローディングフックを作りました。 これはユーザーがリクエストしたルートにユーザーを送る前に、 ルーターが posts サブスクリプションを読み込むようにします。

私たちは waitOn関数をルーターレベルでグローバルに定義しているので、 このシーケンスはユーザーが初めてアプリにアクセスした際に一度だけ起こります。 その後、このデータはブラウザーのメモリにすでに読み込まれ、ルーターは再び待つ必要がなくなります。

これでルーターがサブスクリプションを処理するようにしたので、 main.js のコードを安全に削除することができます。(main.js は現在何も入っていない状態です。)

サブスクリプションを待つことは良いアイデアです。それは単にユーザーエクスペリエンスのためだけではなく、データが常にテンプレート内から利用できると問題なく想定できるからです。 基礎的な情報を利用する前に、レンダリングしたテンプレートを処理する必要がなくなります。 そうするにはたいて巧妙な回避方法が必要です。

また、onBeforeAction フィルターを加えて、Iron Router に組み込まれた loading フックを動作させます。 そして、データを待っている間に loading テンプレートを表示するようにします。

<template name="loading">
  {{>spinner}}
</template>
client/views/includes/loading.html

{{>spinner}} は spin パッケージに含まれている部分です。 この部分はアプリの「外側」から来ているわけですが、通常のテンプレートと同様にインクルードすることができます。

コミット 5-2

Wait on the post subscription.

A First Glance At Reactivity

リアクティビリティは Meteor の根幹をなす部分ですが、これまで触れてきませんでした。ローディングテンプレートでこのコンセプトを垣間見ることができます。

データがロードされていないかどうかローディングテンプレートにリダイレクトすることは良いことですが、ユーザーを正しいページにリダイレクトする時をルーターはどうして分かるのでしょうか。(???) 

今のところはとりあえずリアクティビリティがその役割をしているとだけ言っておきましょう。 このことについてはこの後すぐに学びますので、心配ご無用です!

 特定の投稿にルーティングする

ここまでで、どのように postsList テンプレートにルーティングするのか見てきました。 ここでは1つ1つの投稿の詳細を表示するためのルートをセットアップしましょう。

(There’s just one catch?):1つの投稿ごとにルートの定義をするとなると、数百の投稿に対して同じことをすることになり、先に進めなくなってしまいます。 そのため、1つの動的なルートをセットアップして、ルートに私たちが見たい投稿を表示させる必要があります。

まず初めに、テンプレートを単純にレンダリングするだけの新しいテンプレートを作ります。これは以前、私たちが投稿リストで使ったテンプレートと同じです。

<template name="postPage">
  {{> postItem}}
</template>
client/views/posts/post_page.html

後でこのテンプレートにはコメントなどの要素を入れていきますが、今のところ {{> postItem}} インクルードのシェルのような役目を果たしています。

もう一つ指定したルートを作っていきましょう。 今回は、/posts/ の形で URL パスを postPage テンプレートに割り当てます。

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id'
  });
});

lib/router.js

この特別な :_id 構文はルーターに2つのことを指定します: 1つ目は、/posts/xyz/ の形のルートをマッチさせます。/posts/xyz/ の “xyz” はどんなものでも問題ありません。 2つ目は、ルーターの params配列で _id プロパティ内の “xyz” 地点を見つけたものはどんなものでも中に入れます。

ここでは便宜上のために _id だけを使っていることに留意してください。 このルーターはあなたが実際の _id を渡しているのか、ランダムの文字列を渡しているのか判断することはできません。

私たちは今、正しいテンプレートにルーティングしていますが、まだ何か見逃しています: ルーターは私たちが表示したい投稿の _id を識別しますが、テンプレートはまだ何もわからない状態です。 では、どのようにしてこのギャップを埋めるのでしょうか。

ありがたいことに、Iron Router には解決策が組み込まれています: Iron Router は、テンプレートのデータコンテキストを指定するのです。 これはテンプレートとレイアウトで作られたおいしいケーキの中をデータコンテキストが満たしていると考えることができます。 簡単に言うと、 テンプレートの中をいっぱいに満たしているものがデータコンテキストです。

The data context.
The data context.

この場合、私たちは URL から得た _id を基にした投稿を探して正しいデータコンテキストを取得しています。

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });
});

lib/router.js

そのため、ユーザーがこのルートにアクセスするたびに、 私たちは適切な投稿を見つけて、その情報をテンプレートに引き渡します。 findOne がクエリにマッチした1つの投稿を返して、引数としての id を {_id: id} と略した表現であることを覚えておきましょう。

ルートの data関数内で、this は現在マッチしているルートに対応しています。 私たちは指定したルート部分にアクセスするために this.params を使うことができます。 (そのことを、私たちは path の中で:を前に置くことで表現しています。)(???)

More About Data Contexts

テンプレートのデータコンテキストを設定することで、テンプレートヘルパー内の this の値をコントロールすることができます。

これは {{#each}} イテレータで非明示的に処理されます。 {{#each}} イテレータは 自動的に現在繰り返し処理されるアイテムそれぞれに繰り返し処理のデータコンテキストを設定します。  

{{#each widgets}}
  {{> widgetItem}}
{{/each}}

しかし、{{#with}} を使うことで同じことを明示的に行うことができます。 {{#with}} は単純に「このオブジェクトを取ってきて、次のテンプレートにそれを適用しろ」と示しています。 例えば、このように書くことができます。

{{#with myWidget}}
  {{> widgetPage}}
{{/with}}

テンプレートの呼び出しに、コンテキストを引数として引き渡すことで同じ結果を得ることができることがわかります。  そのため先ほどのコードのブロックはこのように書きなおすことができます:

{{> widgetPage myWidget}}

動的に指定したルートヘルパーを使う

最終的に、私たちは個別の投稿にリンクをしたい時は常に正しい場所を指し示す必要があります。 再び、私たちは とすることができますが、ルートヘルパーを使った方がより確実です。

私たちは投稿ルートを postPage と指定したので、{{pathFor ‘postPage’}} ヘルパーを使います。

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

でも待ってください。 /posts/xyz の中の xyz 部分をどこでルーターが取得するのかどれだけ正確に判断するのでしょうか。 結局、私たちはルーターに一つも _id を引き渡していません。

結局のところ、Iron Router は自分自身で id を判断できるほどスマートだということです。 Iron Router に postPage ルートを使うように指示すると、 Iron Router はルートがどのid を要求するのか識別します。(こういった理由で私たちはpath を定義しました。)

そのため、ルーターは最もロジカルな場所で利用できる _id を探します: {{pathFor 'postPage’}} ヘルパーのデータコンテキスト、つまりは this です。 図らずも this は個別の投稿に対応するので、(驚くことに!) _id プロパティを内包しています。

この代わりに、ヘルパーに2つの引数を渡すことで(つまり、{{pathFor 'postPage’ someOtherPost}}として)、 ルーターがどこで _id プ ロパティを探したいかを明示的に指定することもできます。 たとえば、このパターンの実用的な使い方は投稿リストの前や次のリンクを取得することでしょう。

正しく動作しているか確かめるために、  投稿リストをブラウザで開いて、'Discuss’ ボタンをクリックしましょう。  きっとこのように表示されるはずです。

A single post page.
A single post page.

HTML5 pushState

もう一つ理解することは、URL の変化が HTML5 の pushState を使うことで起こっていということです。

Iron Router はアプリの状態に必要な変化をさせるだけでなく、 サイト内の URL クリックを取得して、 ブラウザーがアプリから(browsing away ?)することを防ぎます。

もしすべてが正しく動いているなら、ページは瞬時に変化します。 実際には、変化するのが早過ぎるため、ある種のページ移行が必要になります。 この点はこの章では範囲外ですが、興味深いトピックです。

The Session

Sidebar 5.5

Meteorはリアクティブなフレームワークです。 つまり、データが変化すると、何もしなくても確実にアプリケーション内のデータも変化するということです。 (???)

私たちはすでに作業を 見てきました。  どのようにテンプレートが変化するのか (??????)

これがどのように動いているのかは後の章でじっくり見ていきます。 ここでは、私たちは一般的なアプリでとても役立つ 基本的なリアクティブの特徴を 紹介したいと思います。 

The Meteor Session

現在のMicroscopeでは ユーザーのアプリケーションの現在の状態は(???) ユーザーが見ているURLが含まれています。(??データベース)

しかし、多くの場合、 一時的な状態を保存する必要があります。  現在のユーザーの アプリケーションの(???)にだけ関係しています。 (たとえば、要素が現れるか隠れるかどうか)。 セッションはこうするための便利な方法です。

セッションはグローバルのリアクティブデータストアです。(???) グローバルシングルトンオブジェクトという意味のグローバルです。(???)1つのセッションはどこからでもアクセスできます。 グローバル変数はよく悪いものだと見られますが、この場合セッションはアプリケーションの違う部分への中央 コミュニケーションバスとして使われます。(???)

Changing the Session

セッションは、Sessionでどこからでも使うことができます。セッションの値をセットするには、次のようにやります。(???)

 Session.set('pageTitle', 'A different title');
Browser console

Session.get(‘mySessionProperty’);を使うことで、(???)  データを読むことができます。 これはリアクティブなデータソースです。 つまり、ヘルパー内にこれを入れると セッション変数が変化した時に 反動的に ヘルパーの出力が変化するという意味です。(???)

これを試してみるために、レイアウトテンプレートに次のようなコードを追加しましょう。

<header class="navbar">
  <div class="navbar-inner">
    <a class="brand" href="{{pathFor 'postsList'}}">{{pageTitle}}</a>
  </div>
</header>
client/views/application/layout.html
Template.layout.helpers({
  pageTitle: function() { return Session.get('pageTitle'); }
});
client/views/application/layout.js

Meteorの自動リロード(つまり「ホットコードリロード」)はセッション変数を保存します。(???) そのため、私たちは今「違ったタイトル」がナビゲーションバーに表示されているのがわかると思います。 そうでなかったら、もう一度先ほどのSession.set()コマンドを入力してください。

さらにもう一度、ブラウザーコンソール上で値が変化したら、また違ったタイトルが表示されます。

 Session.set('pageTitle', 'A brand new title');
Browser console

セッションはグローバルに使うことができるので、アプリケーション内のどこでもこうした変化をさせることができます。 こうすることで私たちは多くの力を得られますが(???)、使いすぎてしまうと罠にはまります。

Identical Changes

Session.set()でセッション変数を変更すると、全く同じ値をセットします。 Meteorは リアクティブチェーン(???)を回避できるほど頭が良いので、不必要なメソッドコールを回避します。

Introducing Autorun

私たちはリアクティブデータストアの例で、テンプレートヘルパー内で(???)、を見てきました。 しかし、Meteorでのコンテキストの多く(テンプレートヘルパーなど)は本質的にリアクティブです。 Meteorのアプリのコードの多くはまだ素のリアクティブではないJavaScriptです。

次のようなコードをアプリのどこかにあるとします。

helloWorld = function() {
  alert(Session.get('message'));
}

私たちはセッション変数と呼んできましたが、このコンテキストはリアクティブではないと呼んでいないのは、 私たちが変数を変えるたびに新しいalertを取得しないという意味です。 (???)

ここでAutorunが登場します。その名前の通り、 autorunブロックの中に入れたコードは自動的に動きます。(???)  また、リアクティブデータストアが使われる度に変化します。動き続けます (???)

ブラウザーコンソールでこのようにタイピングしてみてください。

 Deps.autorun( function() { console.log('Value is: ' + Session.get('pageTitle')); } );
Value is: A brand new title
Browser console

ご推察の通り、このコードのブロックは autorunが一度動くと  (???)  データをコンソールに出力します。 では、タイトルを変化させてみましょう。

 Session.set('pageTitle', 'Yet another value');
Value is: Yet another value
Browser console

魔法のようだ!セッションの値が変化すると、   コンソールに新しい値を再出力することで もう一度内容を動かす必要があると (???)  autorunは 識別しました。 

そこで、先ほどの例に戻りましょう。 私たちが セッション変数が変わる度に 新しいアラートを動作させたい場合に、 autorunブロックにコードをラップする必要があります。

Deps.autorun(function() {
  alert(Session.get('message'));
});

これまで見てきたように、autorunはリアクティブデータソースをトラッキング(???)して、(???)に反応することにとても役立ちます。 

Hot Code Reload

Microscopeの開発を通して、私たちはMeteorの時間節約する機能を利用してきていました: ホットコードリロード(HCR)です。 私たちははソースコードファイルを保存する時はいつでも、 Meteorは変化を見つけて、各クライアントにページをリロードすることを伝えて瞬時に Meteorサーバーを動かすように再起動します。

これは自動的にページがリロードすることに似ていますが、重要な違いがあります。

どういうことか理解するために、私たちが使ってきたセッション変数を再設定することから始めます。

 Session.set('pageTitle', 'A brand new title');
 Session.get('pageTitle');
'A brand new title'
Browser console

ブラウザーウィンドウを手動でリロードすると、 セッショ変数は 当然ながらなくなります。(???) (これは新しいセッションが作られたからです。) 一方で、たとえば、ソースファイルを保存することでホットコードリロードを動作させると、ページはリロードしますが、 セッション変数もまだ設定されています。試してみましょう!

 Session.get('pageTitle');
'A brand new title'
Browser console

そのため、ユーザーが何をしているのか正確に把握するためにセッション変数を使います。 HCRは ユーザーに見えないところで実行されます。 全てのセッション変数の値を保存する こうすることで、ユーザーの混乱を最小限にするという確証を得て Meteorアプリケーションの新製品のバージョン(???)をデプロイすることができます。

この点について少し考えてみましょう。 もし私たちが URLやセッションの全ての状態を保持できるとしたら、 私たちは 各クライアントのアプリケーションのソースコードの動作を混乱を最小限にしてユーザーが気づくことなく変えることができます。

では、ページを手動で更新した際にどんなことが起きているのかチェックしていきましょう。

 Session.get('pageTitle');
null
Browser console

ページをリロードすると、セッションがなくなります。 HCR(???)、Meteorは ブラウザー内のローカルストレージに セッションを保存します。そしてリロードした後に再び読み込みます。 しかし、交互に起こるリロードの動作は理に適っています:(???) ユーザーがページをリロードすると、同じURLを再び見るようなものです。 いつユーザーがそのURLを訪れたのかを理解する  状態が始まることを リセットするべきです。(???)

重要なレッスンはこうしたことです:

  1. ユーザーの状態を常にセッションやURLで保存しているので、ユーザーはホットコードリロード時に混乱を最小限にすることができます。
  2. URLの(???)ユーザー間で共有したいどんな状態でも保存します。  

Adding Users

6

ここまで私たちは実用的な方法で静的な固定データを作って表示することでシンプルな試作品を作りました。

さらに、データを挿入して、データの変化が瞬時に現れることで、 どのように UI がデータの変化に反応するのかを見てきました。 しかし、このサイトはデータを入力できないので骨抜き状態です。実はまだユーザーもいないのです!

では、どのようにこの問題を解決するのか見ていきましょう。

アカウント:ユーザーをシンプルに作る

ほとんどのウェブフレームワークでは、ユーザーアカウントを付け加えることは面倒なものとしておなじみです。 たしかに、これはすべてのプロジェクトでやらなければなりませんが、簡単なことではありません。 さらには、OAuth やサードパーティの認証スキームを出来る限り早く使えるようにならなくてはなりません。 世の中はうんざりするほど速く進みます。

幸運なことに、Meteorはこの点も肩代わりしてくれます。 Meteor package がサーバー( JavaScript )とクライアント( JavaScript と HTML と CSS )の両方のコードに寄与するため、 私たちはほぼ無料でアカウントシステムを使うことができます。

meteor add accounts-ui で Meteor に組み込まれたアカウントの UI を使うことができますが、 ここでは Bootstrap を使ってアプリを作るので、 accounts-ui-bootstrap-dropdown を使います。 (違いはスタイリングだけなのでご心配なく) コマンドライン上で、このようにタイピングしてください。

$ mrt add accounts-ui-bootstrap-dropdown
$ mrt add accounts-password
Terminal

この2つのコマンドで私たちは特別なアカウントテンプレートを使うことができます; {{> loginButtons}} ヘルパーを使うことで、サイトに( them?)をインクルードすることができます。 ちょっとしたヒント:align 属性を使うことでログインのドロップダウンの表示をコントロールすることができます。 (たとえば: {{> loginButtons align=“right”}}).

では、ヘッダーにログインボタンを追加しましょう。 ヘッダーのコード量がが多くなってきているので、  テンプレート内に書き込む余地を与えましょう。(ヘッダーのコードはclient/views/includes/の中に入れます。) 私たちは追加のマークアップや Bootstrap クラスをサイトの外観を良くするために使っていきます。

<template name="layout">
  <div class="container">
    {{>header}}
    <div id="main" class="row-fluid">
      {{yield}}
    </div>
  </div>
</template>
client/views/application/layout.html
<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

さて、アプリを見てみるとサイトの右上にアカウントログインボタンがあるのがわかります。

Meteor's built-in accounts UI
Meteor’s built-in accounts UI

////

////

Accounts.ui.config({
  passwordSignupFields: 'USERNAME_ONLY'
});
client/helpers/config.js

コミット 6-1

Added accounts and added template to the header

Creating Our First User

先に進んでアカウントのサインアップをします: すると、「サインイン」ボタンは変化してユーザーネームを表示します。 これでユーザーアカウントが作られたことが確認できます。 しかし、ユーザーアカウントのデータはどこから来たのでしょうか。

アカウントパッケージを入れたことで、Meteor は新たに Meteor.users でアクセスできる特別なコレクションを作ります。 これを見るにはプラウザコンソールでこのように入力します。

 Meteor.users.findOne();
Browser console

コンソールはユーザーオブジェクトを意味しているオブジェクトを返します。 見てみると、ユーザーネームとあなただけを識別する _id がそこにあるのがわかります。  また、Meteor.user() を使って、現在ログインしているユーザー情報を得ることができます。

それではログアウトをして、次は違うユーザーネームでサインアップしてみます。 Meteor.user() は現在では2人のユーザーを返すことでしょう。 実行してみます:

 Meteor.users.find().count();
1
Browser console

コンソールは1を返しました。ちょっと待って、2じゃないの?(???) 最初のユーザーは削除されたのでしょうか? 再び最初のユーザーでログインしてみると、ユーザーが削除されていないことがわかります。

標準的なデータストア、MongoDB の中をチェックしていきましょう。 ターミナルで meteor mongo と入力して MongoDB にログインして、チェックします。

> db.users.count()
2
Mongo console

間違いなく2人のユーザーがいます。ではなぜブラウザーでは1人だけしかいなかったのでしょうか?

A Mystery Publication!

4章を振り返ると、autopublish を停止させたことを思えているでしょう。 コレクションがクライアントのローカルのコレクションとつながって、 サーバーから自動的にすべてのデータを送信することを阻止しました。 私たちはデータを交差させるために、パブリケーションとサブスクリプションのペアを作る必要がありました。 

私たちはまだユーザーパブリケーションについて何も設定していません。ではどうして私たちはユーザーデータを見ることができるのでしょうか。

その答えはアカウントパッケージが現在ログインしているユーザーの基本的なアカウントの詳細を “auto-publish” するからです。 そうでないとしたら、ユーザーはこのサイトにログインすることができません!

しかし、アカウントパッケージはカレントユーザーだけをパブリッシュします。 そうした理由で、ユーザーは他のユーザーアカウントの詳細を見ることはできないです。

パブリケーションはログインユーザーごとに一人のユーザーオブジェクトをパブリッシュしています。  (ログインしていない時はなにもありません。)

さらに、ユーザーコレクション内のドキュメントはサーバーとクライアントで同じフィールドを保持していないようです。  MongoDB では、ユーザーは多くのデータを(it=ドキュメント?)に保存しています。それを見るために、Mongo ターミナルに戻って、このように入力します:

> db.users.findOne()
{
  "createdAt" : 1365649830922,
  "_id" : "kYdBd9hr3fWPGPcii",
  "services" : {
    "password" : {
      "srp" : {
        "identity" : "qyFCnw4MmRbmGyBdN",
        "salt" : "YcBjRa7ArXn5tdCdE",
        "verifier" : "df2c001edadf4e475e703fa8cd093abd4b63afccbca48fad1d2a0986ff2bcfba920d3f122d358c4af0c287f8eaf9690a2c7e376d701ab2fe1acd53a5bc3e843905d5dcaf2f1c47c25bf5dd87764d1f58c8c01e4539872a9765d2b27c700dcdedadf5ac82521467356d3f91dbeaf9848158987c6d359c5423e6b9cabf34fa0b45"
      }
    },
    "resume" : {
      "loginTokens" : [
        {
          "token" : "BMHipQqjfLoPz7gru",
          "when" : 1365649830922
        }
      ]
    }
  },
  "username" : "tmeasday"
}
Mongo console

一方で、ブラウザーでユーザーオブジェクトはかなり少なくなっています。 同じコマンドを入力して見ることができます。

 Meteor.users.findOne();
Object {_id: "kYdBd9hr3fWPGPcii", username: "tmeasday"}
Browser console

この例では、ローカルのコレクションが実際のデータベースでどのようにしてセキュアなサブセットになるのかを表しています。 ログインユーザーはサインインをするためには十分な実際のデータベースだけを見ています。 これは今後見ていくことを学ぶ上で役に立つパターンです、 

これはあなたがさらにユーザーデータをパブリック(???)にできないという意味ではありません。 Meteor.users コレクション内で、どのようにしてフィールドを選択してパブリッシュするのかを見るのかを見る場合は、Meteor docs を参照しましょう。

Reactivity

Sidebar 6.5

コレクションがMeteorのコアな機能だとすると、リアクティビリティは そのコアを 便利にする( shell?)です。

コレクションは データの変化を処理するアプリケーションの( way?)を (radically?)に (transform?)します。 たとえば、AJAXなどで人力でデータの変化をチェックして、 HTMLにその変化をパッチするよりも、 Meteorによって、データの変化がどんなときでも( come in?)して、シームレスにユーザーインターフェースに適用することができます。

少し時間をとってこの点を考えてみます: (underlying?)なコレクションが更新されると、Meteorは裏側でどんなユーザーインターフェースのパーツでも変化させます。 

これをするために必須な(way?)は、 .observe()を使うことです。 .observe()はドキュメントがカーソルの変化にマッチした時に コールバックを(fiew?)する カーソル関数です。 

Posts.find().observe({
  added: function(post) {
    // when 'added' callback fires, add HTML element
    $('ul').append('<li id="' + post._id + '">' + post.title + '</li>');
  },
  changed: function(post) {
    // when 'changed' callback fires, modify HTML element's text
    $('ul li#' + post._id).text(post.title);
  },
  removed: function(post) {
    // when 'removed' callback fires, remove HTML element
    $('ul li#' + post._id).remove();
  }
});

こうしたコードが どのようにして (pretty quickly?)に 複雑化するのか すでに分かっているかもしれません。 投稿ごとの属性の変化を処理すると想定すると、投稿の

  • 内の複雑なHTMLを変化する必要があります。 (???)、(??????)

    When Should We Use observe()?

    上記のパターンを使うことは特に、サードパーティーのウィジットを(dealing with?)する時に必要となります。 たとえば、コレクションデータに基づいたリアルタイムなマップのピンを加えるか削除すると考えてみましょう。  

    その場合、Meteorコレクションと( get the map to “talk” with?)して、 データの変化の反応を見分けるために、 observe()コールバックを使う必要があります。 一例をあげると、マップ API のdropPin() や removePin()メソッドを呼び出すために addedとremoved コールバックを使うことになります。

    A Declarative Approach

    Meteorではより良い方法が用意されています。 宣言型アプローチである リアクティビリティです。 宣言型であることで、(specify behaviors for every possible change?)する代わりに (once?)オブジェクト間の 関係を定義でき、同期されていることを( know ?)できます。

    これはパワフルなコンセプトです。(???) なぜなら、リアルタイムシステムには 予期できない時にすべて変化できる 多くの入力データがあるからです。。

    observeコールバックについて考える代わりに( All this to say ?)、 Meteorでこのように書くことができます:

    <template name="postsList">
      <ul>
        {{#each posts}}
          <li>{{title}}</li>
        {{/each}}
      </ul>
    </template>
    

    その後で、このようにして投稿リストを呼んできます:

    Template.postsList.helpers({
      posts: function() {
        return Posts.find();
      }
    });
    

    裏側で、Meteorはobserve()コールバックにつないています。 また、 リアクティブデータが変化すると 関係のあるHTMLのセッションを再び取り出しています。(???)

    Dependency Tracking in Meteor: Computations

    Meteorはリアルタイムでリアクティブなフレームワークですが、Meteorアプリの中の全てのコードがリアクティブというわけではありません。 この場合では、 何かが変わる度にアプリ全体が再実行することでしょう。(???) その代わりに、リアクティビリティはコードの特定の範囲を制限されています。  私たちはその範囲をコンピュテーションと呼んでいます。

    コンピュテーションは 変化によってリアクティブデータソースを毎回実行しているコードのブロックです。(???) たとえば、セッション変数 リアクティブデータソースを(have?)して リアクティブに( to it?)に反応したいとすると、 コンピュテーションを設定する必要があります。

    しっかりとこの設定をする必要はありません。 というのは、Meteorがすでに 各テンプレートに (special?)なコンピュテーションをレンダリングしているからです。 ( テンプレートヘルパー内のコードとコールバックは初期設定でリアクティブであるということ意味しています。)(???)

    リアクティブデータソースは いつその値が変化するか知らせることができるように(it?)を使って すべてのコンピュテーションをトラッキングします。 そうするために、( it?)はコンピュテーションで invalidate()関数を呼び出します。

    コンピュテーションは通常、無効化(on?) 再評価する内容 を設定します。(???) そしてこれが テンプレートコンピュテーションに 何が起きているかということです。  (テンプレートコンピュテーションは ページをより効率的に(try and redraw?)するマジックを使います。) 必要であれば  (???) 無効化 コンピュテーションがすることを さらにコントロールできますが、 実際これは ほとんどの場合で  あなたが使っている行動です。(???)

    Setting Up a Computation

    私たちはコンピュテーションの裏側の仕組みを理解したので、 実際に(one?)を設定することは 不相応なほど(???)簡単そうです。

    Deps.autorun(function() {
      console.log('There are ' + Posts.find().count() + ' posts');
    });
    

    私たちは Meteor.startup()ブロック内の Depsブロックをラップする必要があります。   Meteorが Postsコレクションを (loading?)終えた時に (it ?)が実行します。(???)

    > Posts.insert({title: 'New Post'});
    There are 4 posts.
    

    すべての結果として、   私たちは とても自然な方法で リアクティブデータを使う コードを書くことができます。  依存システムは 裏側で 適切な時点で 再評価する 処理をします。 (???)

  • Creating Posts

    7

    これまでで、私たちはコンソールでデータベースを呼び出す Posts.insert を使うことで、簡単に投稿を作成する方法を見てきました。 しかし、ユーザーがコンソールを開いて新しい投稿をすることはないでしょう。

    結局のところ、私たちはユーザーがアプリに新しい記事を投稿できるように、ちょっとしたユーザーインターフェースを作る必要があります。

    新たに投稿ページを作る

    新しいページにルートを定義することから始めます。

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.map(function() {
      this.route('postsList', {path: '/'});
    
      this.route('postPage', {
        path: '/posts/:_id',
        data: function() { return Posts.findOne(this.params._id); }
      });
    
      this.route('postSubmit', {
        path: '/submit'
      });
    });
    
    lib/router.js

    ヘッダーへのリンクを追加する

    このルートを定義すると、私たちはヘッダーに投稿ページのリンクを追加することができます。

    <template name="header">
      <header class="navbar">
        <div class="navbar-inner">
          <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </a>
          <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
          <div class="nav-collapse collapse">
            <ul class="nav">
              <li><a href="{{pathFor 'postSubmit'}}">New</a></li>
            </ul>
            <ul class="nav pull-right">
              <li>{{loginButtons}}</li>
            </ul>
          </div>
        </div>
      </header>
    </template>
    
    client/views/includes/header.html

    ルートを設定するということは、ユーザーが /submit URLのウェブページを見ると Meteor が postSubmit テンプレートを表示するということです。では、テンプレートを書いていきましょう。

    <template name="postSubmit">
      <form class="main">
        <div class="control-group">
            <label class="control-label" for="url">URL</label>
            <div class="controls">
                <input name="url" type="text" value="" placeholder="Your URL"/>
            </div>
        </div>
    
        <div class="control-group">
            <label class="control-label" for="title">Title</label>
            <div class="controls">
                <input name="title" type="text" value="" placeholder="Name your post"/>
            </div>
        </div>
    
        <div class="control-group">
            <label class="control-label" for="message">Message</label>
            <div class="controls">
                <textarea name="message" type="text" value=""/>
            </div>
        </div>
    
        <div class="control-group">
            <div class="controls">
                <input type="submit" value="Submit" class="btn btn-primary"/>
            </div>
        </div>
      </form>
    </template>
    
    
    client/views/posts/post_submit.html

    注釈:かなりマークアップしましたが、これは Twitter Bootstrap を使っているためです。 form 要素だけは必要ですが、残りのマークアップはアプリの見た目を若干良くするために行っています。今はこのようになっているはずです。

    The post submit form
    The post submit form

    これでシンプルなフォームができました。私たちがこのフォームの動作について 心配する必要はありません。 フォームで submit イベントを受け取って、JavaScriptでデータを更新するためです。 (Meteor アプリが機能しないJavaScriptを使った完全な非関数だと考えると、 ( a non-JS?)フォールバックを提供しても、意味がありません。)

    投稿を作る

    form の submit イベントにイベントハンドラをバインドしていきましょう。 ボタンでの click イベントよりも、submit イベントを使うのがベストです。 それは submit イベントはすべての投稿方法をカバーするためです。 (たとえば、URL フィールドに( hitting enter?)するようなものでも)

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val(),
          message: $(e.target).find('[name=message]').val()
        }
    
        post._id = Posts.insert(post);
        Router.go('postPage', post);
      }
    });
    
    client/views/posts/post_submit.js

    コミット 7-1

    Added a submit post page and linked to it in the header.

    この関数はさまざまなフォームフィールドの値を明確にするため、jQuery を使っています。  また、その結果から新しい投稿オブジェクトを追加します。 私たちはブラウザが(doesn’t go ahead and try to submit the form=先に進まず、フォームから投稿しない?)ために、 イベントハンドラへのイベント引数で preventDefault する必要があります。

    最終的に、私たちは新しい投稿ページにルーティングすることができました。 コレクションでの insert()関数はデータベースに挿入されたオブジェクトに生成された id を返します。 (,which?)でルーターの go()関数は私たちがウェブで見るための URL を作り出します。

    その結果、ユーザーは(submit?)にヒットして、投稿が作られ、 ユーザーは瞬時に新しい投稿のディスカッションページに導かれます。

    セキュリティを強化する

    投稿を作るのは良いのですが、私たちはどんな人でも投稿を作れるようにしたくありません:ユーザーがログインをした上で投稿を行うようにしたいものです。  もちろん、私たちはログアウトしたユーザーから新しい投稿フォームを見えなくするところから始めていきます。 もしかしたら、まだユーザーはブラウザーコンソールからログインせずに投稿を作ることができるかもしれません。 (and we can’t have that?) 

    ありがたいことに、データセキュリティは Meteor コレクションに正しく (is baked?)されています; あなたが新しいプロジェクトを作るとき、(it=データセキュリティ?)は初期設定では停止しています。 そのようになっているのは、Meteor で簡単にアプリを作り始めることができるようにするためで、退屈なことは後でやることになります。

    私たちが作ってきたアプリは、こうした補助輪が必要なくなったので、補助輪を外していきましょう!insecure パッケージを削除します:

    $ meteor remove insecure
    
    Terminal

    そうした後で、投稿フォームが動作しないことに気づくことでしょう。 これは insecure パッケージがないと、クライアントサイドが投稿コレクションへの挿入が許可されないためです。 私たちはどのような時にクライアントが投稿を挿入して良いか、あるいは投稿の挿入をサーバーサイドでするか Meteor に明確なルールを与える必要があります。

    投稿の挿入を許可する

    フォームがまた動くようにするために、私たちはクライアントでの投稿の挿入を許可する方法を示します。 最終的に、私たちはまた違った( technique=技術or方法?)を規定するのですが、 今のところは次のようにすることで簡単にもう一度動くようにします。

    Posts = new Meteor.Collection('posts');
    
    Posts.allow({
      insert: function(userId, doc) {
        // only allow posting if you are logged in
        return !! userId;
      }
    });
    
    collections/posts.js

    コミット 7-2

    Removed insecure, and allowed certain writes to posts.

    私たちは Posts.allow を呼び出して 「クライアントがPostsコレクションに投稿を挿入しても良い状況だ。」と Meteor に指定します。 今回は、私たちは「 userId を持つユーザーに限ってクライアントは投稿を挿入することを許可する」と言っています。

    ユーザーが変更する userId は、allow コールとdeny コールに引き渡されます。 (ユーザーが誰もログインしていない場合は null を返します。)これは必ず役立ちます。 ユーザーアカウントは Meteor のコアと結びついているので、userId が常に正しいと確信が持つことができます。

    ユーザーが投稿を作るにはログインが必要となるようにできました。  ためしにログアウトをして、投稿を作ってみましょう;コンソールでこのように表示されるはずです。

    Insert failed: Access denied
    Insert failed: Access denied

    しかし、私たちはまだいくつかの問題に対処する必要があります。

    • ログアウトしたユーザーはまだ投稿の作成フォームにアクセスできます。
    • 投稿は(in any way?) ユーザーに紐づけられていません。(またサーバーにこれを実行するためのコードがありません。)
    • 同じ URL を投稿して、投稿を重複させることも可能となっています。 

    では、こうした問題点を修正していきましょう。

    新しい投稿フォームへのアクセスをセキュアにする

    では、ログアウトしたユーザーが投稿フォームを見れないようにするところから始めていきましょう。 私たちはこれをルーターレベルで行うために、ルートフックを定義します。

    フックは ルーティングプロセスを(intercept=受け取って?)、(potentially=潜在的に?)にルーターの行動を変えます。 これはあなたが入る前や出る時に、あなたの認証情報をチェックする警備員のようなものだと捉えることができます。 

    私たちはユーザーがログインしているかどうかチェックして、  予定通りの postSubmit テンプレートではなくて、accessDenied テンプレートにレンダリングしてしまわないかをチェックする必要があります。(???)  (この時はルーターを停止します。)では、そのように router.js を修正していきましょう。

    Router.configure({
      layoutTemplate: 'layout'
    });
    
    Router.map(function() {
      this.route('postsList', {path: '/'});
    
      this.route('postPage', {
        path: '/posts/:_id',
        data: function() { return Posts.findOne(this.params._id); }
      });
    
      this.route('postSubmit', {
        path: '/submit'
      });
    });
    
    var requireLogin = function() {
      if (! Meteor.user()) {
        this.render('accessDenied');
        this.stop();
      }
    }
    
    Router.before(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    さらに、アクセスが拒否されたページのためのテンプレートも作ります。

    <template name="accessDenied">
      <div class="alert alert-error">You can't get here! Please log in.</div>
    </template>
    
    client/views/includes/access_denied.html

    コミット 7-3

    Denied access to new posts page when not logged in.

    ログインせずに http://localhost:3000/submit/ にアクセスすると、このようになります。

    The access denied template
    The access denied template

    ルーティングフックの素晴らしい点は、リアクティブであることです。 つまり、ユーザーがログインする際に、私たちは宣言型で良く、コールバックなどについて考える必要はないということです。 ユーザーのログイン状態が変化すると、ルーターのページテンプレートは accessDenied から postSubmit に瞬時に変化します。 私たちはこの処理のために、明示的にコードを書く必要が全くありません。 

    ログインした時にページを再読み込みするようにします。 投稿ページが現れる前のほんのつかの間に、アクセスの拒否されたテンプレートが表示されるところ見かけるかもしれません。 この理由は、(it ?)がサーバーと通信して、(ブラウザのローカルストレージに保存された)ユーザーが現在いるかどうかチェックする前に、Meteor がすぐにテンプレートのレンダリングを始めるためです。

    これはクライアントとサーバー間の込み入ったレイテンシを処理する上で、一般的な問題です。 この問題を避けるために、私たちはユーザーがアクセスしているのか確かめることを待っている短い間に(loading screen=ローディングスクリーン?)を表示します。

    結局、現段階で私たちはユーザーが正しいログイン認証情報を保持しているのかわからないので、私たちが(do?)する限り accessDenied や postSubmit テンプレートを表示することはできません。

    Meteor.loggingIn() が true の間にロードディングテンプレートを使うため、フックを修正します。    ~~~js Router.map(function() { this.route(‘postsList’, {path: ’/’});

    this.route('postPage’, { path: ’/posts/:id’, data: function() { return Posts.findOne(this.params.id); } });

    this.route('postSubmit’, { path: ’/submit’ }); });

    var requireLogin = function() { if (! Meteor.user()) { if (Meteor.loggingIn()) this.render(this.loadingTemplate); else this.render('accessDenied’);

    this.stop();
    

    } }

    Router.before(requireLogin, {only: 'postSubmit’}); ~~~

    lib/router.js

    コミット 7-4

    Show a loading screen while waiting to login.

    リンクを隠す

    ユーザーがログアウトしたした際に、誤ってこのページにアクセスしないようにする最も簡単な方法は、リンクを非表示にすることです。 これはかなり簡単にできます。

    <ul class="nav">
      {{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{/if}}
    </ul>
    
    client/views/includes/header.html

    コミット 7-5

    Only show submit post link if logged in.

    currentUser ヘルパーはアカウントパッケージによって提供されており、 Meteor.user() に相当する Spacebars です。 currentUser ヘルパーはリアクティブなので、ユーザーがログインをするとリンクが表示され、ログアウトをするとリンクが消えます。

    Meteor Method: Better Abstraction and Security

    私たちはログアウトしたユーザー用に、新しい投稿ページへのアクセスをセキュア にしたので、 仮にログアウトしたユーザーがずるをしてコンソールを使って投稿しようとしても、投稿を作れないようにします。 まだまだ、対処すべきことがいくつかあります。

    • 投稿にタイムスタンプする。
    • 同じ URL が投稿できないようにする。
    • 投稿作成者についての詳細 (ID, username など) を追加する。

    こうしたことは submit イベントハンドラでできると考えられます。 しかし、現実的にはすぐに大きな問題に直面します。

    • タイムスタンプでは、私たちはユーザーのコンピュータの時間が正しいとみなす必要がありますが、常にその時間が正しいとは限りません。
    • クライアントはこれまでサイトに投稿されたすべての URL についてわかりません。  クライアントは現在見ている投稿だけがわかります。(これがいかに正確なのかは後々見ていきます)  そのため、クライアントサイドで URL の重複を調べることはできません。
    • 最後に、私たちはクライアントサイドにユーザーの詳細を追加しましたが、  私たちは正確にそれを (enforce?)できないので、  ブラウザーコンソールを使うことで、( up to exploitation?)、アプリを開くことができます。

    以上のような理由から、イベントハンドラをシンプルに保つことが良いということになります。 もし、最も基本的なコレクションへの挿入や更新よりも多くのことをするとしたら、メソッドを使います。

    Meteor のメソッドは クライアントサイドで呼び出されるサーバーサイドの関数です。 私たちはこのことについてよく慣れ親しんでいます。 実のところ、Collection の insert や update、 remove 関数はすべてメソッドなのです。 どのように自分でメソッドを作るのか見ていきましょう。

    まず、post_submit.js に戻ります。 Posts コレクションに直接挿入するのではなく、post というメソッドを呼び出します。

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val(),
          message: $(e.target).find('[name=message]').val()
        }
    
        Meteor.call('post', post, function(error, id) {
          if (error)
            return alert(error.reason);
    
          Router.go('postPage', {_id: id});
        });
      }
    });
    
    client/views/posts/post_submit.js

    Meteor.call関数は第一引数で指定したメソッドを呼び出します。  コール(この場合は、フォームから作られた post オブジェクト)に引数を与えることができ、 最後にサーバーサイドのメソッドが行われた時に実行するコールバックを加えます。  ここで問題があった場合にユーザーに警告を出します。 もし問題がない場合は、ユーザーを最新の投稿ディスカッションページにリダイレクトします。

    それから、私たちは collections/posts.js ファイルでメソッドを定義します。 とにかく、Meteor メソッドは allow() ブロックを(bypass =迂回?)するので、私たちは posts.js からallow() ブロックを削除します。 メソッドはサーバーで実行されるため、Meteorは(they =メソッド?)が信頼に値すると見なします。

    Posts = new Meteor.Collection('posts');
    
    Meteor.methods({
      post: function(postAttributes) {
        var user = Meteor.user(),
          postWithSameLink = Posts.findOne({url: postAttributes.url});
    
        // ensure the user is logged in
        if (!user)
          throw new Meteor.Error(401, "You need to login to post new stories");
    
        // ensure the post has a title
        if (!postAttributes.title)
          throw new Meteor.Error(422, 'Please fill in a headline');
    
        // check that there are no previous posts with the same link
        if (postAttributes.url && postWithSameLink) {
          throw new Meteor.Error(302,
            'This link has already been posted',
            postWithSameLink._id);
        }
    
        // pick out the whitelisted keys
        var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
          userId: user._id,
          author: user.username,
          submitted: new Date().getTime()
        });
    
        var postId = Posts.insert(post);
    
        return postId;
      }
    });
    
    collections/posts.js

    コミット 7-6

    Use a method to submit the post.

    このメソッドは少し複雑になっていますが、なんとか話についていくことができるでしょう。

    最初に、user 変数を定義して、同じリンクがすでに存在しているかどうかチェックします。 それから、ユーザーがログインしているかチェックして、ログインしてない場合はエラーを投げます。 (最終的にブラウザで alert されます。) 私たちは 投稿にタイトルがあるか確認する post オブジェクトの妥当性を検証します。 

    次に、同じ URL の投稿があった場合に、リダイレクトを意味する 302 エラーを投げて、ユーザーに以前作られた投稿に行って見るように伝えます。

    MeteorのErrorクラスは3つの引数をとります。 1つめのerrorは選択したエラーコードです。(この場合では302) 2つめのreasonは人間が読み取れるエラーの説明です。 3つめのdetailsは、役立つ追加情報となります。

    この場合、私たちは 見つけた投稿のIDを渡すために3つの引数を使います。 ネタバレ注意:以前から存在している投稿にユーザーをリダイレクトするために、私たちは後でこの3つの引数を使います。 

    すべてのチェックが終わると、私たちはUnderscore の pick を使って挿入したいフィールドをつかみます。 (ユーザーがブラウザーコンソールでこのメソッドを呼び出す(ensure?)はデータをデータベースに入れることができません。) また、投稿したユーザーについての情報を現在の時間と同様に extend を使って投稿に含めます。

    最後に、私たちは投稿を挿入して、ユーザーに新しい投稿の id を返します。

    Sorting Posts

    これで私たちはすべての投稿に投稿した日付があるので、  日付の属性を使ってデータをソートすることは理に適っています。 そうするために、私たちはMongoDB の sort 演算子を使います。sort 演算子はソートするためのキーを構成するオブジェクトを要求します。 (and a sign indicating whether they are ascending or descending???)

    Template.postsList.helpers({
      posts: function() {
        return Posts.find({}, {sort: {submitted: -1}});
      }
    });
    
    client/views/posts/posts_list.js

    コミット 7-7

    Sort posts by submitted timestamp.

    ちょっとした労力がいりましたが、私たちは最終的にユーザーがアプリ内に内容を入力できる安全なユーザーインターフェースを作りました。  

    しかし ユーザーがコンテンツを作ることができるすべてのアプリは編集や削除をする方法をユーザーに提供する必要があります。 この点は Editing Posts の章で学ぶことになります。

    Latency Compensation

    Sidebar 7.5

    ////

    Without latency compensation
    Without latency compensation

    ////

    ////

    • +0ms: ////
    • +200ms: ////
    • +500ms: ////

    If this were the way Meteor operated, then there’d be a short lag between performing such actions and seeing the results (that lag being more or less noticeable depending on how close you were to the server). We can’t have that in a modern web application!

    Latency Compensation

    With latency compensation
    With latency compensation

    ////

    ////

    • +0ms: ////
    • +0ms: ////
    • +200ms: ////
    • +500ms: ////

    ////

    Observing Latency Compensation

    ////

    ////

    ////

    Meteor.methods({
      post: function(postAttributes) {
        // […]
    
        // pick out the whitelisted keys
        var post = _.extend(_.pick(postAttributes, 'url', 'message'), {
          title: postAttributes.title + (this.isSimulation ? '(client)' : '(server)'),
          userId: user._id, 
          author: user.username, 
          submitted: new Date().getTime()
        });
    
        // wait for 5 seconds
        if (! this.isSimulation) {
          var Future = Npm.require('fibers/future');
          var future = new Future();
          Meteor.setTimeout(function() {
            future.return();
          }, 5 * 1000);
          future.wait();
        }
    
        var postId = Posts.insert(post);
    
        return postId;
      }
    });
    
    collections/posts.js

    ////

    ////

    ////

    Template.postSubmit.events({
      'submit form': function(event) {
        event.preventDefault();
    
        var post = {
          url: $(event.target).find('[name=url]').val(),
          title: $(event.target).find('[name=title]').val(),
          message: $(event.target).find('[name=message]').val()
        }
    
        Meteor.call('post', post, function(error, id) {
          if (error)
            return alert(error.reason);
        });
        Router.go('postsList');
      }
    });
    
    client/views/posts/post_submit.js

    コミット 7-5-1

    Demonstrate the order that posts appear using a sleep.

    ////

    Our post as first stored in the client collection
    Our post as first stored in the client collection

    ////

    Our post once the client receives the update from the server collection
    Our post once the client receives the update from the server collection

    Client Collection Methods

    ////

    ////

    1. ////
    2. ////

    Methods Calling Methods

    ////

    ////

    ////

    Editing Posts

    8

    これまでで投稿を作ることができたので、次のステップは投稿の編集と削除をできるようにすることです。  そうするための UI コードはとてもシンプルですが、  そろそろ Meteor でユーザーパーミッションを管理する方法を話す良い時期でしょう。

    はじめに、ルーターをつなぎましょう。  私たちは投稿編集ページアクセスするためのルートを加えて、データコンテキストを設定します:

    Router.configure({
      layoutTemplate: 'layout'
    });
    
    Router.map(function() {
      this.route('postsList', {path: '/'});
    
      this.route('postPage', {
        path: '/posts/:_id',
        data: function() { return Posts.findOne(this.params._id); }
      });
    
      this.route('postEdit', {
        path: '/posts/:_id/edit',
        data: function() { return Posts.findOne(this.params._id); }
      });
    
      this.route('postSubmit', {
        path: '/submit'
      });
    });
    
    var requireLogin = function() {
      if (! Meteor.user()) {
        if (Meteor.loggingIn())
          this.render('loading')
        else
          this.render('accessDenied');
    
        this.stop();
      }
    }
    
    Router.before(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    The Post Edit Template

    これで私たちはテンプレートに集中することができます。  この postEdit テンプレートはスタンダードなフォームです:

    <template name="postEdit">
      <form class="main">
        <div class="control-group">
            <label class="control-label" for="url">URL</label>
            <div class="controls">
                <input name="url" type="text" value="{{url}}" placeholder="Your URL"/>
            </div>
        </div>
    
        <div class="control-group">
            <label class="control-label" for="title">Title</label>
            <div class="controls">
                <input name="title" type="text" value="{{title}}" placeholder="Name your post"/>
            </div>
        </div>
    
        <div class="control-group">
            <div class="controls">
                <input type="submit" value="Submit" class="btn btn-primary submit"/>
            </div>
        </div>
        <hr/>
        <div class="control-group">
            <div class="controls">
                <a class="btn btn-danger delete" href="#">Delete post</a>
            </div>
        </div>
      </form>
    </template>
    
    client/views/posts/post_edit.html

    そしてこれが(goes with it?)するpost_edit.jsマネージャーです。

    Template.postEdit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var currentPostId = this._id;
    
        var postProperties = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        }
    
        Posts.update(currentPostId, {$set: postProperties}, function(error) {
          if (error) {
            // display the error to the user
            alert(error.reason);
          } else {
            Router.go('postPage', {_id: currentPostId});
          }
        });
      },
    
      'click .delete': function(e) {
        e.preventDefault();
    
        if (confirm("Delete this post?")) {
          var currentPostId = this._id;
          Posts.remove(currentPostId);
          Router.go('postsList');
        }
      }
    });
    
    client/views/posts/post_edit.js

    そろそろ、このコードのほとんどはおなじみになってきたことでしょう。

    私たちは2つのテンプレートのイベントコールバックがあります:  ひとつはフォームの submit イベントで、もうひとつはリンクを削除する click イベントです。

    delete コールバックは、かなりシンプルです:  デフォルトのクリックイベントを抑制して、確認を求めます。  (get it?=これを終えると?)、テンプレートのデータコンテキストから現在の投稿 ID を取得して、  ( it?=現在の投稿ID)を削除して、最終的にユーザーをホームページにリダイレクトします。

    updateコールバックは少し長いですが、それほど複雑ではありません。  デフォルトのイベントを抑制して、現在の投稿を取得した後で、  私たちはページから新しいformフィールドの値を取得して、postProperties オブジェクトに(them?)を格納します。

    これで私たちはオブジェクトを Meteor の Collection.update() メソッドに渡して、もしアップデートが失敗したら、( either?=どちらかの?)エラーを表示するコールバックを使います。  あるいは、アップデートに成功したらユーザーを投稿ページに送ります。

    Adding Links

    ユーザーが投稿の編集ページにアクセスする方法ができるように、私たちは投稿にリンクを加えます。

    <template name="postItem">
      <div class="post">
        <div class="post-content">
          <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
          <p>
            submitted by {{author}}
            {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
          </p>
        </div>
        <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
      </div>
    </template>
    
    client/views/posts/post_item.html

    もちろん、私たちは (somebody else’s form?=誰かのフォーム)に編集リンクを 表示したくありません。  これは ownPostヘルパーが中に入る場所です。

    Template.postItem.helpers({
      ownPost: function() {
        return this.userId == Meteor.userId();
      },
      domain: function() {
        var a = document.createElement('a');
        a.href = this.url;
        return a.hostname;
      }
    });
    
    client/views/posts/post_item.js
    Post edit form.
    Post edit form.

    コミット 8-1

    Added edit posts form.

    投稿編集フォームは良さそうに見えますが、今は何も編集することはできません。 どういうことでしょうか。

    Setting Up Permissions

    私たちは以前にinsecureパッケージを削除したので、現在クライアントサイドでのすべての変更は拒否されます。

    これを修正するために、私たちはパーミッションルールを設定します。  最初に、lib内に新しく permissions.js ファイルを作ります。  これは最初にパーミッションロジックを読み込みます。(そして、両方の環境で使うことができます。)

    // check that the userId specified owns the documents
    ownsDocument = function(userId, doc) {
      return doc && doc.userId === userId;
    }
    
    lib/permissions.js

     投稿をつくるの章で、私たちは allow()メソッドを削除しました。というのも、私たちは allow()を回避するサーバーメソッドを使って新しい投稿を挿入していたためです。

    しかし、今はクライアントから投稿を編集して削除をするために、  posts.jsに戻って、allow() ブロックを加えましょう。

    Posts = new Meteor.Collection('posts');
    
    Posts.allow({
      update: ownsDocument,
      remove: ownsDocument
    });
    
    Meteor.methods({
      ...
    
    collections/posts.js

    コミット 8-2

    Added basic permission to check the post’s owner.

    Limiting Edits

    自分の投稿を編集できるからといって、あなたがすべてのプロパティを編集できるということではありません。  たとえば、私たちはユーザーが投稿を作って、だれかに投稿を割り当てるようにしたくありません。(???)

    ユーザーが特定のフィールドだけを編集できるようにするために、私たちは Meteor’s deny()コールバックを使います。 

    Posts = new Meteor.Collection('posts');
    
    Posts.allow({
      update: ownsDocument,
      remove: ownsDocument
    });
    
    Posts.deny({
      update: function(userId, post, fieldNames) {
        // may only edit the following two fields:
        return (_.without(fieldNames, 'url', 'title').length > 0);
      }
    });
    
    collections/posts.js

    コミット 8-3

    Only allow changing certain fields of posts.

    私たちは変更されたフィールドのリストを含んだ fieldNames配列を取得しています。  また、url や titleではないフィールドを含んだ(sub-array=下位の配列?)を返すために、Underscoreのwithout()メソッドを使います。

    すべてが標準の場合、この配列は空っぽで、長さは0になります。  だれかが何か(funky?=ファンキー?)なことをしたら、この配列の長さは1かそれ以上になって  コールバックは true を返します。(このようにアップデートを拒否します)

    Method Calls vs Client-side Data Manipulation

    投稿を作るために、私たちはpostメソッドを使います。一方で、投稿を編集したり削除したりするために、私たちは直接クライアントで update と remove を呼び出してallow とdenyでアクセスを制限しています。

    いつ( one?)を適切にして、(not the other?) でしょうか?(???)

    物事が 比較的単純で、あなたが適切に allow とdeny でルールを表現できます。

    クライアントから 直接データベースを操作することで、(perception of immediacy?)を作って、  あなたが優雅に失敗事例に対処することを忘れない限り、より良いユーザーエクスペリエンスに役立てることができます。   (つまり、結局はサーバーが変更を言い返す時は、うまくいきませんでした。)

    しかし、ユーザーのコントロールの範囲外にする必要になり始めるとすぐに  (たとえば、新しい投稿にタイムスタンプをしたり、正しいユーザーに(it?)を割り当てるなど)、  おそらく、メソッドを使うのが良いでしょう。

    メソッドコールはのいくつかのシナリオで適切です 。

    • 伝えるためのリアクティビリティと同期を待つよりも、コールバックから値を返すか知る必要の時(???)。
    •  多数のコレクションを(ship?)するにはコスト高になる( heavy ?)データベース機能のために。
    • データを要約して集約する場合(たとえば、count, average, sum)。

    Allow and Deny

    Sidebar 8.5

    Meteorのセキュリティシステムは、変更したい場合にメソッドに変更を行う必要なく、 データベースの変更を制御することができます。

    私たちは余分なプロパティを持つ投稿を装飾してその投稿のURLがすでに投稿されていたとき、特別な行動をとるような補助作業を行うために、 記事を作成する際に特別なpostメソッドを作成する必要がありました。

    一方で、記事の更新や削除するための新しいメソッドを作成する必要はありませんでした。 ユーザーがこれらのアクションを実行する権限を持っていたかどうかを確認するために必要な、allowdenyコールバックで簡単にできました。

    これらのコールバックを使用すると、私たちはデータベースの変更の詳細については宣言型であること、 および幾つかの種類の更新の使用することで、できると言うことができます。 アカウントシステムと統合するという事実はおまけです。

    複数のコールバック

    必要に応じて、我々は多くのallowコールバックを定義することができます。

    我々はただ与えられた変更のためにtrueを返すためにそれらの少なくとも一つを必要としています。

    それで、Posts.insertがブラウザから呼び出されることで(クライアントサイドのコードかコンソールからのものかは関係なく)、 サーバーはtrueを返すものが一つでもあればinsertは許可と判断します。 それがいずれも見つからない場合は、挿入を許可しませんし、クライアントに403エラーを返します。

    同様に、一つ以上のdenyコールバックを定義することができます。これらのコールバックのいずれかがtrueを返した場合は、変更がキャンセルされ、403が返されます。 このロジックの意味することは、insertが成功するには一つ以上のallow insertコールバックとすべてのdenyinsertコールバックが実行されることを意味します。

    Note: n/e stands for Not Executed
    Note: n/e stands for Not Executed

    言い換えれば、Meteorは最初のdenyで始まるコールバックリストを下に移動させ、 その後allowの1つがtrueを返すまで、すべてのコールバックを実行します。

    このパターンの実用的な例は、2つのallow()コールバックを持つ可能性があり、 ポストは、1つめで現在のユーザーに属しているかどうかをチェック、もう一つで現在のユーザーが管理者権限を持つかどうかをチェックします。 現在のユーザーが管理者である場合は、それらのコールバックの少なくとも一つがtrueを返しますので、これは、彼らがどんな記事を更新することができるかが保証されます。

    レイテンシー補正

    .update()など)データベース変異メソッドは他のメソッドと同様に、待ち時間補償されることを忘れないでください。 だから、例えば、あなたがブラウザのコンソールから、あなたに属していない投稿を削除しようとすると、 ローカルのコレクションは、投稿が簡単に消えて表示されますが、サーバーがそのことを通知されてから実際の削除が行われるため、実際にはその瞬間に文書は削除されていません。

    もちろん、この動作は、コンソールから発生する問題はありません。 (ユーザーがコンソール上でデータを台無しにしようとした場合、彼らのブラウザで何かが起りますが、こちら側に影響はしません) ただし、これはあなたのユーザーインターフェイスに起こらないことを確認する必要があります。 たとえば、削除は許可していない文書のための削除ボタンを表示していないことを確認するために労力を割く必要があります。

    ありがたいことに、あなたはクライアントとサーバの間の権限コードを共有することができますので、 (たとえば、ライブラリ関数canDeletePost(user, post)を書くことができ、/libディレクトリにそれを置き共有します。) したがって余分なコードを必要としません。

    サーバー側のアクセス権

    許可システムはクライアントから開始されデータベース変異に適用されることを覚えておいてください。 サーバーでは、Meteorは、すべての操作が許可されていることを前提としています。

    このことは、サーバー側でdeletePostMeteorメソッドを記述した場合、あなたがクライアントから呼び出すことができ、 誰もがどんな投稿を削除することができることを意味します。 そのメソッド内のユーザー権限をチェックしない限り、実行は避けたいと思うはずです。

    Errors

    9

    投稿に問題がある時に、 ユーザーに警告をするために単純にブラウザの標準的なalert()ダイアログを使うことは少々不愉快で、 素晴らしいUXに役立つわけではありません。 この点をよりよくできます。

    多目的で使えるエラーを報告する仕組みを作りましょう。 これはユーザーのワークフローを中断することなくユーザーに何が起きているか知らせる上でより良い役割を果たします。

    人気のMacOSアプリに似たウィンドウの右上隅、 Growlのような新たなエラーを表示する単純なシステムを実装しようと思います。

    ローカルコレクションの紹介

    始めるにあたり、私たちのエラーを格納するコレクションを作成する必要があります。 エラーは、現在のセッションにのみ関連し、永続的である必要はないことを考えると、 新しい何かをする、ローカルコレクションを作成しようと思います。

    上記を達成するために、我々はclientディレクトリ内のエラーを作成します。 (このコレクションのデータはサーバー側のデータベースに保存されることはありません) そのMongoDBのコレクション名がnullに設定します(クライアント専用のコレクションを作ります。)

    // Local (client-only) collection
    Errors = new Mongo.Collection(null);
    
    client/helpers/errors.js

    これでコレクションが作られたので、 私たちはこのコレクションにエラーの追加を呼び出すthrowError関数を入れることができます。 私たちはallowdenyなどのセキュリテイを気にする必要がありません。 というのも、このコレクションは「ローカル」で、MongoDBに保存されていないからです。

    throwError = function(message) {
      Errors.insert({message: message});
    };
    
    client/helpers/errors.js

    エラーを格納するためにローカルコレクションを使うことの利点は、 すべてのコレクションと同様に、リアクティブだからです。 つまり、私たちは他のコレクションデータを表示するのと同じように リアクティブにエラーを表示することができます。

    エラー表示

    メインレイアウトの上部でエラーを表示していきます:

    <template name="layout">
      <div class="container">
        {{> header}}
        {{> errors}}
        <div id="main" class="row-fluid">
          {{> yield}}
        </div>
      </div>
    </template>
    
    client/templates/application/layout.html

    errors.html 内に、errors and error テンプレートを作りましょう:

    <template name="errors">
      <div class="errors">
        {{#each errors}}
          {{> error}}
        {{/each}}
      </div>
    </template>
    
    <template name="error">
      <div class="alert alert-danger" role="alert">
        <button type="button" class="close" data-dismiss="alert">&times;</button>
        {{message}}
      </div>
    </template>
    
    client/templates/includes/errors.html

    2つのテンプレート

    私たちが2つのテンプレートを1つのファイルに置いていることにお気づきでしょう。 今まで私たちは 「1つのファイルに1つのテンプレート」という慣習を支持してきましたが、 Meteorに関する限りでは、1つのファイルにすべてのテンプレートを置くこともできます。 (混乱を招くことになりますがずべてをmain.htmlに収めることすらできます!)

    この場合、両方のerrorテンプレートはとても短いので、 私たちはリポジトリを少しきれいにしておくために、例外として同じファイルにerrorテンプレートを置きました。 

    私たちはテンプレートヘルパーをまとめる必要があり、用意ができています。

    Template.errors.helpers({
      errors: function() {
        return Errors.find();
      }
    });
    
    client/templates/includes/errors.js

    実はすでに新しいエラーメッセージを手動で試すことができます。 以下のようにブラウザコンソールでタイプするだけです。:

    throwError("I'm an error!");
    
    Testing error messages.
    Testing error messages.

    コミット 9-1

    Basic error reporting.

    2種類のエラー

    この時点で、「アプリレベル」エラーと「コードレベル」のエラーとを区別することは重要です。

    アプリレベルエラーは、一般的にユーザがトリガーとなり、 ユーザーが順番にそれらに基づいて行動していきます。 これらは、検証エラー、権限エラー、「Not Found.」エラーなどというものです。 これらはユーザーが遭遇した瞬間に問題が解決するのを助けるために、 ユーザーに表示するエラーの一種です。

    コードレベルエラーは、上記以外の種類で、予想外にあなたのコード内の実際のバグがトリガーで、 おそらく直接ユーザーに、見せたくない部分です。 その代わりに、サードパーティのエラー追跡サービスがいくつかありますのでそれらで追跡することになります。 (例えばKadira)

    この章で取り扱うバグは、アプリレベルエラーのタイプに焦点を当てます。

    エラーの作成

    今、エラーを表示する方法を知りましたが、私たちは何かを表示する前に、 表示するためのトリガーを必要としています。 でも、実はすでに良好なエラーのシナリオを実装しています。:重複した投稿の警告です。 単純にpostSubmitイベントヘルパー内のalert呼び出しを、 新しいthrowError関数に置き換えるだけです。:

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        };
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return throwError(error.reason);
    
          // show this result but route anyway
          if (result.postExists)
            throwError('This link has already been posted');
    
          Router.go('postPage', {_id: result._id});
        });
      }
    });
    
    client/templates/posts/post_submit.js

    さらにpostEditイベントヘルパーにも同じことをします。

    Template.postEdit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var currentPostId = this._id;
    
        var postProperties = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        }
    
        Posts.update(currentPostId, {$set: postProperties}, function(error) {
          if (error) {
            // display the error to the user
            throwError(error.reason);
          } else {
            Router.go('postPage', {_id: currentPostId});
          }
        });
      },
      //...
    });
    
    client/templates/posts/post_edit.js

    コミット 9-2

    Actually use the error reporting.

    試してましょう:URLにhttp://meteor.comを入力して投稿を作ってみましょう。 このURLがすでに固定ファイルの投稿情報に入っているため、このように表示されるはずです。

    Triggering an error
    Triggering an error

    エラーの削除

    数秒後にエラーメッセージは消えていきます。 これは、私たちはこの本の冒頭に戻ればわかりますが、追加のスタイルシートに含まれたCSSマジックの小辺に 実際にあります。

    @keyframes fadeOut {
      0% {opacity: 0;}
      10% {opacity: 1;}
      90% {opacity: 1;}
      100% {opacity: 0;}
    }
    
    //...
    
    .alert {
      animation: fadeOut 2700ms ease-in 0s 1 forwards;
      //...
    }
    
    client/stylesheets/style.css

    opacityプロパティのための4つのキーフレームを指定するfadeOut CSSアニメーション (0%、10%、90%、100%を設定)を定義し、 .alertクラスにこのアニメーションを適用しています。

    アニメーションは、2700ミリ秒の合計に対して実行されease-inタイミングの式を使用します。 一度だけ実行0秒の遅延で実行し、それが実行されてたら、最後のキーフレームに滞在します。

    アニメーション vs アニメーション

    Meteor自身が制御アニメーションを行わず、 (あらかじめ決められ、我々のアプリのコントロールの外にある)、 CSSベースのアニメーションを使用していることに不思議に思われるかもしれません。

    Meteorは挿入アニメーションのサポートを提供しませんが、 我々はエラーに集中するためにこの章を使っています。だから我々は今のところ「イマイチ」なCSSアニメーションを使用しますし、 我々はアニメーションの章のための派手なものを残しておきます。

    これは動作しますが、複数のエラーをトリガします。 (例えば、同じリンクを3回提出する等を試してみてください。) それらが互いの上に積み重ね取得していることに気付きますか。

    Stack overflow.
    Stack overflow.

    これは.alert見た目としては消えていますが、DOMには存在しているからなのです。 この問題を解決する必要があります。

    これはまさにMeteorが輝く状況の一つです。 Errorsコレクションがリアクティブであるため、 私たちはこれらの古いエラーを取り除くためにやらなければならないのは、 コレクションからそれらを削除することです!

    Meteor.setTimeoutを使用し、タイムアウト後(この場合は、3000ミリ秒)に 実行されるコールバック関数を指定します。

    Template.errors.helpers({
      errors: function() {
        return Errors.find();
      }
    });
    
    Template.error.rendered = function() {
      var error = this.data;
      Meteor.setTimeout(function () {
        Errors.remove(error._id);
      }, 3000);
    };
    
    client/templates/includes/errors.js

    コミット 9-3

    Clear errors after 3 seconds.

    renderedコールバックは ブラウザでテンプレートがレンダリングされた後に一回だけ実行されます。 コールバック内でthisが参照するのは現在のテンプレートインスタンスで this.dataはレンダリングされたオブジェクトデータにアクセスします。 (この場合はエラー)

    バリーデーション

    これまでのところ、どの種類のフォーム上の検証も強制していません。 最低でも、私たちは、ユーザーがURLとその新しいポストのタイトルの両方を提供したいと思います。 それでは、彼らがそれを行うことを確認してみましょう。

    私たちは、欠落しているフィールドに二つのことをします。 最初は、私たちはあらゆる問題のあるフォームフィールドの親要素divhas-error CSSクラスを付与します。 次に、フィールドの下に有用なエラーメッセージを表示させます。

    開始します。まずは新しいヘルパーを受け入れるために postSubmitテンプレートを下準備しましょう:

    <template name="postSubmit">
      <form class="main form">
        <div class="form-group {{errorClass 'url'}}">
          <label class="control-label" for="url">URL</label>
          <div class="controls">
              <input name="url" id="url" type="text" value="" placeholder="Your URL" class="form-control"/>
              <span class="help-block">{{errorMessage 'url'}}</span>
          </div>
        </div>
        <div class="form-group {{errorClass 'title'}}">
          <label class="control-label" for="title">Title</label>
          <div class="controls">
              <input name="title" id="title" type="text" value="" placeholder="Name your post" class="form-control"/>
              <span class="help-block">{{errorMessage 'title'}}</span>
          </div>
        </div>
        <input type="submit" value="Submit" class="btn btn-primary"/>
      </form>
    </template>
    
    client/templates/posts/post_submit.html

    私たちは(titleurlそれぞれ)各ヘルパーにパラメータを渡していることに注意してください。 これは、パラメータに基づいてその動作を変更する、同じヘルパーの両方のリソースを再利用できます。

    ここからが楽しいところです:これらのヘルパーを実際に作っていきます。

    あらゆる潜在的なエラーメッセージを含むpostSubmitErrorsオブジェクトを格納するためにセッションを使用します。 ユーザーがフォームと対話するように、このオブジェクトは順番にリアクティブに変化していき、 フォームのマークアップと内容を更新します。

    まず、postSubmitテンプレートが作成されるたびにオブジェクトを初期化します。 これにより、ユーザはこのページへの前回の訪問から残された古いエラーメッセージが表示されないことが保証されます。

    次に、2つのテンプレートヘルパーを定義します。 彼らは両方のSession.get('postSubmitErrors')fieldプロパティを見ます。 (fieldurlまたはtitleの事です。ヘルパーを呼んでいる場所に応じて変わります)

    errorMessageは、単にメッセージ自体を返します。 errorClassはメッセージの有無をチェックしメッセージが存在する場合、 has-errorを返します。

    Template.postSubmit.created = function() {
      Session.set('postSubmitErrors', {});
    }
    
    Template.postSubmit.helpers({
      errorMessage: function(field) {
        return Session.get('postSubmitErrors')[field];
      },
      errorClass: function (field) {
        return !!Session.get('postSubmitErrors')[field] ? 'has-error' : '';
      }
    });
    
    client/templates/posts/post_submit.js

    これらのヘルパーは、ブラウザのコンソールを開き、 次のコードを入力することで正常に動作していることをテストできます。:

    Session.set('postSubmitErrors', {title: 'Warning! Intruder detected. Now releasing robo-dogs.'});
    
    Browser console
    Red alert! Red alert!
    Red alert! Red alert!

    次のステップは、postSubmitErrorsセッションオブジェクトをフォームにフックします。

    その前に、postオブジェクトを見てposts.jsで新しい validatePost関数を作成し、 関連するすべてのエラーを含むerrorsオブジェクトを返します。 (すなわち、titleurlフィールドが欠落しているかどうかを判定します):

    //...
    
    validatePost = function (post) {
      var errors = {};
    
      if (!post.title)
        errors.title = "Please fill in a headline";
    
      if (!post.url)
        errors.url =  "Please fill in a URL";
    
      return errors;
    }
    
    //...
    
    lib/collections/posts.js

    postSubmitイベントヘルパーから関数を呼び出します。

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        };
    
        var errors = validatePost(post);
        if (errors.title || errors.url)
          return Session.set('postSubmitErrors', errors);
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return throwError(error.reason);
    
          // show this result but route anyway
          if (result.postExists)
            throwError('This link has already been posted');
    
          Router.go('postPage', {_id: result._id});
        });
      }
    });
    
    client/templates/posts/post_submit.js

    何らかのエラーが存在する場合、どこかにこの値を返す為ではなく、 ヘルパーの実行を中止するために returnを使用していることに注意してください。

    Caught red-handed.
    Caught red-handed.

    サーバサイド・バリデーション

    まだ終わってはいません。クライアントサイドでのURLとタイトルのバリデーション表示をできました。 しかしサーバサイドはどうでしょう? 結局のところ、手動でブラウザコンソールからpostInsertメソッドを呼び出すことによって、 空のポストを入力を試みることができます。

    サーバー上ですべてのエラーメッセージを表示する必要がないにもかかわらず、 同じ validatePost機能を利用することができます。 イベントヘルパーではなく、postInsertメソッド内からそれを呼ぶことにします。

    Meteor.methods({
      postInsert: function(postAttributes) {
        check(this.userId, String);
        check(postAttributes, {
          title: String,
          url: String
        });
    
        var errors = validatePost(postAttributes);
        if (errors.title || errors.url)
          throw new Meteor.Error('invalid-post', "You must set a title and URL for your post");
    
        var postWithSameLink = Posts.findOne({url: postAttributes.url});
        if (postWithSameLink) {
          return {
            postExists: true,
            _id: postWithSameLink._id
          }
        }
    
        var user = Meteor.user();
        var post = _.extend(postAttributes, {
          userId: user._id,
          author: user.username,
          submitted: new Date()
        });
    
        var postId = Posts.insert(post);
    
        return {
          _id: postId
        };
      }
    });
    
    lib/collections/posts.js

    繰り返しますが、ユーザーは通常このメッセージ「You must set a title and URL for your post(投稿のタイトルとURLを設定する必要があります)」を参照する必要はないようにしてください。 誰かが苦労してまとめたユーザーインターフェースをバイパスすることを望んでいる、 と代わりに直接コンソールを使用している場合、それは表示されます。

    これをテストするには、ブラウザコンソールを開き、URLを空欄で投稿を入力してみてください:

    Meteor.call('postInsert', {url: '', title: 'No URL here!'});
    

    適切に仕事をしていれば、「You must set a title and URL for your post」というメッセージとともに、 恐ろしげなコードがたくさん返ってきます。

    コミット 9-4

    Validate post contents on submission.

    編集のバリデーション

    さっさと済ませるために、投稿編集フォームに同じ検証を適用します。 コードはかなりの量になります。まず、テンプレート:

    <template name="postEdit">
      <form class="main form">
        <div class="form-group {{errorClass 'url'}}">
          <label class="control-label" for="url">URL</label>
          <div class="controls">
              <input name="url" id="url" type="text" value="{{url}}" placeholder="Your URL" class="form-control"/>
              <span class="help-block">{{errorMessage 'url'}}</span>
          </div>
        </div>
        <div class="form-group {{errorClass 'title'}}">
          <label class="control-label" for="title">Title</label>
          <div class="controls">
              <input name="title" id="title" type="text" value="{{title}}" placeholder="Name your post" class="form-control"/>
              <span class="help-block">{{errorMessage 'title'}}</span>
          </div>
        </div>
        <input type="submit" value="Submit" class="btn btn-primary submit"/>
        <hr/>
        <a class="btn btn-danger delete" href="#">Delete post</a>
      </form>
    </template>
    
    client/templates/posts/post_edit.html

    そしてテンプレートヘルパー:

    Template.postEdit.created = function() {
      Session.set('postEditErrors', {});
    }
    
    Template.postEdit.helpers({
      errorMessage: function(field) {
        return Session.get('postEditErrors')[field];
      },
      errorClass: function (field) {
        return !!Session.get('postEditErrors')[field] ? 'has-error' : '';
      }
    });
    
    Template.postEdit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var currentPostId = this._id;
    
        var postProperties = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        }
    
        var errors = validatePost(postProperties);
        if (errors.title || errors.url)
          return Session.set('postEditErrors', errors);
    
        Posts.update(currentPostId, {$set: postProperties}, function(error) {
          if (error) {
            // display the error to the user
            throwError(error.reason);
          } else {
            Router.go('postPage', {_id: currentPostId});
          }
        });
      },
    
      'click .delete': function(e) {
        e.preventDefault();
    
        if (confirm("Delete this post?")) {
          var currentPostId = this._id;
          Posts.remove(currentPostId);
          Router.go('postsList');
        }
      }
    });
    
    client/templates/posts/post_edit.js

    我々は、フォームをサブミット後に行った作業のように、 また、サーバ上で私たちの投稿を検証したいと思う。 あなたは投稿を編集するメソッドを用意せず、 クライアントから直接呼び出すupdateを使ったことを覚えているだろうか。

    これは、代わりに新しいdenyコールバックを追加する必要があることを意味します:

    //...
    
    Posts.deny({
      update: function(userId, post, fieldNames, modifier) {
        var errors = validatePost(modifier.$set);
        return errors.title || errors.url;
      }
    });
    
    //...
    
    lib/collections/posts.js

    post引数は既存のポストを参照していることに注意してください。 この場合、更新が(Posts.update({$set: {title: ..., url: ...}})のように) modifier$setプロパティの内容にvalidatePostを呼んでいる理由を、検証したい。 修飾子$setpostオブジェクト全体にurlurlプロパティが含まれているため、 これが動作します。もちろん、それは部分的な更新がtitleだけだったりに影響を与えたり、 urlだけの場合は失敗しますが、実際には問題にはらないでしょう。

    あなたは、第二denyコールバックに気づくかもしれません。 複数のdenyコールバックを追加する場合、 それらのいずれかがtrueを返した場合、操作は失敗します。 この場合には、titleurlが更新対象だった場合、 もうひとつがからでなければupdateは成功します。

    コミット 9-5

    Validate post contents when editing.

    Meteorパッケージの作成

    Sidebar 9.5

    私たちは、エラーが動作して再利用可能なパターンを構築しましたので、 スマートパッケージにそれをパッケージ化し、 Meteorのコミュニティとそれを共有しませんか?

    開始するには、我々はMeteorデベロッパーアカウントを持っていることを確認する必要があります。 meteor.comにて作れます。 ですがこの本のためにサインアップしたときは、すでにそのようにしたかもしれません! いずれの場合でも、この章で、アカウントを頻繁に使用するので、 ユーザ名が何であるかを把握する必要があります。

    この章では、ユーザ名tmeasdayを使用します - あなた自身のアカウントに置き換えてください。

    まず、パッケージのためのいくつかの構造を作成する必要があります。 これを行うには、 meteor create --package tmeasday:errorsコマンドを使用します。 Meteorはpackages/tmeasday:errors/というフォルダ名を作成し、 内部のいくつかのファイルを作成することに注意してください。 ファイルpackage.jsを編集するところから始めます。 それは、パッケージをどのように使用するかのMeteorを知らせ、 オブジェクトもしくは関数を必要とあれば、エクスポートします。

    Package.describe({
      name: "tmeasday:errors",
      summary: "A pattern to display application errors to the user",
      version: "1.0.0"
    });
    
    Package.onUse(function (api, where) {
      api.versionsFrom('0.9.0');
    
      api.use(['minimongo', 'mongo-livedata', 'templating'], 'client');
    
      api.addFiles(['errors.js', 'errors_list.html', 'errors_list.js'], 'client');
    
      if (api.export)
        api.export('Errors');
    });
    
    packages/tmeasday:errors/package.js

    実世界の使用のためにパッケージを開発するとき、 あなたレポジトリのGitのURLでPackage.describeブロックのgitセクションを埋めるのは 良い練習になります。(例えばhttps://github.com/tmeasday/meteor-errors.git) ユーザーがソースコードを読むことができます、そして、(あなたがGitHubを使っているなら) AtmosphereにあなたのパッケージのReadmeが表示されます。

    パッケージに3つのファイルを追加してみましょう。私たちは、いくつかの適切な名前空間と 少しのクリーナーAPIを除きあまり変化せずにMicroscopeからこれらのファイルを引き抜くことができます。 (Meteor配下のコードを削除し共通化できるということです):

    Errors = {
      // Local (client-only) collection
      collection: new Mongo.Collection(null),
    
      throw: function(message) {
        Errors.collection.insert({message: message, seen: false})
      }
    };
    
    packages/tmeasday:errors/errors.js
    <template name="meteorErrors">
      <div class="errors">
        {{#each errors}}
          {{> meteorError}}
        {{/each}}
      </div>
    </template>
    
    <template name="meteorError">
      <div class="alert alert-danger" role="alert">
        <button type="button" class="close" data-dismiss="alert">&times;</button>
        {{message}}
      </div>
    </template>
    
    packages/tmeasday:errors/errors_list.html
    Template.meteorErrors.helpers({
      errors: function() {
        return Errors.collection.find();
      }
    });
    
    Template.meteorError.rendered = function() {
      var error = this.data;
      Meteor.setTimeout(function () {
        Errors.collection.remove(error._id);
      }, 3000);
    };
    
    packages/tmeasday:errors/errors_list.js

    Microscopeでパッケージをテスト

    今、私たちは、変更されたコードが動作することを確認するために、 Microscopeを使用してローカルにテストします。 私たちのプロジェクトにパッケージをリンクするために、我々はmeteor add tmeasday:errorsを実行します。 その後、新しいパッケージにより冗長化されてきた既存のファイルを削除する必要があります。

    rm client/helpers/errors.js
    rm client/templates/includes/errors.html
    rm client/templates/includes/errors.js
    
    removing old files on the bash console

    もう一つ行う事は適切なAPIを使用するためにいくつかのマイナーなアップデートをすることです:

      {{> header}}
      {{> meteorErrors}}
    
    client/templates/application/layout.html
    Meteor.call('postInsert', post, function(error, id) {
      if (error) {
        // display the error to the user
        Errors.throw(error.reason);
    
    
    client/templates/posts/post_submit.js
    Posts.update(currentPostId, {$set: postProperties}, function(error) {
      if (error) {
        // display the error to the user
        Errors.throw(error.reason);
    
    client/templates/posts/post_edit.js

    コミット 9-5-1

    Created basic errors package and linked it in.

    これらの変更が行われた後、私たちは私たちのオリジナルのプレパッケージの振る舞いが戻ってるはずです。

    テストを書く

    パッケージを開発する際の最初のステップで、アプリケーションに対してテストしてますが、 次は正常にパッケージの動作をテストするテストスイートを書きます。 Meteorに付属しているTinytest(パッケージ用に作られました。)を使います。 これは簡単にテストを実行でき、他の人とパッケージを共有するときに心の平和を維持することを可能にします。

    いくつかのテストを実行するためにTinytestを使用してテストファイルを作成してみましょう:

    Tinytest.add("Errors - collection", function(test) {
      test.equal(Errors.collection.find({}).count(), 0);
    
      Errors.throw('A new error!');
      test.equal(Errors.collection.find({}).count(), 1);
    
      Errors.collection.remove({});
    });
    
    Tinytest.addAsync("Errors - template", function(test, done) {
      Errors.throw('A new error!');
      test.equal(Errors.collection.find({}).count(), 1);
    
      // render the template
      UI.insert(UI.render(Template.meteorErrors), document.body);
    
      Meteor.setTimeout(function() {
        test.equal(Errors.collection.find({}).count(), 0);
        done();
      }, 3500);
    });
    
    packages/tmeasday:errors/errors_tests.js

    テストで基本的なMeteor.Errors関数の動きをチェックします。 同様にテンプレート内でrenderedコードがまだ機能している事をダブルチェックします。

    我々は、(APIがまだ確定しておらず改変の可能性があるため)ここでMeteorパッケージテストを書く詳細をカバーしていませんが、 うまくいけばそれがどのように動作するか、かなり自明にできます。

    Meteorがpackage.jsでテストをどのように動かすか指示可能です。以下のコードを確認ください。:

    Package.onTest(function(api) {
      api.use('tmeasday:errors', 'client');
      api.use(['tinytest', 'test-helpers'], 'client');
    
      api.addFiles('errors_tests.js', 'client');
    });
    
    packages/tmeasday:errors/package.js

    コミット 9-5-2

    Added tests to the package.

    以下のようにテストを動かします。

    meteor test-packages tmeasday:errors
    
    Terminal
    Passing all tests
    Passing all tests

    パッケージの置き換え

    今、パッケージをリリースし、世界でそれを利用できるようにします。私たちは、 Meteorのパッケージサーバーにそれをプッシュし、Atmopshereから取得できるようにします。

    幸いなことに、それは非常に簡単です。cdで、パッケージのディレクトリに入り、 meteor publish --createを実行します。:

    cd packages/tmeasday:errors
    meteor publish --create
    
    Terminal

    今すぐパッケージがリリースされていることを、プロジェクトから一旦削除して、再度追加し、 確認することができます。

    rm -r packages/errors
    meteor add tmeasday:errors
    
    Terminal (run from the top level of the app)

    コミット 9-5-4

    Removed package from development tree.

    今、私たちはMeteorが初めて私達のパッケージをダウンロードし表示されるはずです。よくやった!

    コメント機能

    10

    ソーシャルニュースサイトの目的はユーザーのコミュニティを作ることであり、 ユーザー同士が話し合える方法を提供しなければ、ユーザーコミュニティを作ることは難しいでしょう。 この章では、コメントを追加します!

    では、コメントを保存するための新しいコレクションを作って、 基本的なテストデータをコレクションに入れることから始めていきましょう。

    Comments = new Mongo.Collection('comments');
    
    lib/collections/comments.js
    // Fixture data
    if (Posts.find().count() === 0) {
      var now = new Date().getTime();
    
      // create two users
      var tomId = Meteor.users.insert({
        profile: { name: 'Tom Coleman' }
      });
      var tom = Meteor.users.findOne(tomId);
      var sachaId = Meteor.users.insert({
        profile: { name: 'Sacha Greif' }
      });
      var sacha = Meteor.users.findOne(sachaId);
    
      var telescopeId = Posts.insert({
        title: 'Introducing Telescope',
        userId: sacha._id,
        author: sacha.profile.name,
        url: 'http://sachagreif.com/introducing-telescope/',
        submitted: new Date(now - 7 * 3600 * 1000)
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: tom._id,
        author: tom.profile.name,
        submitted: new Date(now - 5 * 3600 * 1000),
        body: 'Interesting project Sacha, can I get involved?'
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: sacha._id,
        author: sacha.profile.name,
        submitted: new Date(now - 3 * 3600 * 1000),
        body: 'You sure can Tom!'
      });
    
      Posts.insert({
        title: 'Meteor',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://meteor.com',
        submitted: new Date(now - 10 * 3600 * 1000)
      });
    
      Posts.insert({
        title: 'The Meteor Book',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://themeteorbook.com',
        submitted: new Date(now - 12 * 3600 * 1000)
      });
    }
    
    server/fixtures.js

    新しく作ったコレクションにパブリッシュとサブスクリプションをすることを忘れずに。

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    Meteor.publish('comments', function() {
      return Comments.find();
    });
    
    server/publications.js
    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() {
        return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
      }
    });
    
    lib/router.js

    コミット 10-1

    Added comments collection, pub/sub and fixtures.

    この固定データ読み込みを実行するために、データベースをクリアするmeteor resetを使う必要があります。 リセット後に、新しいアカウントを作ってログインし直すことを忘れずに!

    最初に、私たちは(完全に偽物の)二人のユーザーを作りました。そして、 データベースに二人のデータを挿入して、その後でデータベースからデータを選択するためにidを使います。 それから私たちは最初の投稿にユーザーがお互いにコメントを追加しました。投稿にコメントをリンクして(postIdを使って) この時は、postIdを使って投稿とuserIdを使ってユーザーへのコメントにリンクしています。 私たちは非正規化したフィールドであるauthorと一緒に投稿日時と本文を各コメントに追加しました。 

    また、私たちはルーターを増やしてコメントと投稿を待つようにしました。

    コメントを表示する

    データベースにコメントを入れるのは良いのですが、私たちはディスカッションページにコメントを表示する必要があります。 そろそろこのプロセスに慣れてきているころでしょうか。 というのも、あなたはこのステップに関連したアイデアをすでに持っているからです。

    <template name="postPage">
      {{> postItem}}
    
      <ul class="comments">
        {{#each comments}}
          {{> commentItem}}
        {{/each}}
      </ul>
    </template>
    
    client/templates/posts/post_page.html
    Template.postPage.helpers({
      comments: function() {
        return Comments.find({postId: this._id});
      }
    });
    
    client/templates/posts/post_page.js

    私たちは投稿のテンプレート内に{{#each comments}}ブロックを置いたので、thiscommentsヘルパー内の投稿です。 関連するコメントを見つけるために、私たちはpostId属性を使って投稿にリンクされたコメントをチェックします。   ヘルパーとSpacebarsを学習したことを考えると、コメントをレンダリングすることはかなり簡単です。 すべてのコメントの情報を格納するために、私たちはtemplates内に新しくcommentsディレクトリを作りました。

    <template name="commentItem">
      <li>
        <h4>
          <span class="author">{{author}}</span>
          <span class="date">on {{submittedText}}</span>
        </h4>
        <p>{{body}}</p>
      </li>
    </template>
    
    client/templates/comments/comment_item.html

    それでは、よりユーザーフレンドリーなフォーマットにsubmitted日付をフォーマットするため、 手早くテンプレートヘルパーを設定しましょう:

    Template.commentItem.helpers({
      submittedText: function() {
        return this.submitted.toString();
      }
    });
    
    client/templates/comments/comment_item.js

    それから、それぞれの投稿にコメントの数を表示します。:

    <template name="postItem">
      <div class="post">
        <div class="post-content">
          <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
          <p>
            submitted by {{author}},
            <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
            {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
          </p>
        </div>
        <a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
      </div>
    </template>
    
    client/templates/posts/post_item.html

    そして、commentsCountヘルパーをpost_item.jsに追加します。:

    Template.postItem.helpers({
      ownPost: function() {
        return this.userId === Meteor.userId();
      },
      domain: function() {
        var a = document.createElement('a');
        a.href = this.url;
        return a.hostname;
      },
      commentsCount: function() {
        return Comments.find({postId: this._id}).count();
      }
    });
    
    client/templates/posts/post_item.js

    コミット 10-2

    Display comments on `postPage`.

    これで、固定データのコメントを表示して見ることができるはずです:

    Displaying comments
    Displaying comments

    コメントをサブミットする

    ユーザーが新しいコメント作るための方法を追加します。 私たちがたどるプロセスはユーザーが新しい投稿を作成する許可する方法にとても似ています。 

    私たちは各投稿の下にコメント投稿ボックスを作ることから始めていきましょう。 

    <template name="postPage">
      {{> postItem}}
    
      <ul class="comments">
        {{#each comments}}
          {{> commentItem}}
        {{/each}}
      </ul>
    
      {{#if currentUser}}
        {{> commentSubmit}}
      {{else}}
        <p>Please log in to leave a comment.</p>
      {{/if}}
    </template>
    
    client/templates/posts/post_page.html

    それからコメントフォームテンプレートを作ります。:

    <template name="commentSubmit">
      <form name="comment" class="comment-form form">
        <div class="form-group {{errorClass 'body'}}">
            <div class="controls">
                <label for="body">Comment on this post</label>
                <textarea name="body" id="body" class="form-control" rows="3"></textarea>
                <span class="help-block">{{errorMessage 'body'}}</span>
            </div>
        </div>
        <button type="submit" class="btn btn-primary">Add Comment</button>
      </form>
    </template>
    
    client/templates/comments/comment_submit.html
    The comment submit form
    The comment submit form

    コメントをサブミットするために、投稿をサブミットするためにしたものと同様の方法で、 comment_submit.jscommentメソッドを呼び出します。:

    Template.commentSubmit.created = function() {
      Session.set('commentSubmitErrors', {});
    }
    
    Template.commentSubmit.helpers({
      errorMessage: function(field) {
        return Session.get('commentSubmitErrors')[field];
      },
      errorClass: function (field) {
        return !!Session.get('commentSubmitErrors')[field] ? 'has-error' : '';
      }
    });
    
    Template.commentSubmit.events({
      'submit form': function(e, template) {
        e.preventDefault();
    
        var $body = $(e.target).find('[name=body]');
        var comment = {
          body: $body.val(),
          postId: template.data._id
        };
    
        var errors = {};
        if (! comment.body) {
          errors.body = "Please write some content";
          return Session.set('commentSubmitErrors', errors);
        }
    
        Meteor.call('commentInsert', comment, function(error, commentId) {
          if (error){
            throwError(error.reason);
          } else {
            $body.val('');
          }
        });
      }
    });
    
    client/templates/comments/comment_submit.js

    以前、postをサーバーサイドのMeteorメソッドをセットアップしたように、 コメントを作るためにcommentMeteorメソッドをセットアップして、 すべてがちゃんとしているかチェックして、最後に新しいコメントをcommentsコレクションに挿入します。

    Comments = new Mongo.Collection('comments');
    
    Meteor.methods({
      commentInsert: function(commentAttributes) {
        check(this.userId, String);
        check(commentAttributes, {
          postId: String,
          body: String
        });
    
        var user = Meteor.user();
        var post = Posts.findOne(commentAttributes.postId);
    
        if (!post)
          throw new Meteor.Error('invalid-comment', 'You must comment on a post');
    
        comment = _.extend(commentAttributes, {
          userId: user._id,
          author: user.username,
          submitted: new Date()
        });
    
        return Comments.insert(comment);
      }
    });
    
    lib/collections/comments.js

    コミット 10-3

    Created a form to submit comments.

    ここでは手が込んだことは何もしていません。 ただユーザーがログインしているか、コメントに本文があるか、コメントが投稿にリンクされているかチェックしています。

    The comment submit form
    The comment submit form

    コメントのサブスクリプションの操作

    今のところ、私たちはすべての投稿にすべてのコメントをパブリッシュしています。 これはちょっと無駄が多いように見えます。 結局、私たちはどんなときでもコメントデータの小さな一部分を使うだけなので、 どのコメントをパブリッシュさせるかコントロールするために パブリケーションとサブスクリプションを改善していきましょう。  

    この点を考えると、commentsのパブリケーションにサブスクライブする必要のある時というのは、 ユーザーが投稿の個別ページにアクセスする時だけなので、 私たちは特定の投稿に関係するコメント部分だけを読み込む必要があります。

    最初のステップとして、私たちはコメントにサブスクライブする方法を変えていきます。 今まで私たちはルーターレベルでサブスクライブしていました。 つまり、私たちはルーターが初期化するときにすべてのデータを一度に読み込んでいます。

    しかし、私たちはサブスクリプションがパスのパラメータで決めるようにしたいのです。 パラメータはどの時点でも明確に変えることができます。 そのため、私たちはサブスクリプションコードをルーターレベルからルートレベルに 移行する必要があります。

    これは別の結果です:アプリを初期化する時にデータをロードするかわりに、 今の私たちはルートにヒットした時はいつもロードしています。 これはアプリ内を見ている間にロード時間またせてしまいます、 しかし、これはあなたが永遠にすべてのデータセットをフロント側にもたせるつもりでない限り、 避けられないマイナス面です。

    最初に、私たちはconfigureブロックでMeteor.subscribe('comments')を削除して コメントをあらかじめ読み込むことをストップします(前の状態に戻すと言い換えてもいいでしょう) :

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() {
        return Meteor.subscribe('posts');
      }
    });
    
    lib/router.js

    それから、新たにルートレベルのwaitOn関数をpostPageルートへ追加します。

    //...
    
    Router.route('/posts/:_id', {
      name: 'postPage',
      waitOn: function() {
        return Meteor.subscribe('comments', this.params._id);
      },
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    //...
    
    lib/router.js

    私たちはthis.params._idをサブスクリプションへの引数として渡していることにお気づきかと思います。 そのため、現在の投稿に帰属しているコメントへのデータセットを制限するために新しい情報を使いましょう。:

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    Meteor.publish('comments', function(postId) {
      check(postId, String);
      return Comments.find({postId: postId});
    });
    
    server/publications.js

    コミット 10-4

    Made a simple publication/subscription for comments.

    1つだけ問題があります:ホームページに戻るときに、すべての投稿でコメントは0だと言っています。

    Our comments are gone!
    Our comments are gone!

    コメントのカウント

     この理由はすぐに明らかになります: 唯一postPageルート上でコメントを読み込むので、 commentsCountヘルパー内でComments.find({postId:this._id})を呼び出すときに Meteorは私たちを提供するために必要なクライアント側のデータを見つけることができません。

    この問題に対処する最も良い方法は、投稿にコメントの数を非正規化することです。 (これが何を意味しているのか自信がなくでも、問題ありません。次の補足事項でカバーします!) これまで見てきたように、コードにちょっとだけ複雑なものを追加していますが、 投稿リストを表示する上で、すべてのコメントにパブリッシュする必要がなくなったことで、 私たちが得るパフォーマンスベネフィットには、それだけの価値があります。

    postデータ構造にcommentsCountを追加することで これを実現します。 はじめに、私たちは投稿のテストデータをアップデートします。 (さらに、これをリロードするために meteor reset をします。後でユーザーアカウントを作り直すことを忘れずに。):

    // Fixture data
    if (Posts.find().count() === 0) {
      var now = new Date().getTime();
    
      // create two users
      var tomId = Meteor.users.insert({
        profile: { name: 'Tom Coleman' }
      });
      var tom = Meteor.users.findOne(tomId);
      var sachaId = Meteor.users.insert({
        profile: { name: 'Sacha Greif' }
      });
      var sacha = Meteor.users.findOne(sachaId);
    
      var telescopeId = Posts.insert({
        title: 'Introducing Telescope',
        userId: sacha._id,
        author: sacha.profile.name,
        url: 'http://sachagreif.com/introducing-telescope/',
        submitted: new Date(now - 7 * 3600 * 1000),
        commentsCount: 2
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: tom._id,
        author: tom.profile.name,
        submitted: new Date(now - 5 * 3600 * 1000),
        body: 'Interesting project Sacha, can I get involved?'
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: sacha._id,
        author: sacha.profile.name,
        submitted: new Date(now - 3 * 3600 * 1000),
        body: 'You sure can Tom!'
      });
    
      Posts.insert({
        title: 'Meteor',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://meteor.com',
        submitted: new Date(now - 10 * 3600 * 1000),
        commentsCount: 0
      });
    
      Posts.insert({
        title: 'The Meteor Book',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://themeteorbook.com',
        submitted: new Date(now - 12 * 3600 * 1000),
        commentsCount: 0
      });
    }
    
    server/fixtures.js

    固定ファイル(fixtures.js)を更新するときにいつものように、 データベースをmeteor resetして再実行される事を確認する必要があります。

    次に、すべての新しい投稿が0コメントからスタートするようにします。: 

    //...
    
    var post = _.extend(postAttributes, {
      userId: user._id,
      author: user.username,
      submitted: new Date(),
      commentsCount: 0
    });
    
    var postId = Posts.insert(post);
    
    //...
    
    collections/posts.js

    それから、MongoDBの$inc演算子を使うことで新しいコメントを作る時に、 関係するcommentsCountを更新します。(これで数値に関するフィールドを1つずつ増加させます。): 

    //...
    
    comment = _.extend(commentAttributes, {
      userId: user._id,
      author: user.username,
      submitted: new Date()
    });
    
    // update the post with the number of comments
    Posts.update(comment.postId, {$inc: {commentsCount: 1}});
    
    return Comments.insert(comment);
    
    //...
    
    collections/comments.js

    最後に、postのフィールドに直接アクセスするため、単純にclient/templates/posts/post_item.jsからcommentsCount helperを削除します。

    コミット 10-5

    Denormalized the number of comments into the post.

    これでユーザー同士で話し合えるようになったので、ユーザーが新しいコメントに気づかないとしたら、 残念なことです。なんと驚くことに、次の章はこれを防ぐために通知を実装する方法を説明します!。

    非正規化

    Sidebar 10.5

    データを非正規化するとは、「通常」の形式でそのデータを格納しないことを意味します。 言い換えれば、非正規化は、ぶら下がった同じデータの複数のコピーを有することを意味します。

    前の章では、我々はすべてのコメントを時間かけてロードすることを避けるために、 postオブジェクトにコメントの数を非正規化しました。 データモデリングの意味では、その値を把握するために、 いつでもコメントの正しい組み合わせを数えることができるようにするには、冗長です。 (パフォーマンスの考慮を除外しています)

    非正規化は、多くの場合、開発者のための余分な作業を意味します。 この例では、我々はコメントを追加または削除するたびに、 commentsCountフィールドが正確のままだと保証するために、関連する投稿を更新した事を覚えておく必要があります。 これはまさに、MySQL等のリレーショナルデータベースはしかめ面になるアプローチです。

    しかし、通常のアプローチは、欠点があります。: commentsCountプロパティなしで、私たちが最初に何をしていたか思い出してください。 コメントを数えることができるように、毎回、紐付いているすべてのコメントを送信する必要がでてきます。 非正規化は、完全にこれを避けることができます。

    特別なパブリケーション

    私たちは、興味を持っているコメント数を取得し特殊なパブリケーションを作成することも可能でしょう。 (例えば、現在、サーバー上に集約クエリを経由して、見ることができる投稿のコメントカウント)

    しかし、それはこのようなパブリケーションコードの複雑さが、 非正規化によって作成された困難を上回るならば検討する価値があります..

    もちろん、このような考察は、アプリケーション事に異なります。: もし、データの整合性が最重要なコードを記述している場合、パフォーマンスの向上より、 データの不整合を回避することがはるかに重要かつ優先順位の高いです。

    ドキュメントに埋め込むべき vs 複数のコレクションを使う

    もしMongoDBの経験がある方ならば場合は、 コメント用に第二のコレクションを作成したことを見て驚いたかもしれません: なぜ投稿ドキュメント内のリスト内へコメントを埋め込まなかったのでしょう?

    これは、コレクションレベルで動作しているときのMeteorツールの多くは、 たくさんの良い仕事が得られることが判明したのです。たとえば:

    1. カーソルを反復処理(collection.find()の結果に対して)するとき{{#each}}ヘルパーは非常に効率的でした。 それはより大きな文書内のオブジェクトの配列を反復処理するときは同じことが当てはまりません。
    2. allowdenyの操作はドキュメントレベルの方が容易です。 個別のコメントの変更が正しいことを保証するのをpostレベルで操作するのはより複雑です。
    3. DDPは、文書の最上位の属性のレベルで動作します。–それの意味するところは、 commentspostのプロパティであった場合、コメントがポストに作成されるたびに、 サーバーが接続されている各クライアントに、そのポストの全体の更新されたコメントの一覧を送信します。
    4. パブリケーションとサブスクリプションは、文書のレベルで制御することが非常に簡単です。 例えば、 ある投稿のコメントをページ分割したい場合、コメントが専用のコレクション無ければ、 実装が難しい事がわかるでしょう。

    Mongoは文書を取り出すための高価なクエリ数を削減するために文書に埋め込むことを提案します。 しかし、少なくともそれはMeteorのアーキテクチャではアカウントを取る場合くらいです。 ほとんどの時間は、基本的に自由であるクライアントサイドのデータベースアクセスでコメントを照会しているからです。

    非正規化のマイナス面

    あなたのデータを非正規化するべきではないと判断されるのは良い議論です。 非正規化に対する良いドキュメントとしてSarah MeiのWhy You Should Never Use MongoDB があります。

    通知機能

    11

    これでユーザーはお互いの投稿にコメントできるようになったので、 会話が始まったことを知らせると良いでしょう。 

    そのようにするために、投稿にコメントが付いたことを投稿者に知らせて、 そのコメントを見るためのリンクを提供します。

    これは Meteor の機能がよく輝くところです: Meteor はデフォルトでリアルタイムなので、そうした通知は瞬時に表示されます。 ユーザーがページを再読み込みしたり、チェックしたりすることを私たちは待つ必要はありません。 特別なコードを書くことなく、新しい通知をポップアップできるからです。   

    通知を作成

    投稿に誰かがコメントをした時の通知を作っていきます。 将来的には、通知を多くのシナリオをカバーするように拡張できますが、 今のところはユーザーに何が起きているか知らせることで十分でしょう。

    では、Notificationsコレクションを作っていきましょう。 自分の投稿の新しいコメントごとにマッチする通知を挿入する createCommentNotification関数を作っていきます。

    クライアントからの通知を更新されますので、 allow呼び出しが防弾(?bulletproof)であることを確認する必要があります。:

    • update呼び出しを行うユーザーは、変更されている通知を所有している。
    • ユーザーは、単一のフィールドを更新しようとしている。
    • その単一のフィールドは、私たちの通知のreadプロパティです。
    Notifications = new Mongo.Collection('notifications');
    
    Notifications.allow({
      update: function(userId, doc, fieldNames) {
        return ownsDocument(userId, doc) &&
          fieldNames.length === 1 && fieldNames[0] === 'read';
      }
    });
    
    createCommentNotification = function(comment) {
      var post = Posts.findOne(comment.postId);
      if (comment.userId !== post.userId) {
        Notifications.insert({
          userId: post.userId,
          postId: post._id,
          commentId: comment._id,
          commenterName: comment.author,
          read: false
        });
      }
    };
    
    lib/collections/notifications.js

    投稿やコメントと同じように、Notificationsコレクションはクライアントとサーバーの両方で共有されます。 一度ユーザーが通知を見た時に、私たちは通知を更新する必要があるので、アップデートできるようにします。 いつものように、更新許可をユーザー自身のデータにリダイレクトするようにします。

    私たちはシンプルな関数を作りました。これはユーザーがコメントしている投稿に目を向け、 誰がそこから通知されるか見つけて、新しい通知を挿入する関数です。

    私たちはすでに サーバーサイドのメソッドでコメントを作っているので、 関数を呼び出すために、このメソッドを増やすことができます。 新しく作ったコメントの_idを変数に保存するため、 return Comments.insert(comment);comment._id = Comments.insert(comment)に  取り替えます。それから、createCommentNotification関数を呼び出します。:

    Comments = new Mongo.Collection('comments');
    
    Meteor.methods({
      commentInsert: function(commentAttributes) {
    
        //...
    
        comment = _.extend(commentAttributes, {
          userId: user._id,
          author: user.username,
          submitted: new Date()
        });
    
        // update the post with the number of comments
        Posts.update(comment.postId, {$inc: {commentsCount: 1}});
    
        // create the comment, save the id
        comment._id = Comments.insert(comment);
    
        // now create a notification, informing the user that there's been a comment
        createCommentNotification(comment);
    
        return comment._id;
      }
    });
    
    lib/collections/comments.js

    通知もパブリッシュしましょう:

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    Meteor.publish('comments', function(postId) {
      check(postId, String);
      return Comments.find({postId: postId});
    });
    
    Meteor.publish('notifications', function() {
      return Notifications.find();
    });
    
    server/publications.js

    そしてクライアントでサブスクライブします。:

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() {
        return [Meteor.subscribe('posts'), Meteor.subscribe('notifications')]
      }
    });
    
    lib/router.js

    コミット 11-1

    Added basic notifications collection.

    Displaying Notifications

    これで私たちは前に進むことができるようになり、ヘッダーに通知のリストを追加できます。

    <template name="header">
      <nav class="navbar navbar-default" role="navigation">
        <div class="container-fluid">
          <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
          </div>
          <div class="collapse navbar-collapse" id="navigation">
            <ul class="nav navbar-nav">
              {{#if currentUser}}
                <li>
                  <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
                </li>
                <li class="dropdown">
                  {{> notifications}}
                </li>
              {{/if}}
            </ul>
            <ul class="nav navbar-nav navbar-right">
              {{> loginButtons}}
            </ul>
          </div>
        </div>
      </nav>
    </template>
    
    client/templates/includes/header.html

    そして、notifications テンプレートと notification テンプレートをつくります。 (この2つは1つの notifications.html ファイルを共同で利用します。):

    <template name="notifications">
      <a href="#" class="dropdown-toggle" data-toggle="dropdown">
        Notifications
        {{#if notificationCount}}
          <span class="badge badge-inverse">{{notificationCount}}</span>
        {{/if}}
        <b class="caret"></b>
      </a>
      <ul class="notification dropdown-menu">
        {{#if notificationCount}}
          {{#each notifications}}
            {{> notificationItem}}
          {{/each}}
        {{else}}
          <li><span>No Notifications</span></li>
        {{/if}}
      </ul>
    </template>
    
    <template name="notificationItem">
      <li>
        <a href="{{notificationPostPath}}">
          <strong>{{commenterName}}</strong> commented on your post
        </a>
      </li>
    </template>
    
    client/templates/notifications/notifications.html

    私たちはコメントされた投稿へのリンクがあるそれぞれの通知のための画面 とコメントされた投稿のユーザーの名前を見ることができます。 それぞれの通知がコメントした投稿へのリンク、 およびその上でコメントしたユーザーの名前を含むようにするための画面があることがわかります。

      次に、私たちはマネージャーで、通知の正しいリストを選んで、 ユーザーがポイントしているリンクをクリックした時に、通知を"read"としてアップデートする必要があります。

    Template.notifications.helpers({
      notifications: function() {
        return Notifications.find({userId: Meteor.userId(), read: false});
      },
      notificationCount: function(){
        return Notifications.find({userId: Meteor.userId(), read: false}).count();
      }
    });
    
    Template.notificationItem.helpers({
      notificationPostPath: function() {
        return Router.routes.postPage.path({_id: this.postId});
      }
    });
    
    Template.notificationItem.events({
      'click a': function() {
        Notifications.update(this._id, {$set: {read: true}});
      }
    });
    
    client/templates/notifications/notifications.js

    コミット 11-2

    Display notifications in the header.

    通知はエラーと違って、それほど難しくないと思うかもしれません。 確かにこの2つはとても似ている構造をしています。 しかし、1つだけキーとなる違いがあります: 私たちはクライアント-サーバー間を同期した適切なコレクションを作りました。 これは通知が永続的であるという意味であり、 私たちが同じユーザーアカウントを使う限り、 再読み込みするブラウザでも違うデバイスでもこの通知は健在です。

    試しにやってみます: 2つめのブラウザーを開きます。(Firefoxだとしましょう) 新しいユーザーアカウントを作って、あなたのメインアカウントで作った投稿にコメントします。 (Chromeを開いたままで)すると、このようになっているはずです。

    Displaying notifications.
    Displaying notifications.

    通知へのアクセスをコントロールします。

    通知はうまく動いています。しかし、少し問題があります:この通知は公開されているのです。

    2つめのブラウザーを開いたままだとしたら、ブラウザーコンソールで次のようなコードを動かしてみましょう。

     Notifications.find().count();
    1
    
    Browser console

    (コメントした)この新しいユーザーはすべての通知を持つべきではありません。 ユーザーがNotificationsコレクションで見ることができる通知は originalなユーザーに属しているはずです。

    潜在的なプライベートの問題はさておき、 私たちはすべてのユーザーがブラウザで読み込まれるすべてのユーザーの通知を保有する余裕はありません。 十分に大きなサイトでは、 これはブラウザが使用できるメモリをオーバーロードして、深刻なパフォーマンスの問題をもたらすことになります。

    この問題はパブリケーションで解決します。 各ブラウザーで共有させたいコレクションの部分を正確に規定するために、私たちはパブリケーションを使います。

    これをするために、私たちはNotifications.find()よりも パブリケーションで違ったカーソルを返す必要があります。 つまり、私たちは現在のユーザーの通知に対応するカーソルを返したいのです。 

    そうすることは、十分に簡単です。 publish関数はthis.userIdで使うことができる現在のユーザーの_idを持つためです。

    Meteor.publish('notifications', function() {
      return Notifications.find({userId: this.userId, read: false});
    });
    
    server/publications.js

    コミット 11-3

    Only sync notifications that are relevant to the user.

    私たちが 2つのブラウザーウィンドウをチェックすると、 2つの異なるnotificationsコレクションを見ることができます。:

     Notifications.find().count();
    1
    
    Browser console (user 1)
     Notifications.find().count();
    0
    
    Browser console (user 2)

    実際に、アプリにログインしたりログアウトした時に、通知のリストが変化するべきです。  それはユーザーアカウントが変化するたびに、 パブリケーションが自動的に再パブリッシュするためです。

    我々のアプリは、ますます機能的になりつつあり、多くのユーザーが参加し、 リンクの投稿を開始として、我々は決して終わることのないホームページで終わる危険性があります。 私たちは、ページネーションを実装することによって、次の章でこれに対処します。

    高度なリアクティビティ

    Sidebar 11.5

    自分で依存性の追跡コードを記述する必要があることはまれですが、 依存関係解決の流れが動作する方法を追跡するためにそれを理解することは確かに便利です。

    Microscope上の各投稿を「Like」したか、 現在のユーザーのFacebookの友人の多くを追跡したいと思っていると仮定します。 すでに適切なAPI呼び出しを行い、Facebookとユーザーを認証し、 関連データを解析する方法の詳細は動いていて、 現在、「Like」の数を返すクライアントサイドの非同期関数、 getFacebookLikeCount(user, url, callback)を持っているとします。

    このような関数について覚えておくべき重要なことは、 それが非常に非リアクティブで非リアルタイムであるということです。

    これは、FacebookへHTTPリクエストを行い、いくつかのデータを取得します。 そして、非同期コールバック内でアプリケーションが使用できるようにします。 しかし、カウントするFacebookで切り替わるときに関数は、 それ自身によって再実行されませんし、基になるデータがないとUIが変更されません。

    これを修正するには、我々は数秒毎に関数を呼び出すために、 setIntervalを使用して起動することができます。:

    currentLikeCount = 0;
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId).url,
          function(err, count) {
            if (!err)
              currentLikeCount = count;
          });
      }
    }, 5 * 1000);
    

    いつでも、 変数currentLikeCountをチェックし、 5秒の余裕をもって正しい番号を取得することを期待することができます。 私たちは、今、ヘルパーでこの変数を使用することができます:

    Template.postItem.likeCount = function() {
      return currentLikeCount;
    }
    

    しかし、何もまだcurrentLikeCountが変化した時、再描画するようにテンプレートに何も伝えていません。 変数は、それ自体によって変化することになった擬似リアルタイムですが、 それはまだかなりMeteorエコシステムの他の部分と正常に通信できないので、 リアクティブではありません。

    リアクティビティをトラッキング:計算

    Meteorのリアクティビティは、計算のセットを追跡するデータ構造体の依存によって仲介されます。

    Meteor’s reactivity is mediated by dependencies, data structures that track a set of computations.

    以前のリアクティビティのサイドバーで見たように、 計算はリアクティブデータを使用するコードの箇所があります。 この例では、そこに暗黙のうちにpostItemテンプレート用に作成さる計算があり、 そのテンプレートのマネージャー上のすべてのヘルパーは、同様にそれ自身の計算を持っています。

    As we saw in the earlier reactivity sidebar, a computation is a section of code that uses reactive data. In our case, there’s a computation that’s been implicitly created for the postItem template, and every helper on that template’s manager has it’s own computation as well.

    リアクティブデータについて「気にする」というコードの一部としての計算と考えることができます。 データの変更は、それが(invalidate()を介して)通知され、 この計算になり、何かが行われる必要があるか否かを決定する計算になります。

    You can think of the computation as the section of code that “cares” about the reactive data. When the data changes, it will be this computation that is informed (via invalidate()), and it’s the computation that decides whether something needs to be done.

    変数をリアクティブ関数に変える

    currentLikeCount変数をリアクティブデータソースに変え、 依存関係を使用し計算のすべてを追跡する必要があります。 それは変数から(値を返却する)関数へ変えていくことが必要です。:

    var _currentLikeCount = 0;
    var _currentLikeCountListeners = new Tracker.Dependency();
    
    currentLikeCount = function() {
      _currentLikeCountListeners.depend();
      return _currentLikeCount;
    }
    
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId),
          function(err, count) {
            if (!err && count !== _currentLikeCount) {
              _currentLikeCount = count;
              _currentLikeCountListeners.changed();
            }
          });
      }
    }, 5 * 1000);
    

    currentLikeCount()中で使用されているすべての計算を追跡するため、 _currentLikeCountListeners依存性を設定したことです。 _currentLikeCountの値が変更された時、 すべての追跡計算を取り返す、依存関係上のchange()関数を呼び出します。

    What we’ve done is setup a _currentLikeCountListeners dependency, which tracks all the computations within which currentLikeCount() has been used. When the value of _currentLikeCount changes, we call the changed() function on that dependency, which invalidates all the tracked computations.

    これらの計算はその後、先に行くとケースバイケースで変化に対応することができます。

    These computations can then go ahead and deal with the change on a case-by-case basis.

    それは、単純なリアクティブデータソースの定型文のように思えた場合、あなたは正しいです。 Meteorは簡単にリアクティブソースにするためのツールがいくつか用意されています。 (同じように直接計算を使用する必要はありません、あなたは通常は自動実行を使用します) currentLikeCount()関数が何をしているかを正確に把握するための、 reactive-varというプラットフォームパッケージがあります。それを追加します:

    meteor add reactive-var
    

    コードを単純化するためにそれを使用することができます:

    var currentLikeCount = new ReactiveVar();
    
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId),
          function(err, count) {
            if (!err) {
              currentLikeCount.set(count);
            }
          });
      }
    }, 5 * 1000);
    

    今、ヘルパーでcurrentLikeCount.get()を呼ぶことで、それは以前のように動作します。 とても便利な(ほぼSessionと同等の)リアクティブのキーと値のストアを、 提供する別のプラットフォームパッケージreactive-dictもあります。

    TrackerとAngularの比較

    AngularはGoogleの善意の人々によって開発された クライアントサイドのリアクティブレンダリングライブラリです。 アプローチはかなり異なっているので、 Angularの依存性追跡とMeteorのそれを比較してみます。

    私たちは、Meteorのモデルは、コードと呼ばれる計算のブロックを使用していることを見てきました。これらの計算は、適切なときにそれらを無効にするの世話をする特別な「リアクティブ」のデータソース(関数)によって追跡されています。 invalidate()を呼び出す必要があるときにデータソースは、明示的にその依存関係のすべてに通知します。 データが変更されたときに、一般的であるが、データソースは、 潜在的に他の理由で無効化をトリガするかを決定できることに注意ください。

    加えて、計算は通常は再実行が無効化されたときに、 あなたが好きなように振る舞うためにそれらを設定することができます。 すべてこれは私たちに、リアクティビティの制御を高レベルを提供します。

    Angularでは、リアクティビティがscopeオブジェクトによって媒介されます。 スコープは特別なメソッドと、プレーンJavaScriptオブジェクトの組み合わせと考えることができます。

    スコープの値にリアクティビティ依存したい場合、 中へ(つまり、あなたは、範囲のどの部分に関心があるか)興味を持っている部分に式を提供して、 scope.$watchを呼びます。つまり、値が変わって欲しい部分を明示的にします。

    When you want to reactively depend on a value in a scope, you call scope.$watch, providing the expression that you are interested in (i.e. which parts of the scope you care about) and a listener function that will run every time that expression changes. So you explicitly state exactly what you want to do every time the value of the expression changes.

    Facebookの例に戻り、以下のように書きます:

    $rootScope.$watch('currentLikeCount', function(likeCount) {
      console.log('Current like count is ' + likeCount);
    });
    

    もちろん、めったにMeteorにて計算を設定しないと同じように、 Angularでは$watchは滅多に呼ばれず、ng-modelディレクティブと{{expressions}}による 自動の監視設定により、変更時の再レンダリングを管理します。

    このようなリアクティブな値が変わった場合、scope.$apply()が呼ばれなければなりません。 これは、スコープのすべての監視を再評価し、値が変わった式の監視のリスナー関数だけを呼び出します。

    したがい、それはリスナーが再評価されるべきかを正確に伝えるよう制御を与えるのではなく、 スコープのレベルで作用する点という除き、scope.$apply()は、dependency.changed()に似ています。 つまり、制御のこのわずかな不足がAngularにそれが正確にリスナーが再評価される必要があるかを、 決定する方法で、非常にスマートかつ効率的にする機能を提供します。

    So scope.$apply() is similar to dependency.changed(), except that it acts at the level of the scope, rather than giving you the control to say precisely which listeners should be re-evaluated. That being said, this slight lack of control gives Angular the ability to be very smart and efficient in the way it determines precisely which listeners need to be re-evaluated.

    Angularでは、getFacebookLikeCount()の関数コードは以下のようにかけると思います。:

    Meteor.setInterval(function() {
      getFacebookLikeCount(Meteor.user(), Posts.find(postId),
        function(err, count) {
          if (!err) {
            $rootScope.currentLikeCount = count;
            $rootScope.$apply();
          }
        });
    }, 5 * 1000);
    

    確かに、Meteorは私たちのために力仕事のほとんどの世話をし、 我々のたくさんの仕事を外してくれるリアクテイビティから利益をもたらしてくれました。 望むならば、これらのパターンを学ぶことは、さらに物事をプッシュする必要がある場合でも、 助けになってくれます。

    ページネーション

    12

    見た目がすごいMicroscopeであれば、それが世界にリリースされたとき、大ヒットを期待することができます。

    ですので、実際にリリースする前に、 新しい投稿の数によるパフォーマンスの意義について少し考える必要があります!

    これまでに、どのようにクライアントサイドのコレクションが サーバーのデータの一部を含めるのか説明しました。 また、notificationとcommentsコレクションでも同じことを行いました。

    しかし今のところ、まだ私たちは接続しているユーザーに対して、1回のアクセスでパブリッシュしています。 リンクが数千掲載されてた場合、最終的には、これは問題になります。 これを解決するために、我々は我々の記事をページ分割する必要があります。

    もっとたくさんの投稿を追加します

    最初に、ページネーションが実際に意味をなすように、fixture.jsにデータに十分な投稿を詰め込みましょう。:

    // Fixture data
    if (Posts.find().count() === 0) {
    
      //...
    
      Posts.insert({
        title: 'The Meteor Book',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://themeteorbook.com',
        submitted: new Date(now - 12 * 3600 * 1000),
        commentsCount: 0
      });
    
      for (var i = 0; i < 10; i++) {
        Posts.insert({
          title: 'Test post #' + i,
          author: sacha.profile.name,
          userId: sacha._id,
          url: 'http://google.com/?q=test-' + i,
          submitted: new Date(now - i * 3600 * 1000),
          commentsCount: 0
        });
      }
    }
    
    server/fixtures.js

    meteor resetを実行した後で、アプリを再起動すると、こんな感じになります:

    Displaying dummy data.
    Displaying dummy data.

    コミット 12-1

    Added enough posts that pagination is necessary.

    無限ページネーション

    私たちは"無限"スタイルのページネーションを実装しています。 この意味というのは、最初にスクリーンに10個の投稿を表示し、 下部に“load more”リンクを配置するということです。 このリンクをクリックすると、さらに10個の投稿をリストに追加し、際限なく続きます。 つまり、画面上に表示する投稿の数を表す単一のパラメータを使用して、 全体のページネーションシステムを制御できることを意味します。

    この単一パラメータがサーバーにどれだけの数の投稿をクライアントに送信するか識別できるように指示する方法が必要となります。 それは、すでにルータのpostsパブリケーションにサブスクライブしているので、 これを利用して、ルータに同様にページネーションを処理してもらいます。

    これを設定する最も簡単な方法は、フォームに単純にパスのポストリミットパラメータ部分を設定したhttp://localhost:3000/25のようなURLを与えることです。 他の方法に比べて、URLを使用することの追加ボーナスは、 現在25の記事を表示し、誤ってブラウザウィンドウをリロードしてしまっている場合、あなたはまだ、 再びページが読み込また時に、同じ25の投稿を見ることができる点です。

    これを正しく行うために、私たちは投稿にサブスクリプションする方法を変える必要があります。 以前にコメントの作成の章で行ったように、 私たちはサブスクリプションのコードをルーターレベルからルートレベルに移行する必要があります。

    これは、すべてを一度に取り込むことがたくさんかもしれないが、コードは綺麗になるはずです。

    最初に、私たちはRouter.configure()ブロック内でpostsパブリケーションへのサブスクリプションを停止します。 といっても、Meteor.subscribe('posts')を削除するだけで、notificationsサブスクリプションだけを残します。

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() {
        return [Meteor.subscribe('notifications')]
      }
    });
    
    lib/router.js

    それからpostsLimit引数をルートのパスに追加します。引数の名前の後に?を追加することは、オプショナルであることを意味しています。そのため、ルートはhttp://localhost:3000/50にマッチするだけでなく、元の古いhttp://localhost:3000にもマッチします。

    //...
    
    Router.route('/:postsLimit?', {
      name: 'postsList',
    });
    
    //...
    
    lib/router.js

    /:parameter?形式のパスがすべての可能なパスにマッチしているか着目することは重要です。 各ルートはそれがカレントパスにマッチしているかどうか見るためにうまくパースされるので、特異性を減少させるために、私たちのルートを整理し確認する必要があります。

    言い換えると、/posts/:_idのような、より特殊なルートをターゲットするルートは、最初に来るべきで、 postsListルートはファイルの下層に移すべきです。というのは、すべてにちゃんとマッチさせるためです。

    これからサブスクライブして正しいデータを見つける上で、大変な問題に取り掛かることなります。 私たちはpostsLimit引数が存在しない場合に対処する必要があるので、 私たちは デフォルトの値をそれに割り当てます。 私たちはページネーションをいじくるための十分な余地をあたえるために、“5”を使います。

    //...
    
    Router.route('/:postsLimit?', {
      name: 'postsList',
      waitOn: function() {
        var limit = parseInt(this.params.postsLimit) || 5;
        return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
      }
    });
    
    //...
    
    lib/router.js

    postsパブリケーションの名前と一緒に、私たちが JavaScript オブジェクト({limit: postsLimit})を引き渡していることにお気づきでしょう。 このオブジェクトは サーバーサイドのPosts.find()文へのオプション引数として使われます。 このように実装して、サーバーサイドのコードに切り替えてみましょう。

    Meteor.publish('posts', function(options) {
      check(options, {
        sort: Object,
        limit: Number
      });
      return Posts.find({}, options);
    });
    
    Meteor.publish('comments', function(postId) {
      check(postId, String);
      return Comments.find({postId: postId});
    });
    
    Meteor.publish('notifications', function() {
      return Notifications.find({userId: this.userId});
    });
    
    server/publications.js

    パラメータを渡す

    パブリケーションコードはfind()文のオプションとして渡す、 サーバーにクライアントから送られたどのようなJavaScriptオブジェクト (この場合は{limit: postsLimit})でも信頼するように指示を出しています。 これでブラウザーコンソールでユーザーが好きなそんなオプションも送信することができるようになります。  

    この場合、これは比較的無害です。 というのは、一人のユーザーがするすべてのことは違う投稿に再指示することだったり、 (最初に許可したいなど)したい制限を変えることだからです。 実際のアプリはおそらく限界を制限する必要があるでしょう!

    ありがたいことに、check()を使用することにより、我々は、ユーザーが (いくつかのケースではfieldsのような書類上のプライベートデータを公開したいというような) こっそり追加のオプションを設定することはできないことを知っています。

    より安全なパターンは、あなたのデータの制御にとどまることを確認するために、 個々のパラメータ自身の代わりに、オブジェクト全体を渡すことかもしれないです。:

    Meteor.publish('posts', function(sort, limit) {
      return Posts.find({}, {sort: sort, limit: limit});
    });
    

    今、私たちは、ルートレベルでサブスクライブしており、それはまた、同じ場所でのデータコンテキストを設定するために理にかなっています。 既存パターンから少し外れますが data関数は、単にカーソルを返す代わりにJavaScriptオブジェクトを返すようにします。 これは、私たちがpostsと呼ぶことにした名付けられたのデータコンテキストを、作成しています。

    Now that we’re subscribing at the route level, it would also make sense to set the data context in the same place. We’ll deviate a bit from our previous pattern and make the data function return a JavaScript object instead of simply returning a cursor. This lets us create a named data context, which we’ll call posts.

    これが意味することは、その代わり、暗黙的にテンプレート内部のthisは、 データコンテキストとしてpostsで利用できるようになるということです。 これとは別に小さな要素から、コードが身近に感じなければならない:

    What this means is simply that instead of being implicitly available as this inside the template, our data context will be available at posts. Apart from this small element, the code should feel familiar:

    //...
    
    Router.route('/:postsLimit?', {
      name: 'postsList',
      waitOn: function() {
        var limit = parseInt(this.params.postsLimit) || 5;
        return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
      },
      data: function() {
        var limit = parseInt(this.params.postsLimit) || 5;
        return {
          posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
        };
      }
    });
    
    //...
    
    lib/router.js

    ルートレベルでデータコンテキストをセットしたので、 posts_list.jsファイル内のpostsテンプレートヘルパーを安全に取り除くことができます。 私たちはデータコンテキストをヘルパーと同じ名前のpostsと名づけたので、 私たちはpostsListテンプレートを触る必要がありません!

    それではおさらいしてみましょう。ここに私たちの新しい改良router.jsコードがどのように見えるかです:

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() {
        return [Meteor.subscribe('notifications')]
      }
    });
    
    Router.route('/posts/:_id', {
      name: 'postPage',
      waitOn: function() {
        return Meteor.subscribe('comments', this.params._id);
      },
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    Router.route('/posts/:_id/edit', {
      name: 'postEdit',
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    Router.route('/submit', {name: 'postSubmit'});
    
    Router.route('/:postsLimit?', {
      name: 'postsList',
      waitOn: function() {
        var limit = parseInt(this.params.postsLimit) || 5;
        return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
      },
      data: function() {
        var limit = parseInt(this.params.postsLimit) || 5;
        return {
          posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
        };
      }
    });
    
    var requireLogin = function() {
      if (! Meteor.user()) {
        if (Meteor.loggingIn()) {
          this.render(this.loadingTemplate);
        } else {
          this.render('accessDenied');
        }
      } else {
        this.next();
      }
    }
    
    Router.onBeforeAction('dataNotFound', {only: 'postPage'});
    Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    コミット 12-2

    Augmented the postsList route to take a limit.

    私たちのブランドの新しいページネーションシステムを試してみましょう。 今、単純にURLパラメータを変更することで、 ホームページ上の記事を任意の数を表示する機能を持っています。 たとえば、http://localhost:3000/3にアクセスしてみてください。 あなたは今、このようなものが表示されるはずです。:

    Controlling the number of posts on the homepage.
    Controlling the number of posts on the homepage.

    なんでページじゃない?

    なぜ私たちはGoogleの検索結果ページのように 10個の投稿ごとに 次のページを表示するのではなく、 “無限ページネーション”アプローチを使っているのでしょうか?。 それは、Meteorによるリアルタイムパラダイムだからです。

    Googleの検索結果ページネーションパターンを使って、Postsコレクションをページングすることを想像しましょう。 そして、10番目から20番目の投稿を表示する2ページ目にいるとします。 もし他のユーザーが1ページ目の10個の投稿のどれかを削除したとしたら、どうなるのでしょうか?   このアプリはリアルタイムなので、データセットが変化します。 10番目の投稿は今、9番目になり、画面から消え去ります。 11番目の投稿が表示対象となります。 最終的に、ユーザーは目に見えない理由で急に投稿の変化を見ることになります!

    たとえ私たちが このUXの特異な行動を我慢したとしても、 従来のページネーションは技術的な理由で実行することが難しいです。

    先ほどの例に戻りましょう。 私たちは10から20の投稿をPostsコレクションからパブリッシュしています。 しかし、クライアントでのこうした投稿を、あなたはどのようにして見つけるのでしょうか? クライアントサイドのデータセットの中に全体で10個の投稿しかないので、 あなたは10から20の投稿を取得することはできません。

    1つの解決策はサーバーで10個の投稿をパブリッシュすることで、 そのときにパブリッシュされたすべての投稿を取り出すためにクライアントサイドでPosts.find()を行います。

    もしあなたが1つだけのサブスクリプションをもつとしたら、 これはうまくいきます。  すぐにできます。しかし、もし1つ以上の投稿サブスクリプションを持つするとしたら、どうなるのでしょうか?

    それではひとつ目のサブスクリプションが10〜20番目のポストを要求し、 別の一つが30〜40番目の投稿を要求したとしましょう。 合計でクライアント側のロードされた20個の記事を持っていますが、どのサブスクリプションから得たものか 知る方法はありません。

    以上のような理由から、Meteorと連携している時に従来のページネーションはあまり意味をなさないのです。

    ルートコントローラーの作成

    var limit = parseInt(this.params.postsLimit) || 5;の行を2度繰り返していることにお気づきかもしれません。 さらに、数字の “5” を ハードコーディングすることは 理想的ではありません。 これはそれほど深刻なことではありませんが、 できればDRY (Don’t Repeat Yourself) 原則に従うのがよいので、 どのようにリファクタリングするのか少し見ていきましょう。

    ここで Iron Router の新しい機能、ルートコントローラを紹介します。 ルートコントローラはルーティング機能をどんなルートでも引き継いで、 再利用可能で素晴らしいパッケージにまとめるためのシンプルな方法です。 ここでは1つのルートにルートコントローラを使いますが、 次の章ではこの機能がいかに役立つのか見ていきます。

    //...
    
    PostsListController = RouteController.extend({
      template: 'postsList',
      increment: 5,
      postsLimit: function() {
        return parseInt(this.params.postsLimit) || this.increment;
      },
      findOptions: function() {
        return {sort: {submitted: -1}, limit: this.postsLimit()};
      },
      waitOn: function() {
        return Meteor.subscribe('posts', this.findOptions());
      },
      data: function() {
        return {posts: Posts.find({}, this.findOptions())};
      }
    });
    
    //...
    
    Router.route('/:postsLimit?', {
      name: 'postsList'
    });
    
    //...
    
    lib/router.js

    一つ一つ見ていきましょう。最初に、RouteControllerを拡張してコントローラを作ります。 それから以前行ったように、templateプロパティをセットして、 その次に新たにincrementプロパティをセットします。

    新たに現在の上限を返すlimit関数と オプションオブジェクトを返すfindOptions関数を定義します。 これは余分なステップのように見えるかもしれませんが、後々これを利用します。

    次に以前と同じようにwaitOn関数とdata関数を定義します。 ただし、新しく利用しているfindOptions関数は除きます。

    コントローラはPostsListControllerと呼ばれ、ルートがpostsListと命名されているので、 Iron Routerは自動的にコントローラを使用します。 ですので(コントローラは、ここでそれらを処理しているため) ルート定義からwaitOndataを削除する必要があります。 私たちは別の名前でコントローラを使用するために必要な場合、 controllerオプションはを使用できます。(次の章でその一例が表示されます)

    コミット 12-3

    Refactored postsLists route into a RouteController.

    “More Link"のロードを追加する

    ページネーションは動作しているので、コードは良さそうに見えます。 1つだけ問題があります: ページネーションを実際に使うには、URL を手動で変えるしかありません。 これではユーザーエクスペリエンスに全く役立ちません。 そのため、この修正に取り掛かりましょう。

    私たちがやりたいことはシンプルです。 投稿リストの下に “load more” ボタンを追加して  クリックされるごとに、 現在表示されている投稿の数値を5増加させます。 つまり、私が現在http://localhost:3000/5にいるとしたら、 “load more”ボタンを押すと、私はhttp://localhost:3000/10に移動します。 ここまで読み進めた方だったら、このちょっとした算数ができるはずです!

    すでに述べたように、ルートにページネーションロジックを追加します。 私たちは、明示的に匿名カーソルを使用するのではなく、 命名したデータコンテキストを使ったことを覚えていますか? まあ、そこにdata関数はカーソルのみを渡すことができないと言うルールはありませんので、 “load more”ボタンのURLを生成するために、同じ方法を使用します。

    //...
    
    PostsListController = RouteController.extend({
      template: 'postsList',
      increment: 5,
      postsLimit: function() {
        return parseInt(this.params.postsLimit) || this.increment;
      },
      findOptions: function() {
        return {sort: {submitted: -1}, limit: this.postsLimit()};
      },
      waitOn: function() {
        return Meteor.subscribe('posts', this.findOptions());
      },
      posts: function() {
        return Posts.find({}, this.findOptions());
      },
      data: function() {
        var hasMore = this.posts().count() === this.postsLimit();
        var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
        return {
          posts: this.posts(),
          nextPath: hasMore ? nextPath : null
        };
      }
    });
    
    //...
    
    lib/router.js

    このルーターマジックについて、さらに深く見ていきましょう。 私たちが現在取り組んでいるPostsListControllerコントローラーから引き継がれた postsListルートがpostsLimit引数をとることを思い出しましょう。

    {postsLimit: this.limit() + this.increment}this.route.path()に入力した時、 私たちはデータコンテキストとしてJavaScript オブジェクトを使って、 postsListルートに自身のパスを作るように命令しています。   言い換えると、自身のカスタムメイドのデータコンテキストによって、 暗黙的なthisを取り替えることを除いて、 これはまさに{{pathFor 'postsList'}}Spacebarsヘルパーを使うことと同じことです。   

    表示するより多くの記事がある場合にのみ、 そのパスを取って、私たちのテンプレートのデータコンテキストにそれを追加している。 それを行う方法は少しトリッキーです。

    this.limit()は、私たちが表示したい現在の投稿数を返します。 または、現在のURLを基にした値か、URLにパラメータが含まれていなければ、デフォルト値(5)を返します。 

    他方で、this.postsは現在のカーソルを参照するので、 this.posts.count()は実際にカーソルの中の投稿数を参照します。

    ここで私たちが言っていることは、もし私たちがnposts を要求してnを戻すと、 “load more”ボタンを表示し続けるということです。 リミットに達するとnより小さいの値が返るので、私たちはそのボタンの表示をストップすべきです。

    とはいえ、このシステムはあるケースにて失敗します: もし、データベースのアイテム数がは正確にn出会った場合、何が起こるかというと、 クライアントがn投稿あることを確認し取得すると“load more”ボタンが表示し続けるのです。

    残念なことに、この問題に単純な次善策はないので、 今のところ私たちはこの完全でない実装を解決する必要があります。 

    もしさらに投稿をロードさせるとしたら、 するために残されていることは投稿リストのボタンの下に“load more”リンクを追加して、このリンクだけをを表示します。

    <template name="postsList">
      <div class="posts">
        {{#each posts}}
          {{> postItem}}
        {{/each}}
    
        {{#if nextPath}}
          <a class="load-more" href="{{nextPath}}">Load more</a>
        {{/if}}
      </div>
    </template>
    
    client/templates/posts/posts_list.html

    これで投稿リストは現在このようになっているはずです:

    The “load more” button.
    The “load more” button.

    コミット 12-4

    Added nextPath() to the controller and use it to step thr…

    より良いユーザー体験

    ページネーションは現在正常に動作していますが、変な癖があります: 毎回私たちは“load more”をクリックして、ルータは、新しいデータが入ってくるようにするために、 我々は待っている間Iron RouterのwaitOn機能はloadingテンプレートに私たちを送信し、 より多くのポストを要求します。 その結果、私たちは、送信さるたびにページの上部に戻され、 私たちのブラウジングを再開する度に下にスクロールする必要があるということです。

    だから最初、私たちはすべての後にサブスクリプションをwaitOnしないように Iron Router に指示する必要があります。 その代わりに、我々は、subscriptionsフックで私たちのサブスクリプションを定義します。

    また、データコンテキストの一部としてthis.postsSub.readyを参照できるready変数を渡している。 これで、ポストスクリプションがロードを完了したことを、 テンプレートに教えてあげます。

    //...
    
    PostsListController = RouteController.extend({
      template: 'postsList',
      increment: 5,
      postsLimit: function() {
        return parseInt(this.params.postsLimit) || this.increment;
      },
      findOptions: function() {
        return {sort: {submitted: -1}, limit: this.postsLimit()};
      },
      subscriptions: function() {
        this.postsSub = Meteor.subscribe('posts', this.findOptions());
      },
      posts: function() {
        return Posts.find({}, this.findOptions());
      },
      data: function() {
        var hasMore = this.posts().count() === this.postsLimit();
        var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
        return {
          posts: this.posts(),
          ready: this.postsSub.ready,
          nextPath: hasMore ? nextPath : null
        };
      }
    });
    
    //...
    
    lib/router.js

    ポストの新しいセットをロードしている間、ポストリストの下部にあるスピナーを表示するために、 テンプレートにこのready変数をチェックします:

    <template name="postsList">
      <div class="posts">
        {{#each posts}}
          {{> postItem}}
        {{/each}}
    
        {{#if nextPath}}
          <a class="load-more" href="{{nextPath}}">Load more</a>
        {{else}}
          {{#unless ready}}
            {{> spinner}}
          {{/unless}}
        {{/if}}
      </div>
    </template>
    
    client/templates/posts/posts_list.html

    コミット 12-5

    Add a spinner to make pagination nicer.

    Accessing Any Post

    私たちは現在、デフォルトで最新の5つの投稿をロードしていますが、 投稿の個別ページを見たい人がいたらどうなるでしょうか?

    An empty template.
    An empty template.

    これを試すと、“not found”エラーに出くわします。 これはこういうことです: postsListルートをロードする時に、 私たちはルーターがpostsパブリケーションにサブスクライブするように指示を出しています。 しかし、私たちはpostPageルートに何をするのか指示を出しませんでした。

    しかし、今までで私たちが 行う方法のすべてはnの最新の投稿リストにサブスクライブすることです。 では、サーバーに1つの特定の投稿を要求するにはどうしたら良いのでしょうか? ここでちょっとした秘密を教えましょう: あなたは 各コレクションに対して1つ以上のパブリケーションを持つことができます! 

    行方不明の投稿を取り戻すために、新たに独立したsinglePostパブリケーションを作ります。 これは_idで識別して1つの投稿だけをパブリッシュします。

    Meteor.publish('posts', function(options) {
      return Posts.find({}, options);
    });
    
    Meteor.publish('singlePost', function(id) {
      check(id, String)
      return Posts.find(id);
    });
    
    //...
    
    server/publications.js

    さあ、クライアントサイドに正しい投稿をサブスクライブしましょう。 私たちはすでにpostPageルートのwaitOn関数にcommentsパブリケーションをサブスクライブしています。 そのため、ここででsinglePostにサブスクリプションを簡単に追加できます。 postEditルートへのサブスクリプション追加することを忘れずに。 というのは、postEditルートも同じデータが必要だからです。

    //...
    
    Router.route('/posts/:_id', {
      name: 'postPage',
      waitOn: function() {
        return [
          Meteor.subscribe('singlePost', this.params._id),
          Meteor.subscribe('comments', this.params._id)
        ];
      },
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    Router.route('/posts/:_id/edit', {
      name: 'postEdit',
      waitOn: function() {
        return Meteor.subscribe('singlePost', this.params._id);
      },
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    //...
    
    lib/router.js

    コミット 12-6

    Use a single post subscription to ensure that we can alwa…

    ページネーションが完了すると、このアプリはスケーリングの問題に苦しむことはありません。 そのため、ユーザーは以前よりも多くのリンクを投稿することでしょう。 では、こうしたリンクにどうにかしてランク付けする良い方法はないものでしょうか? これこそが、次の章の話題です!

    投票機能

    13

    私たちのサイトは だんだんと一般向けになってきました。 今、本サイトは、最高のリンクを見つけ、より人気になっているリンクを取得するのは厄介です。 記事を並び変えるためのなんらかのランキングシステムが必要です。

    私たちは、カルマ、ポイントの時間ベースの崩壊、 そして他の多くのものを持つ複雑なランキングシステムを構築することができます。 (そのほとんどは、Telescope、Microscopeの兄に実装されています) しかしこのアプリでは、シンプルにして、投票数で投稿を格付けることにします。

    ユーザーに 投稿に投票する方法を提供することから始めていきましょう。

    データモデル

    私たちは、私たちがユーザーにupvoteボタンを表示するだけでなく、 2回投票しているかどうかを知ることができる各投稿毎のupvotersのリストを格納します。

    データのプライバシー & パブリケーション

    私たちは、すべのユーザーに投票者のリストを公開します。 これでブラウザーコンソールで自動的にそのデータを公的にアクセスできるようにします。

    これはある種のデータプライバシー問題ですが、 これはコレクションを動かす方法に起因しています。 たとえば、私たちはユーザーに誰が投稿に投票したのかわかるようにしたいでしょうか? この場合、その情報を公的に利用することは、実際のところ全く影響も及ぼしませんが、 この問題を少なくとも認識することは重要です。

    また、それが簡単に数字を取得するために、 投稿にupvotersの合計数を非正規化します。 投稿にupvotersvotesを2つの属性を追加しましょう。 私たちのfixturesファイルにそれらを追加することから始めましょう。:

    // Fixture data
    if (Posts.find().count() === 0) {
      var now = new Date().getTime();
    
      // create two users
      var tomId = Meteor.users.insert({
        profile: { name: 'Tom Coleman' }
      });
      var tom = Meteor.users.findOne(tomId);
      var sachaId = Meteor.users.insert({
        profile: { name: 'Sacha Greif' }
      });
      var sacha = Meteor.users.findOne(sachaId);
    
      var telescopeId = Posts.insert({
        title: 'Introducing Telescope',
        userId: sacha._id,
        author: sacha.profile.name,
        url: 'http://sachagreif.com/introducing-telescope/',
        submitted: new Date(now - 7 * 3600 * 1000),
        commentsCount: 2,
        upvoters: [],
        votes: 0
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: tom._id,
        author: tom.profile.name,
        submitted: new Date(now - 5 * 3600 * 1000),
        body: 'Interesting project Sacha, can I get involved?'
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: sacha._id,
        author: sacha.profile.name,
        submitted: new Date(now - 3 * 3600 * 1000),
        body: 'You sure can Tom!'
      });
    
      Posts.insert({
        title: 'Meteor',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://meteor.com',
        submitted: new Date(now - 10 * 3600 * 1000),
        commentsCount: 0,
        upvoters: [],
        votes: 0
      });
    
      Posts.insert({
        title: 'The Meteor Book',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://themeteorbook.com',
        submitted: new Date(now - 12 * 3600 * 1000),
        commentsCount: 0,
        upvoters: [],
        votes: 0
      });
    
      for (var i = 0; i < 10; i++) {
        Posts.insert({
          title: 'Test post #' + i,
          author: sacha.profile.name,
          userId: sacha._id,
          url: 'http://google.com/?q=test-' + i,
          submitted: new Date(now - i * 3600 * 1000 + 1),
          commentsCount: 0,
          upvoters: [],
          votes: 0
        });
      }
    }
    
    server/fixtures.js

    これまで通り、アプリを停止して、meteor resetを実行して、新しいユーザーアカウントを作ります。 それから、投稿が作成された時に2つのプロパティが初期化されるようにします。

    //...
    
    var postWithSameLink = Posts.findOne({url: postAttributes.url});
    if (postWithSameLink) {
      return {
        postExists: true,
        _id: postWithSameLink._id
      }
    }
    
    var user = Meteor.user();
    var post = _.extend(postAttributes, {
      userId: user._id,
      author: user.username,
      submitted: new Date(),
      commentsCount: 0,
      upvoters: [],
      votes: 0
    });
    
    var postId = Posts.insert(post);
    
    return {
      _id: postId
    };
    
    //...
    
    collections/posts.js

    投稿テンプレート

    最初に、投稿セルにupvoteボタンを追加し、投稿のメタデータとして投稿数を表示します:

    <template name="postItem">
      <div class="post">
        <a href="#" class="upvote btn btn-default"></a>
        <div class="post-content">
          <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
          <p>
            {{votes}} Votes,
            submitted by {{author}},
            <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
            {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
          </p>
        </div>
        <a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
      </div>
    </template>
    
    client/templates/posts/post_item.html
    The upvote button
    The upvote button

    次に、 ユーザーがボタンをクリックしたら、サーバーの upvoteメソッドを呼び出します。:

    //...
    
    Meteor.methods({
      post: function(postAttributes) {
        //...
      },
    
      upvote: function(postId) {
        check(this.userId, String);
        check(postId, String);
    
        var post = Posts.findOne(postId);
        if (!post)
          throw new Meteor.Error('invalid', 'Post not found');
    
        if (_.include(post.upvoters, this.userId))
          throw new Meteor.Error('invalid', 'Already upvoted this post');
    
        Posts.update(post._id, {
          $addToSet: {upvoters: this.userId},
          $inc: {votes: 1}
        });
      }
    });
    
    //...
    
    lib/collections/posts.js

    コミット 13-1

    Added basic upvoting algorithm.

    このメソッドは非常に簡単です。 私たちは、ユーザが、ポストが実際に存在していることを記録されていることを確認するために、 いくつかの防御的なチェックを行います。 その後、ユーザーがすでにポストに投票していないことを再度確認し、いない場合、 投票の合計スコアをインクリメントし、upvotersのセットにユーザーを追加します。

    この最後のステップは、興味深い特別なMongoの演算子を使用しました。 他にも学ぶべき演算子はありますが、これら二つは非常に有用です。 $addToSetは、それがすでに存在していない場合に限り配列プロパティに項目を追加し、 $incは、単純に整数フィールドをインクリメントします。

    ユーザーインターフェースの微調整

    ユーザーがログインしていないか、既にポストをupvotedしている場合、彼らは投票することができません。 UIでこれを反映するために、条件付きでupvoteボタンにdisabledCSSクラスを追加するために ヘルパーを使用します。

    <template name="postItem">
      <div class="post">
        <a href="#" class="upvote btn btn-default {{upvotedClass}}"></a>
        <div class="post-content">
          //...
      </div>
    </template>
    
    client/templates/posts/post_item.html
    Template.postItem.helpers({
      ownPost: function() {
        //...
      },
      domain: function() {
        //...
      },
      upvotedClass: function() {
        var userId = Meteor.userId();
        if (userId && !_.include(this.upvoters, userId)) {
          return 'btn-primary upvotable';
        } else {
          return 'disabled';
        }
      }
    });
    
    Template.postItem.events({
      'click .upvotable': function(e) {
        e.preventDefault();
        Meteor.call('upvote', this._id);
      }
    });
    
    client/templates/posts/post_item.js

    .upvoteから.upvotableへクラスを変更しているので、クリックイベントハンドラを変更することを忘れないでください。

    Greying out upvote buttons.
    Greying out upvote buttons.

    コミット 13-2

    Grey out upvote link when not logged in / already voted.

    次に、あなたが “1 votes”と、単一の投票がラベル付けされていることに気づくでしょう。 適切にこれらのラベルのプロパティを調整するには時間がかかるかもしれません。 複数化は、複雑なプロセスになる可能性がありますが、今のところは、かなり単純な方法でこれを行います。 我々はどこにでも使用することができ、一般的なSpacebarsヘルパーを作ります。

    UI.registerHelper('pluralize', function(n, thing) {
      // fairly stupid pluralizer
      if (n === 1) {
        return '1 ' + thing;
      } else {
        return n + ' ' + thing + 's';
      }
    });
    
    client/helpers/spacebars.js

    以前に作成したヘルパーは、適用対象のテンプレートに縛られてきたました。 しかしUI.registerHelperを使用することによって、 任意のテンプレート内で使用することができるグローバルヘルパーを作成しました:

    <template name="postItem">
    
    //...
    
    <p>
      {{pluralize votes "Vote"}},
      submitted by {{author}},
      <a href="{{pathFor 'postPage'}}">{{pluralize commentsCount "comment"}}</a>
      {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
    </p>
    
    //...
    
    </template>
    
    client/templates/posts/post_item.html
    Perfecting Proper Pluralization (now say that 10 times)
    Perfecting Proper Pluralization (now say that 10 times)

    コミット 13-3

    Added pluralize helper to format text better.

    "1 vote"と見えることを確認してください。

    よりスマートな投票アルゴリズム

    私たちのupvotingコードは格好良いですが、まだ良くすることができます。 upvote Methodでは、Mongoへの2つの呼び出しを行います。: 一つ目は投稿を取得するため、二つ目は更新するためです。

    二つの問題があります。

    第一に、それは二度データベースに行く分多少非効率的です。 しかし、もっと重要なのは、競合状態が導入されています。次のアルゴリズムを追ってください:

    1. データベースから投稿を取得する。
    2. ユーザーが投票したかどうかを確認する。
    3. 投票していない場合は、ユーザーによる投票を行う。

    同じユーザがステップ1と3の間で再びポストに投票した場合はどうなりますか? 現在のコードでは、二度同じポストに投票することができるという可能性があります。

    ありがたいことに、Mongoは賢くなり、手順1〜3を組み合わせて、 単一のMongoのコマンドにすることが可能です。:

    //...
    
    Meteor.methods({
      post: function(postAttributes) {
        //...
      },
    
      upvote: function(postId) {
        check(this.userId, String);
        check(postId, String);
    
        var affected = Posts.update({
          _id: postId,
          upvoters: {$ne: this.userId}
        }, {
          $addToSet: {upvoters: this.userId},
          $inc: {votes: 1}
        });
    
        if (! affected)
          throw new Meteor.Error('invalid', "You weren't able to upvote that post");
      }
    });
    
    //...
    
    collections/posts.js

    コミット 13-4

    Better upvoting algorithm.

    これは「すべての投稿をidで見つけ、このユーザはまだに投票していなければこの方法でそれらを更新する」 という処理です。 ユーザーはまだ投票していない場合、当然ですがidを持つ投稿を見つけるでしょう。 一方ユーザーが投票した場合、問合せは書類と一致しなくなり、その結果、何も起こりません。

    Latency Compensation

    例えば、あなたがチートや投票のその数を微調整することにより、リストの一番上にあなたの記事のいずれかを送信しようとしたとしましょう:

    > Posts.update(postId, {$set: {votes: 10000}});
    
    Browser console

    (ここで、 postIdはあなたの記事の1つのidです)

    システムに対する遊びとしての図々しい試みは私たちのdeny()コールバックによってキャッチされます。 ( collections/posts.jsです。覚えてます?)、そして、すぐに打ち消されます。

    しかし、慎重に見ればあなたはこの行為でlatency compensationを目撃することができるかもしれません。 それは、それは一瞬で進みますが、投稿は元の位置に戻る前に、一時的にリストの一番上にジャンプします。

    何が起こったのか?ローカルのPostsコレクションでは、updateは何事もなく適用されます。 これは瞬時に起こるのですが、投稿がリストの一番上にジャンプします。 一方、サーバー上で、updateが拒否されていました。 なので、ちょっと後(自分のマシン上でMeteorを実行している場合は、ミリ秒単位です。)に、 サーバは、エラーが返され、ローカルコレクションに元に戻すように指示しました。

    最終結果:サーバが応答するのを待っている間、ユーザーインターフェースの助けにはなりますが、 ローカルコレクションを信頼することはできません。 とすぐに、サーバが戻ってくると変更を拒否したように、 ユーザインタフェースは、それを反映するように適応させれます。

    フロントページの投稿ランキング

    今のところ、投票数に基づいた、投稿ごとにスコアがあるので、 最も良い投稿のリストを表示しましょう。 そうするために、私たちはpostコレクションの、2つのサブスクリプションを管理する方法を見て、 postListテンプレートをさらに少しだけ一般的にします。

    始めるにあたり、異なるソート順の二つのサブスクリプションを持ちたいと思います。 ここでのトリックは、両方のサブスクリプションが、 同じpostsパブリケーションに異なる引数でサブスクライブということです!

    また、ページネーションのための二つの新しいルートnewPostsbestPostsを作成しそれぞれ /new/bestというURLでアクセス可能にします。 (もちろんページング用は/new/5/best/5となります)

    これを行うために、我々はPostsListController拡張し、2つの別個のNewPostsListControllerBestPostsListControllerを作成します。 これは、homenewPostsルートの両方に、 単一の継承されたNewPostsListControllerを与えることによって、 まったく同じルートオプションを再利用できるようになります。 そして、Iron Router がいかに柔軟なかを説明できたと思います。

    それでは、{submitted: -1}PostsListControllerのソートプロパティthis.sortを上書き しましょう。NewPostsListController and BestPostsListControllerを置き換えましょう:

    So let’s replace the {submitted: -1} sort property in PostsListController by this.sort, which will be provided by NewPostsListController and BestPostsListController:

    //...
    
    PostsListController = RouteController.extend({
      template: 'postsList',
      increment: 5,
      postsLimit: function() {
        return parseInt(this.params.postsLimit) || this.increment;
      },
      findOptions: function() {
        return {sort: this.sort, limit: this.postsLimit()};
      },
      subscriptions: function() {
        this.postsSub = Meteor.subscribe('posts', this.findOptions());
      },
      posts: function() {
        return Posts.find({}, this.findOptions());
      },
      data: function() {
        var hasMore = this.posts().count() === this.postsLimit();
        return {
          posts: this.posts(),
          ready: this.postsSub.ready,
          nextPath: hasMore ? this.nextPath() : null
        };
      }
    });
    
    NewPostsController = PostsListController.extend({
      sort: {submitted: -1, _id: -1},
      nextPath: function() {
        return Router.routes.newPosts.path({postsLimit: this.postsLimit() + this.increment})
      }
    });
    
    BestPostsController = PostsListController.extend({
      sort: {votes: -1, submitted: -1, _id: -1},
      nextPath: function() {
        return Router.routes.bestPosts.path({postsLimit: this.postsLimit() + this.increment})
      }
    });
    
    Router.route('/', {
      name: 'home',
      controller: NewPostsController
    });
    
    Router.route('/new/:postsLimit?', {name: 'newPosts'});
    
    Router.route('/best/:postsLimit?', {name: 'bestPosts'});
    
    lib/router.js

    注意点として、一つ以上のルートを持ち、パスはどちらの場合では異なるであろうことから、nextPathロジックをPostsListControllerから出して、 NewPostsControllerBestPostsControllerに入れています。

    加えて、votesによってソートする時、タイムスタンプや _idによって後続のソートを指定し、 順序が完全に指定されていることを確認しました。

    Additionally, when we sort by votes, we have a subsequent sorts by submitted timestamp and then _id to ensure that the ordering is completely specified.

    新しいコントローラでは、安全に、以前のpostsListルートを取り除くことができます。 ただ、次のコードを削除します:

     Router.route('/:postsLimit?', {
      name: 'postsList'
     })
    
    lib/router.js

    ヘッダー内にリンクも追加しましょう:

    <template name="header">
      <nav class="navbar navbar-default" role="navigation">
        <div class="container-fluid">
          <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="{{pathFor 'home'}}">Microscope</a>
          </div>
          <div class="collapse navbar-collapse" id="navigation">
            <ul class="nav navbar-nav">
              <li>
                <a href="{{pathFor 'newPosts'}}">New</a>
              </li>
              <li>
                <a href="{{pathFor 'bestPosts'}}">Best</a>
              </li>
              {{#if currentUser}}
                <li>
                  <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
                </li>
                <li class="dropdown">
                  {{> notifications}}
                </li>
              {{/if}}
            </ul>
            <ul class="nav navbar-nav navbar-right">
              {{> loginButtons}}
            </ul>
          </div>
        </div>
      </nav>
    </template>
    
    client/templates/includes/header.html

    最後に、投稿削除のイベントハンドラを更新する必要があります:

      'click .delete': function(e) {
        e.preventDefault();
    
        if (confirm("Delete this post?")) {
          var currentPostId = this._id;
          Posts.remove(currentPostId);
          Router.go('home');
        }
      }
    
    client/templates/posts_edit.js

    これがすべて終わると、私たちは ベスト投稿リストを得ます:

    Ranking by points
    Ranking by points

    コミット 13-5

    Added routes for post lists, and pages to display them.

    A Better Header

    これで2つのリストページがあるので、 どちらのリストを現在見ているのか知ることは難しいかもしれません。 そのため、もっとわかりやすくするために、ヘッダーを再検討してみましょう。 ナビゲーション項目上で.activeクラスを設定するために 現在のパスと1つ以上の名前付きルートを使用してheader.jsマネージャとヘルパーを作成します:

    複数の名前付きルートをサポートしたい理由は、 私たちのhomenewPostsルートの両方(URLはそれぞれ// new) に同じテンプレートを使いたいからです。 両方のケースで<LI>タグをアクティブにするので、 activeRouteClassは十分にスマートでなければならないことを意味します。

    <template name="header">
      <nav class="navbar navbar-default" role="navigation">
        <div class="container-fluid">
          <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="{{pathFor 'home'}}">Microscope</a>
          </div>
          <div class="collapse navbar-collapse" id="navigation">
            <ul class="nav navbar-nav">
              <li class="{{activeRouteClass 'home' 'newPosts'}}">
                <a href="{{pathFor 'newPosts'}}">New</a>
              </li>
              <li class="{{activeRouteClass  'bestPosts'}}">
                <a href="{{pathFor 'bestPosts'}}">Best</a>
              </li>
              {{#if currentUser}}
                <li class="{{activeRouteClass 'postSubmit'}}">
                  <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
                </li>
                <li class="dropdown">
                  {{> notifications}}
                </li>
              {{/if}}
            </ul>
            <ul class="nav navbar-nav navbar-right">
              {{> loginButtons}}
            </ul>
          </div>
        </div>
      </nav>
    </template>
    
    client/templates/includes/header.html
    Template.header.helpers({
      activeRouteClass: function(/* route names */) {
        var args = Array.prototype.slice.call(arguments, 0);
        args.pop();
    
        var active = _.any(args, function(name) {
          return Router.current() && Router.current().route.getName() === name
        });
    
        return active && 'active';
      }
    });
    
    client/templates/includes/header.js
    Showing the active page
    Showing the active page

    ヘルパーの引数

    私たちは、今までこのパターンを使用していませんでした。 他のSpacebarsのタグのように、テンプレートヘルパータグは引数を取ることができます。

    あなたはもちろん、あなたの関数に特定の名前付き引数を渡すことができつつ、 不特定多数の匿名のパラメータを渡し、関数内でargumentsオブジェクトを呼び出すことで、 それらを取得することができます。

    この最後のケースでは、 あなたはおそらく正規のJavaScript配列にargumentsオブジェクトを変換したいと思うでしょうし、 それでSpacebarsによって最後に追加されたハッシュを取り除くためにpop()を呼び出しました。

    各ナビゲーションアイテムで、activeRouteClassヘルパーはルート名のリストを取り、 その後の経路のいずれかがテスト(対応するURLが現在のパスに等しいかどうか) に合格するかどうかを確認するために、 Underscoreのany()ヘルパーを使用しています。

    もし現在のパスにルートがマッチするなら、any()trueを返します。 最後に、boolean && stringというJavaScriptパターンの利点についてですが、これは false && myStringであればfalseを返しますが、true && myStringであれば myStringを返します。

    コミット 13-6

    Added active classes to the header.

    これでユーザーはリアルタイムで投稿に投票することができるので、 ランキングが変化する度に項目ががページ上で上下することがわかります。 この変化時にアニメーションが滑らかできれば、素晴らしいことではないでしょうか?

    高度なパブリケーション

    Sidebar 13.5

    ここまでであなたはパブリケーションとサブスクリプションがどのように相互作用するかをよく理解しているべきです。 それでは、補助輪を取り除き、より少数の高度なシナリオを検討します。

    コレクションを複数回パブリッシュする

    パブリケーションに関する私たちの最初のサイドバー にて、我々はより一般的なパブリケーションとサブスクリプションのパターンのいくつかを見て、 非常に簡単な自身のサイトに実装するための_publishCursor関数作成方法を学びました。

    まずは、_publishCursorが正確に何をするか思い出してみましょう: それは、与えられたカーソルに一致するすべての文書を受け取り、 同じ名前のクライアントコレクションにそれらが来るようにしました。 パブリケーションの名前が関わるものではないことに注意してください。

    First, let’s recall what _publishCursor does for us exactly: it takes all the documents that match a given cursor, and pushes them down into the client-side collection of the same name. Notice that the name of the publication is in no way involved.

    これは、我々はすべてのコレクションのクライアントとサーバーのバージョンをリンクする 複数のパブリケーションを持つことができることを意味します。

    我々はすでに我々が現在表示されている投稿に加えて、 全ての記事のページ番号付きサブセットを公開したときに、 ページネーションの章でこのパターンに遭遇しました。

    別の同様のユースケースは、文書の大規模なセット、 ならびに単一の項目の完全な詳細の概要を公開することです:

    一つのコレクションを2度パブリッシュする
    一つのコレクションを2度パブリッシュする
    Meteor.publish('allPosts', function() {
      return Posts.find({}, {fields: {title: true, author: true}});
    });
    
    Meteor.publish('postDetail', function(postId) {
      return Posts.find(postId);
    });
    

    クライアントはこれら二つのパブリケーションをサブスクライブした場合に、 その'posts'コレクションは2つのソースから取り込まれます: 最初のサブスクリプションからはタイトルと著者の名前、第二サブスクリプションからは 投稿の完全な詳細のリストです。

    postDetailによって公表後も(ただし、そのプロパティの一部のみで) allPostsによって公開されていることを実現するかもしれない。 しかし、Meteorはフィールドを、オーバーラップの管理によってマージし重複なしの投稿を確保します。

    You may realize that the post published by postDetail is also being published by allPosts (although with only a subset of its properties). However, Meteor takes care of the overlap by merging the fields and ensuring there is no duplicate post.

    これは、素晴らしいです なぜなら、我々は投稿の要約のリストをレンダリングする際に、 今必要なものを表示するために、 私たちのためだけの十分なデータを持っているデータオブジェクトを扱っているからです。 しかし、単一の投稿のためにページをレンダリングするとき、 我々はそれを表示するために必要なすべてを持っています。 もちろん、我々はすべてのフィールドが、この場合のすべての投稿で利用できることを期待しないように、 クライアント上で注意する必要があります - これは一般的な落とし穴です!

    それはあなたがドキュメントプロパティを変化させることに制限されないということに留意すべきです。 異なった順序のアイテムで、同じプロパティでのパブリケーションもきちんとパブリッシュできました。

    Meteor.publish('newPosts', function(limit) {
      return Posts.find({}, {sort: {submitted: -1}, limit: limit});
    });
    
    Meteor.publish('bestPosts', function(limit) {
      return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
    });
    
    server/publications.js

    一つのパブリケーションを複数回サブスクライブする

    私達はちょうど単一のコレクションを複数回パブリッシュする方法を見てきました。 今度は、非常に類似した結果を達成することができる別のパターン: 1回のパブリケーションで、複数回のサブスクライブを行うパターンです。

    Microscopeでは、postsのパブリケーションに複数回サブスクライブしますが、Iron Routerは 各サブスクリプション毎に設定して、切断を行います。同時に複数回サブスクライブできない理由はありません。

    例えば、同時に、メモリ内の最新なものと最良なものの投稿の両方をロードしたいとしましょう: For example, let’s say we wanted to load both the newest and best posts in memory at the same time:

    一つのパブリケーションに2度サブスクライブする
    一つのパブリケーションに2度サブスクライブする

    一つのパブリケーションを設定しています:

    Meteor.publish('posts', function(options) {
      return Posts.find({}, options);
    });
    

    そして、我々は、このパブリケーションに複数回サブスクライブします。 実際にはこれは、多かれ少なかれ、確かにMicroscopeでやっていることです。:

    Meteor.subscribe('posts', {submitted: -1, limit: 10});
    Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});
    

    正確には何がここで起きているのでしょうか? 各ブラウザは、二つの異なるサブスクリプション開き、 サーバー上の同じパブリケーションに各々接続しているのです。

    各サブスクリプションは、そのパブリケーションに異なる引数を提供しますが、基本的に、 書類の(異なる)セットはpostsコレクションから摘み取られ、そして クライアントコレクションに送信されてます。

    同じパブリケーションに同じ引数で二回サブスクライブすることすらできます! 有用であろう多くのシナリオを考えるのは難しいですが、柔軟性はある日便利だと思えるかもしれません!

    一つのサスククリプションで複数のCollection

    JOINを利用するMySQLのような、より伝統的なリレーショナルデータベースとは異なり、 MongoのようなNoSQLのデータベースは、非正規化と*埋め込み程度で全てです。 それがMeteorのコンテキストでどのように機能するかを見てみましょう。

    それでは、具体的な例を見てみましょう。 私達は投稿にコメントを追加しました、そしてこれまでのところ、 幸せなことに我々はユーザーが見ている単一の投稿のコメントをパブリッシュするだけでした。

    しかしながら、 私たちはフロントページ上のすべての投稿にコメントを見せたかったとします。 (それによって投稿のページ構成が変わることを念頭に置いています。) このユースケースは、投稿にコメントを埋め込むための十分な理由を提示し、 実際、コメントカウントを非正規化するための動機になったものです。 and in fact is what pushed us to denormalize comment counts.

    もちろん、完全にCommentsコレクションを取り除き、投稿内にコメントを埋め込むことができます。 しかし、以前に非正規化の章で見たように、そうすることによって、 独立したコレクションだった時とくらべの余分な利点のいくつかを失うことになります。

    しかし、別のコレクションを保持しながら、 コメントを埋め込むことを可能にするサブスクリプションを含むトリックがあると判明しました。

    But it turns out there’s a trick involving subscriptions that makes it possible to embed our comments while preserving separate collections.

    それではフロントページの投稿リストとともに、 各々の投稿のためのトップ2のコメントのリストをサブスクライブしたいとしましょう。

    Let’s suppose that along with our front-page list of posts, we want to subscribe to a list of the top 2 comments for each post.

    独立したコメントのパブリケーションで、特に、投稿のリストが何らかの方法(例えば最新10個) で制限されていた場合は、達成することは困難でしょう。 例えば、この図のようなパブリケーションを書く必要があるでしょう:

    It would be difficult to accomplish this with an independent comments publication, especially if the list of posts was limited in some way (say, the 10 most recent). We’d have to write a publication that looked something like this:

    二つのコレクションで一つのサブスクリプション
    二つのコレクションで一つのサブスクリプション
    Meteor.publish('topComments', function(topPostIds) {
      return Comments.find({postId: topPostIds});
    });
    

    topPostIdsのリストが変更されるたびにパブリケーションは破棄され、再確立が必要になるので、 これは、パフォーマンスの観点から問題となります。

    This would be a problem from a performance standpoint, as the publication would need to get torn down and re-established each time the list of topPostIds changed.

    近い方法があります。コレクションごとに複数のパブリケーションを持つことができないですが、 パブリケーション毎に複数コレクション持つことができる事実を使用します:

    There is a way around this though. We just use the fact that we can not only have more than one publication per collection, but we can also have more than one collection per publication:

    Meteor.publish('topPosts', function(limit) {
      var sub = this, commentHandles = [], postHandle = null;
    
      // 投稿に添付されるトップ2つのコメントを送信する
      function publishPostComments(postId) {
        var commentsCursor = Comments.find({postId: postId}, {limit: 2});
        commentHandles[postId] =
          Mongo.Collection._publishCursor(commentsCursor, sub, 'comments');
      }
    
      postHandle = Posts.find({}, {limit: limit}).observeChanges({
        added: function(id, post) {
          publishPostComments(id);
          sub.added('posts', id, post);
        },
        changed: function(id, fields) {
          sub.changed('posts', id, fields);
        },
        removed: function(id) {
          // stop observing changes on the post's comments
          commentHandles[id] && commentHandles[id].stop();
          // delete the post
          sub.removed('posts', id);
        }
      });
    
      sub.ready();
    
      // make sure we clean everything up (note `_publishCursor`
      //   does this for us with the comment observers)
      sub.onStop(function() { postHandle.stop(); });
    });
    

    注意点としてパブリケーション内で何も返していません。 マニュアルでsub自身(.addedもしくはその仲間経由で)にメッセージを送信しています。 だから、カーソルを返すことによって、_publishCursorに依頼する必要はありません。

    Note that we aren’t returning anything in this publication, as we manually send messages to the sub ourselves (via .added() and friends). So we don’t need to ask _publishCursor to do it for us by returning a cursor.

    今、私たち投稿をパブリッシュたびに、我々はまた、自動的にそれに接続され、トップ2つのコメントを公開します。 そしてあとは、単一のサブスクリプションを呼ぶだけです!

    Now, every time we publish a post we also automatically publish the top two comments attached to it. And all with a single subscription call!

    しかし、Meteorのこのアプローチは非常に簡単とは言えませんが、 あなたはまた、このパターンをより容易に使用することを目指すpublish-with-relationsパッケージ をAtmosphereに見ることができます。

    Although Meteor doesn’t make this approach very straightforward yet, you can also look into the publish-with-relations package on Atmosphere, which aims to make this pattern easier to use.

    異なるコレクションとのリンク

    他にサブスクリプションの柔軟性の新発見の知識は私たちに何を与えることができるでしょう? _publishCursorを使用しない場合、サーバー上のソースコレクションは、 クライアント上のターゲットコレクションと同じ名前を持つ必要がある制約に従う必要はありません。

    2つのサブスクリプションのための1つのコレクション
    2つのサブスクリプションのための1つのコレクション

    これをしたいと思う理由の一つはシングルテーブル継承です。 One reason why we would want to do this is Single Table Inheritance.

    投稿から共通のフィールドが格納されているだけでなく、 内容に若干異なってるオブジェクトのさまざまな型を参照したかったとします。 例えば、私たちは各投稿が通常のID、タイムスタンプ、およびタイトルを持ち、 それに加えて、画像、ビデオ、リンク、またはテキストだけを備えた Tumblrに似たブログエンジンを構築することができたとします。 Suppose that we wanted to reference various types of objects from our posts, each of which stored common fields but also differed slightly in content. For example, we could be building a Tumblr-like blogging engine where each post possesses the usual ID, timestamp, and title; but in addition can also feature an image, video, link, or just text.

    オブジェクトのどの種類かを示すためにtypeが属性を使用して、 単一の'resources'コレクション内にすべての種類のオブジェクトを格納することができます。 (videoimagelinkなど)

    We could store all these objects in a single 'resources' collection, using a type attribute to indicate which sort of object they are. (video, image, link, etc.).

    そして、サーバー上の単一Resourcesコレクションを持っていて、そして、 複数のVideosImages等に専用のクライアント上の単一コレクションにマジックの力で 変換することができます:

    And although we’d have a single Resources collection on the server, we could transform that single collection into multiple Videos, Images, etc. collections on the client with the following bit of magic:

      Meteor.publish('videos', function() {
        var sub = this;
    
        var videosCursor = Resources.find({type: 'video'});
        Mongo.Collection._publishCursor(videosCursor, sub, 'videos');
    
        // _publishCursor doesn't call this for us in case we do this more than once.
        sub.ready();
      });
    

    我々は_publishCursorに伝えるだけでビデオをパブリッシュできます。 (ただ返却するだけです。) しかし、クライアント上のresourcesコレクションとしてパブリッシュするのではなく、 その代わりに、‘resources’ではなく'videos’`としてパブリッシュしています。

    We are telling _publishCursor to publish our videos (just like returning) the cursor would do, but rather than publish to the resources collection on the client, instead we are publishing from 'resources' to 'videos'.

    別の似たアイデアは全くサーバ側のコレクションがないクライアント側のコレクションにパブリッシュを 使用することです! たとえば、サードパーティのサービスからのデータをつかみ、 クライアント側のコレクションでそれらをパブリッシュします。

    Another similiar idea is to use publish to a client side collection where there’s no server side collection at all! For instance, you might grab the data from a 3rd party service, and publish them into a client-side collection.

    パブリッシュAPIの柔軟性のおかげで、可能性は無限大です。

    アニメーション

    14

    期限切れ?

    Meteorの最新の改良により、この章では、少し情報が古くなっていることを告白します。

    私たちは、MeteorのAPIが1.0後に安定すれば、書き換えを計画しているので、 あなたがそれに取り組むのを待っていたい場合は、我々は理解します。

    今、Microscopeはリアルタイムの投票、スコアリング、ランキング機能を持っています。 しかし、ホームページ上で投稿が飛び回るのは耳障りで、不規則なユーザーエクスペリエンスにつながります。 我々は、ここを滑らかにするためにアニメーションを使用します。

    MeteorとDOM

    私たちは(動き回る部分を作る)面白い部分を開始する前に、 Meteorは、DOM(ドキュメントオブジェクトモデル - ページのコンテンツを構成するHTML要素コレクション)とどのように相互作用するかを理解する必要があります 。

    心に留めておくべき重要なポイントは、 DOMの要素が本当に「移動」することができないことです。 しかし、これらは(これはMeteorではなく、DOM自体の制限であることに注意してください)削除され、作成することができます。 したがって、要素A及びBの場所の切り替えの錯覚を与えるために、 Meteorが実際に要素Bを削除し、要素Aの前に新しいコピー(B’)を挿入します。

    私たちは、単にBが新しい位置に移動するアニメーションを作ることはできないので、 アニメーション操作が少しトリッキーになります。しかし、心配しないでください。私たちは道を見つけることができます。

    ソ連のランナー

    1980年は冷戦の時代でした。オリンピックは、モスクワで開催され、 ソ連はどんな犠牲を払っても100メートルのダッシュを勝つことを決定しました。 で、華麗なソ連の科学者のグループが選手にテレポーターを装備しました、 とすぐに銃声が聞こえた瞬間にランナーが瞬時にフィニッシュラインに移されました。

    ありがたいことに、レース関係者は直ちに違反に気づき、 他のみんなのように走ることで、レースに参加することが許されているので、 選手は、テレポートしてスタート台に戻るしか選択の余地がありませんでした。

    私の史料は、信頼性がないので話半分でその話を取る必要があります。しかし、 この章を進める上で「ソ連ランナーとテレポーターの話」を心に留め置いてください。

    Breaking It Down

    Meteorが更新を受信して、リアクティブにDOMを変更すると、私たちの投稿が瞬時にちょうどソ連ランナーのように、 その最終的な位置にテレポートされます。違いはオリンピックかアプリ内かどうかです。 しかし、我々は単にものが周りにテレポートすることはできません。 だから我々は戻って「スタート台」に要素をテレポートし、 それを ゴールラインまで"走らせる"を作ります(言い換えれば、アニメーション化)。

    そのようにポストA及びB(それぞれ、位置P1、P2に位置する)を切り替えるために、我々は次の手順を行います:

    1. Bを削除する
    2. DOMで、Aの前にB'を作成します。
    3. B'をP2へテレポート
    4. Aをp1にテレポート
    5. Aをp2にアニメーション
    6. B'をP1にアニメーション

    次の図は、より詳細にこれらの手順を説明します。

    Switching two posts
    Switching two posts

    もう一度くりかえしますが、手順3と4で、 Aとその位置にB ‘が即座に「テレポート」するので、アニメーションしていません。 これは瞬間的ですが、Bが削除されなかったような錯覚を与え、 適切に両方の要素が戻って彼らの新しい位置にアニメーション化されるように配置されます。

    Again, in steps 3 and 4 we’re not animating A and B’ to their positions but “teleporting” them instantly. Since this is instantaneous, it will give the illusion that B was never deleted, and properly position both elements to be animated back to their new position.

    ありがたいことに、Meteorはステップ1と2の制御をするので、唯一ステップ3〜6を心配する必要があります。

    また、ステップ5と6にあるすべての私たちはそれらの適切なスポットに要素を移動しているやっている。 だから、唯一の部分は、 私たちはアニメーションの出発点に要素を送信する、 すなわちステップ3、4を心配する必要があります。

    Moreover, in steps 5 and 6 all we’re doing is moving the elements to their proper spot. So the only part we really need to worry about is steps 3 and 4, i.e. sending the elements to the animation’s starting point.

    適切なタイミング

    これまではアニメーション化の話ではありませんでした。 今、私たちは私たちの投稿をアニメーション化する方法について話します。

    手順3と4の手段の答えは、投稿の _rankプロパティ(順序に依存する)を変化させることです。

    手順5と6は少しトリッキーです。こう考えてください: もし、完全に論理的なアンドロイドに「5分間北に走れ、終わったら、5分間南に走れ」と命令したとしたら、 次に、それはおそらく、最終的な位置が同じ場所になってしまいますので、 エネルギーを節約し、まったく実行されないでしょう。

    だから、全体の10分の間にあなたのアンドロイドが走ることを確実にしたい場合は、 それが最初の5分を走りきるまで待たなければならないということです。そしてその後に戻って来るように指示を出します。

    ブラウザは、同様の方法で動作します:単に古い座標を新たな座標に置き換えても、 同時にそれを両方の指示を出した場合には、何も起こらないでしょう。 言い換えれば、ブラウザは、時間単位で別個の点としての位置の変更を登録する必要があります。 そうしないと、それらをアニメーション化することができないという事です。

    Meteorはこのために組み込みのコールバックを提供できませんでした。しかし、 Meteor.setTimeout()を使うことで、数ミリ秒後に実行を延期し、近いことを実現できます。

    CSSによる位置決め

    ページの周りに並べ替えされている投稿をアニメーション化するために、 我々はCSSの領土に挑戦する必要があります。 CSSの位置決めの簡単な復習を順番にしていきます。

    ページ上の要素は、デフォルトでは静的な位置決めを使用しています。 静的に位置付け要素は、単にページのフロー内に収まると、 画面上の座標を変更したり、アニメーションすることができません。

    一方相対位置決め要素も、ページのフローに収まるって固定されますが、 元の位置に対して相対的に位置決めをできます。

    さらに一歩進んで、絶対的な位置決めは、あなたは要素の特定のx/ y座標が文書または 親要素に対する最初の絶対座標または相対座標に対する相対座標で与えることができます。

    私達は投稿をアニメーション化するために、相対的位置付けを使用します。 我々はすでにCSSの調整をしましたが、 すべては、あなたのスタイルシートにこのコードを追加するだけです:

    .post{
      position:relative;
      transition:all 300ms 0ms ease-in;
    }
    
    client/stylesheets/style.css

    ステップ5と6は非常に簡単になります:私たちが行う必要があるのは、投稿が元の位置に戻るように topから0pxへ(デフォルト値)にリセットし、 “ノーマル"の位置にスライドさせることです。

    基本的に、私たちの唯一の課題は それら(ステップ3,4)*から新しい位置に相対的にアニメーションさせる。ことを考えることです。 言い換えると、どれくらい移動させるかを考えます。 しかし、それは難しくはないです。:正確なオフセットは単純に投稿の前の位置から新しいポジションを引いた値です。

    So basically, our only challenge is figuring where to animate them from (steps 3 and 4) relative to their new position. In other words, how much to offset them. But that’s not very hard either: the correct offset is simply a post’s previous position minus its new one.

    position:absolute

    私たちは、相対的的な親要素からの座標指定にposition:absoluteを使うこともできます。 しかし、絶対配置要素の大きな欠点は、ページのフロー上から該当要素を削除した場合に、親コンテナも崩壊させる原因となってしまうことです。

    つまり、これは人工的に自然にブラウザリフロー要素を残すのは、 JavaScriptを介してコンテナの高さを設定する必要があることを意味します。そのため、可能な限りそれは相対的な位置に固執するのがベストです。

    Total Recall

    しかしもう一つの問題を持っています。 要素Aは、DOMに残り続けており、従ってその前の位置を「記憶」することができますが、 要素Bは生まれ変わりを経験し、そのメモリがきれいにリセットされB`として新たに生成されます。

    だから、私たちがやることはページ内の投稿の現在位置をローカルコレクションで登録することです。 このローカルコレクションは、ブラウザのメモリだけに存在している以外(つまりサーバー上に無い)は、 普通のMeteorのコレクションのように動作します。 この方法により、アニメーション化するため、投稿を削除して再作成した場合でも、知ることができるようにします。

    投稿ランキング

    投稿ランクについて、結果として私たちのコレクションにリストされている順序ですので、 この「ランク」は実際、投稿のプロパティとして存在しません。ランクに応じ投稿をアニメーション化できるようにしたい場合は、 我々は何とか無からこのプロパティを想起させる必要があります。

    ランクはあなたが投稿を並べ替える方法によって異なり、相対的な性質であるので、 我々は、データベース自体でこのrankプロパティを置くことができないことに注意してください。 (例えば、投稿を日付順でランク付けしたり、次にポイント順でソートしたりできます。)

    理想的には私たちのnewPoststopPostsコレクションでそのプロパティを置くでしょうが、 Meteorはまだこれを行うための便利なメカニズムを提供していません。

    なので、私たちは最後の可能なステップ、postListテンプレートマネージャでrankを挿入します:

    Template.postsList.helpers({
      postsWithRank: function() {
        return this.posts.map(function(post, index, cursor) {
          post._rank = index;
          return post;
        });
      }
    });
    
    /client/templates/posts/posts_list.js

    仕組みは、単にPosts.find({}, {sort: {submitted: -1}, limit: postsHandle.limit()})のカーソルを返す postsヘルパーと同じように postsWithRankはカーソルを取り、_rankを投稿それぞれに格納します。

    Instead of simply returning the Posts.find({}, {sort: {submitted: -1}, limit: postsHandle.limit()}) cursor like our previous posts helper, postsWithRank takes the cursor and adds the _rank property to each of its documents.

    postsListテンプレートの更新を忘れないでください:

    <template name="postsList">
      <div class="posts">
        {{#each postsWithRank}}
          {{> postItem}}
        {{/each}}
    
        {{#if nextPath}}
          <a class="load-more" href="{{nextPath}}">Load more</a>
        {{/if}}
      </div>
    </template>
    
    /client/templates/posts/posts_list.html

    Putting It Together

    私たちのアニメーションが私たちのDOM要素のCSS属性とクラスに影響を与えますので、 我々はpostItemテンプレートに動的な{{attributes}}ヘルパーを追加します:

    <template name="postItem">
      <div class="post" {{attributes}}>
    
      //..
    
    </template>
    
    /client/templates/posts/post_item.html

    このやり方で、{{attributes}}ヘルパーを使うことで、Sacebarsの隠された機能のロックを解除します: 返却されたattributesオブジェクトのプロパティはDOMのHTMLの属性(class, style等)にマッピングされます。

    それではattributesヘルパーを作成することによって、すべてのものを一緒に入れてみましょう:

    var POST_HEIGHT = 80;
    var Positions = new Mongo.Collection(null);
    
    Template.postItem.helpers({
    
      //..
    
      },
      attributes: function() {
        var post = _.extend({}, Positions.findOne({postId: this._id}), this);
        var newPosition = post._rank * POST_HEIGHT;
        var attributes = {};
    
        if (! _.isUndefined(post.position)) {
          var offset = post.position - newPosition;
          attributes.style = "top: " + offset + "px";
          if (offset === 0)
            attributes.class = "post animate"
        }
    
        Meteor.setTimeout(function() {
          Positions.upsert({postId: post._id}, {$set: {position: newPosition}})
        });
    
        return attributes;
      }
    });
    
    //..
    
    /client/templates/posts/post_item.js

    ドキュメントの最上部、つまり.postdiv要素内に各DOM要素の高さを、設定しています。 これは、この高さに変化が(例えば、記事のタイトルの折り返しして二行になった場合等) アニメーションのロジックを破壊する明らかな欠点が残ります。 しかし、物事はシンプルに保つために、すべての投稿が今のところ正確に80ピクセルの高さと仮定します。

    次に、我々はPositionsという名前のローカルコレクションを宣言しています。 ローカルコレクション(つまり、クライアント専用)であるように引数は、nullを渡す事に、注意してください。

    今、私たちはattributesヘルパーを構築する準備が整いました。

    Running Schedule

    多くの場合、リアクティブコードの一部を実行されたことを把握するのは難しいです。 それでは、attributesヘルパーについて深く見てみましょう。

    It can often be hard to figure out just when a piece of reactive code will run. So let’s take a deeper look at the attributes helper.

    テンプレートが最初にレンダリングされるときに、すべてのヘルパーは、一度だけ実行されます。 _rankプロパティへの依存のため、それはまた、項目が変更されるたびに、毎回投稿のランキングの変更を再実行します。 そして最後に、依存するPositionsコレクションへの変更も再実行することを意味します。

    Like every helper, it will run once when the template is first rendered. Because of its dependency on the _rank property, it will also re-run every time a post’s ranking changes. And finally, its dependency on the Positions collection also means it will re-run whenever the item in question is modified.

    これは、ヘルパーが連続して2回または3回実行するかもしれないことを意味します。 これは最初は無駄に思えるかもしれないが、それはちょうどリアクティブな方法の仕組みです。 あなたがそれに慣れると、それは単にコードを考えるための別の方法になります。

    This means that the helper might run two or three times in a row. This might seem wasteful at first, but it’s just the way reactivity works. Once you get used to it, it will just become another way of thinking about code.

    The Attributes Helper

    まず、Positionsコレクションに私達の投稿の位置を検索しますとクエリの結果を拡張したthis(このヘルパーにおいては現在の投稿に相当)、 がかえります。 次に、ページの先頭にDOM要素の新しい位置を把握するために、 _rankプロパティを使用します。

    First, we’ll look up our post’s position in the Positions collection and extend this (which in this helper corresponds to the current post) with the result of our query. We then use the _rank property to figure out the DOM element’s new position relative to the top of the page.

    我々は今、二つの別々のケースを管理する必要があります: テンプレートは、(A)をレンダリングしている、もしくは、プロパティが(B)を変更しているためリアクティブに実行している。 そのためにヘルパーが動いています。

    We must now manage two separate cases: either the helper is running because the template is being rendered (A), or it’s running reactively because a property changed (B).

    唯一ケースBだけで、要素をアニメーション化したいので、 ケースBであることを確認するため、post.positionが定義されていることを確認してください。 (後々定義されている方法を示します)。

    We only want to animate the element in case B, which is why we make sure that post.position is defined (we’ll see how it’s defined shortly).

    しかも、ケースBは、2つのサブケース、B1とB2が含まれています: B1はDOM要素「スタート台」(その前の位置)テレポートして戻っている。 B2はその前の位置から新しい位置へアニメートしている。 このどちらかです。

    What’s more, case B includes two sub-cases, B1 and B2: either we’re teleporting our DOM element back to the “starting blocks” (its previous position), or we’re animating it from its previous position to its new one.

    ここでoffset変数の話です。 我々は相対ポジショニングを使用しているので、我々は、 現在の位置に相対要素を送信するために場所を把握したいと思うところです。これは、以前の位置から新しい位置を差し引くことを意味します。

    This is where the offset variable comes in. Since we’re using relative positioning, we’ll want to figure out where to send the element relative to its current position. This means subtracting the new position from the previous one.

    ケースB1またはB2にいるかどうかを把握するためには、単にoffsetを見ればいいのです: offsetが0と異なるなら、それは我々が離れてその原点から移動していった要素だことを意味します。 一方offsetが0に等しい場合、それはその原点座標にアニメーションして戻って*いることを意味し、 我々はそれがゆっくりと遷移することを確認するために要素にクラスanimateを追加するべきであることを意味します。

    To figure out whether we’re in case B1 or B2, we can simply look at offset: if offset is different than 0, it means we’re moving the element away from its origin. On the other hand if offset is equal to 0, it means we’re animating the element back to its origin coordinate, and we can add the class animate to the element to make sure it transitions slowly.

    タイムアウト

    特定のプロパティが変更されたときに、これらの3つの状況(A、B1、およびB2)は、すべてのリアクティブにトリガされます。 この場合、setTimeout関数はPositionsコレクションを変更することにより、リアクティブコンテキストの再評価をトリガします。

    These three situations (A, B1, and B2) are all triggered reactively when certain properties change. In this case, the setTimeout function triggers the reevaluation of the reactive context by modifying the Positions collection.

    ユーザーが最初にページをロードするときに、リアクティブ動作の流れはこのようになります:

    • attributesヘルパーは、初めて実行されます。
    • post.positionが定義されていません。(A)
    • setTimeoutは実行されpost.positionを定義します。
    • リアクティブにattributesヘルパーが再実行されます。
    • offsetは0から0に移動するので(目に見えるアニメーションはなかった)、何の動きもおこりません。(B2)

    そして、ここは、upvoteが検出されたときに何が起こるかです:

    • _rankは変更されattributesヘルパーの再評価をトリガします。
    • post.position(B)によって定義されています。
    • ** offsetが0に等しくないので、アニメーションは存在しません。(B1)
    • setTimeoutpost.positionを再定義、実行します。
    • リアクティブにattributesヘルパー再実行されます。
    • offsetは0に戻ります(アニメーション実行)。(B2)

    今すぐあなたのサイトを開いて投票してみてください。投稿はスムーズに上下に移動し、バレエのような優雅に表示されるはずです!

    コミット 14-1

    Added post reordering animation.

    新規投稿のアニメーション

    投稿が正しく並べ替えていますが、まだ「新しい投稿」のアニメーションを持っていません。 新しい投稿が単純に私たちのリストの一番上にポップアップ表示される代わりに、フェードインするようにしてみましょう。

    //..
    
    attributes: function() {
      var post = _.extend({}, Positions.findOne({postId: this._id}), this);
      var newPosition = post._rank * POST_HEIGHT;
      var attributes = {};
    
      if (_.isUndefined(post.position)) {
        attributes.class = 'post invisible';
      } else {
        var delta = post.position - newPosition;
        attributes.style = "top: " + delta + "px";
        if (delta === 0)
          attributes.class = "post animate"
      }
    
      Meteor.setTimeout(function() {
        Positions.upsert({postId: post._id}, {$set: {position: newPosition}})
      });
    
      return attributes;
    }
    
    //..
    
    /client/templates/posts/post_item.js

    私たちはここでやっていることはケース(A)を分離し、要素にinvisibleのCSSクラスを追加しています。 ヘルパーが次にリアクティブに再実行され、要素が代わりにanimateクラスを取得すると、 不透明度のアニメーション変化により要素をフェードインされます。

    コミット 14-2

    Fade items in when they are drawn.

    CSS & JavaScript

    あなたは、アニメーションのために.invisible CSSクラスをトリガとして、 topで行ったCSSのopacityプロパティでアニメーション化していることに気づいたかもしれません。top`では、インスタンスデータの特定の値に依存したプロパティをアニメーションするために必要なことでした。

    一方、ここではデータに関係なく表示、非表示を行う必要がありました。 可能な限り、JavaScriptでのCSS操作は行わないことをお勧めしますので、 スタイルシートでアニメーションの細部動作を記述し、クラス追加、削除処理で動作するようにしました。

    我々は最終的に私たちが望んだアニメーションの振る舞いを記述する必要があります。 アプリをロードし、それを試してみましょう! そして、他の動きを考え出すことができるかどうかを確認するために、 .post.animatedクラスで遊ぶことができます。 ヒント:CSSの簡単な機能を試すには良い場所です!

    Meteor Vocabulary

    Sidebar 14.5

    ////

    クライアント

    クライアントの話をする時、FirefoxやSafariなどのトラディショナルブラウザーであろうと、 もしくはネイティブのiPhoneアプリケーションであるUIWebViewのような複雑なものであろうとも、ユーザーの “Webブラウザー"で上で実行されているコードの事を指します。

    コレクション

    Meteorコレクションとは、自動的にクライアントとサーバー間で同期するデータストアです。コレクションには(ポス トのような)名前があり、そして通常クライアントとサーバー上両方に存在しています。それらはお互い、別の動作を しますが、それらはMongo(モンゴ)のAPIを基本とした共通のAPIを持ちます。

    コンピュテーション(計算)

    コンピュテーション(計算)とは、変更によって一部のデータソースが反応すると毎回実行されるコードの塊です。もし あなたが、(例えばセッション変数などの)反応性データーソースを持っていて、それに反応的レスポンスをしてほしい のであれば、それのための計算を設定する必要があります。

    カーソル

    カーソルとは、Mongoコレクション上で照会(クエリ)が実行された結果です。クライアント側では、カーソルは結果だ けの配列に過ぎません。しかしながら、追加されたり、削除されたり、更新されたりとコレクションに関連がある複数の オブジェクトとして監視される”反応”側のオブジェクトです。

    DDP

    DDPとは、Meteorの分散データプロトコルで、ワイヤープロトコルは複数のコレクションを同期するために、そして、 メソッドを呼び出すのに使われます。DDPとは、重いデータを持つ複数のアプリケーションをリアルタイムで、HTTPの代 わりをする凡用プロトコルとして意図されています。

    Deps

    Depsとは、Meteorの反応性システムです。Depsは、HTML自動で基礎となるデーターモデルと同期し続けられるよう、 背後で使われています。

    ドキュメント

    Mongoは、ドキュメントベースのデーターストアであるため、コレクションから出てくる複数のオブジェクトは「ドキ ュメント」とよばれています。これらは、MeteorがDDPを介してそれらの特性を追跡するために使っている、共通の特 殊プロパティ「_id」、のような簡単なJavaScriptのオブジェクト(複数の関数は持てませんが)です。

    ヘルパー

    テンプレートがドキュメントプロパティよりもっと複雑なものをレンダリング(生成)する必要がある場合には、レンダ リングを支援する為に使用される関数を呼び出す事が出来ます。

    Latency Compensation (レイテンシーの埋め合わせ)

    サーバーからリスポンスが帰ってくるまで時間のズレを避けるため、クライアント上のメソッドコールのシュミレ ーションを可能にする技術です。

    Meteor Development Group (MDG)

    フレームワーク自身へ反対し、Meteorを開発している実在の会社です。

    メソッド

    Meteorメソッドは、レイテンシー補正をコレクションが変更し追跡し、許可するいくつかの特別なロジックを持つ、 クライアントからサーバーへのリモートプロシージャーコールです。

    MiniMongo

    クライアント側のコレクションは、Mongoの様なAPIを提供するインメモリデータストアです。この動作をサポートし ているライブラリーは、それがメモリ内で完全に実行されるMongoの小型版と位置づけられるため、「MiniMongo」と よばれています。

    パッケージ

    Meteorパッケージは、サーバー上で実行するJavaScriptコードで成り立っています。クライアント上で実行している JavaScriptコード、リソース(CSSへSASSのような)をどう手続きするのかを指示し、リソースが処理されます。
    パッケージはスーパーパワーライブラリーのようなものです。Meteorはコアパッケージの拡張セットが付属して おり、コミュニティのコレクションは、サードパーティーのパッケージを供給している Atmosphereもあります。

    パブリケーション

    パブリケーションとは、それに加入している各ユーザー向けにカスタマイズされたデーターの名前付きセットです。

    Server

    Meteorサーバーは、Node.jsを介して実行されるHTTPとDDPサーバーです。Meteorライブラリーとサーバーサイドの JavaScriptコードで構成されています。Meteorサーバーを起動する際、(開発に中それ自身で起動する)Mongoデー タベースに接続します。

    Session

    Meteorでのセッションは、ユーザーが実行状態であることを記録するためのアプリケーションによって、クライアント 側の反応性データベースを参照します。

    サブスクリプション

    サブスクリプションは特定のクライアント用のパブリケーションへの接続です。サブスクリプションは、データを同期の 状態にし続ける、サーバー上のパブリケーションと話す、ブラウザー上で実行するコードです。

    テンプレート

    テンプレートとはJavaScriptでHTMLを生成するメソッドです。デフォルトで、Meteorは、ロジックが少ないテンプレー トシステムであるスペースバーをサポートします。将来的にはより多くの物をサポートする計画もあります。

    テンプレートデータコンテキスト

    テンプレートがレンダリングすると、JavaScriptのオブジェクトを参照します。それらはより複雑になる可能性があり 、また、それらの上に利用できる機能を持っています。通常そのような複数のオブジェクトはPOJO(昔からある単なる JavaScriptオブジェクト)であり、しばしばコレクションからのドキュメントであったりします。