##Meteorってなに
NodeJSのフルスタックなリアクティブ・フレームワークです。
裏でWebSocketを使っており、構築中にWeb画面がリアルタイムに変わっていく様子はかなり新鮮です。
すごいところは、フレームワーク内にMongoDBが内蔵されており、そのdbにクライアントサイドでもアクセスできます。(MiniMongoというclientサイドのDB機能が含まれているので、そういうことが可能であるみたいです。)
Baasのfirebaseも感動しましたが、あれはサーバサイドのCollectionとクライアントサイドのCollectionの同期は手動で紐付ける必要がありました。
MeteorはサーバからのCollectionの変更もクライアントからの変更も瞬時にお互いにミラーされ、
サーバサイドも含めての一つのアプリを作っているみたいです。
リアクティブプログラミングが可能なフレームワークでもあり、
例えばSessionの特定の値を監視して、変更された時だけログ出力するといったことも簡単にできます。
最近1.0になったよってアナウンスがあったので、チュートリアルをやってみました。
まだ書きかけでチュートリアルやりながらメモしつつという感じです。
##DiscoverMeteorをやってみる
DiscoverMeteorをやりながらチュートリアルしてみます
尚、DiscoverMeteor日本語訳版が出来つつあります。
インストール
インストールはすごく簡単ですがNodeJsはインストール済みであることが前提かもしれません。
curl https://install.meteor.com | sh
まずはチュートリアルに従い「microscope」とかいう「ソーシャルニュース」アプリを作ってみます。
$ meteor create microscope
meteor create <<プロジェクト名>>
でプロジェクトを作ります。プロジェクト名のディレクトリが作られます。
アプリ起動の仕方
$ cd microscope
$ meteor
簡単なアプリが動き出しましたね。
localhost:3000にアクセスします。
Ctrl+C
でアプリを終了します。
パッケージの使い方を学ぶ
meteorのパッケージ管理ツールでパッケージを追加していきます。
meteor add mizzao:bootstrap-3
meteor add underscore
mizzao:botstrap-3はこちらを参照
underscore packageの方はofficialなパッケージです。
meteorにはパッケージは5種類があります
- platform package:Meteor coreから分割された
- First-party package:Meteorチームがメンテしてる
- Third-party package:ここで探すパッケージ
- Local package:自作の
/packages
に置いたカスタムパッケージ - NPM package:NodeJsのモジュールパッケージ
ファイル構造
作ったばかりの構成はこんな感じです。
.meteor
microscope.css
microscope.html
microscope.js
上記のうち.meteor
以外は削除します。
代わりに以下のデイレクトリを作成します。
作成デイレクトリ | 役割 |
---|---|
/server | サーバサイドで動くコードを置きます |
/client | クライアントサイドで動くコードを置きます |
/public | 画像とか静的なファイルを置きます |
/lib | 後述します |
Meteorにおいて、あるルールで自動でファイルがロードされます。
なのでそのルールに従ってファイルをおけば勝手に読み込まれて組み込まれます。
ちなみに/server
と/client
でjsファイルを置くと、
それぞれサーバサイドとクライアントサイドでロードされますが、
それ以外のデイレクトリに置いた場合は 両方でロードされます。
ロードされるルールは以下となります。
- /libに置いたファイルが最優先でロード
- main.*のファイルがその次にロード
- ファイル名のアルファベット順にロード
あと.meteor
の中についてですが、
.meteor/packages
.meteor/release
がそれぞれ、
インストールされているパッケージ情報、meteorのバージョンがわかります。
それ以外はさわらないほうがいいとのこと。
以下の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;
}
.jumbotron{
text-align: center;
}
.jumbotron 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;
}
Templateの使い方を学ぶ
Meteorの開発手法としてガワから作っていって最後に内部を作り込むという手法がオススメだそうです。
まずは/client
の中で作業します
以下の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>
ポイントは{{> postsList}}
です。
ここには postsListテンプレートが入ります。
この後にpostsListテンプレートを作ります。
まずはテンプレートを置くディレクトリを作成しますね。
client/templates
さらにclient/templates/posts
も作って以下のファイルを置いていきます。
<template name="postsList">
<div class="posts">
{{#each posts}}
{{> postItem}}
{{/each}}
</div>
</template>
<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
</div>
</div>
</template>
注意点としてtemplateエレメントのnameであるpostsList
はどのテンプレート上でも使えます
このテンプレートエンジンはSpacebars
と言います。詳細はこちらで
まあ他のテンプレートエンジンに似ているので、なんとなく見てれば動きはわかります。
これを実際に動かすには、posts
オブジェクトをどこかで用意しないと・・・
ということで、以下のファイルを作成します。
var postsData = [
{
title: 'Introducing Telescope',
url: 'http://sachagreif.com/introducing-telescope/'
},
{
title: 'Meteor',
url: 'http://meteor.com'
},
{
title: 'The Meteor Book',
url: 'http://themeteorbook.com'
}
];
Template.postsList.helpers({
posts: postsData
});
ポイントはTemplate.postsList.helpers
です。
templateのpostsList
と紐づいています。
ちなみにpost_item.html
を見ていただくと
{{domain}}
が足りません。domain helper
を作りましょう
Template.postItem.helpers({
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
}
});
ちなみに
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
上記部分はDomain部分だけを取り出せるというjs技だそうです。
それと上記コードの **this
**について言及する必要があります。
これは、posts_list.html
内の{{#each posts}}
にて取り出される各要素がthis
に代入されています。
Meteor Collectionについて学ぶ
さっきまではクライアントサイドだったのでサーバサイドってことですね。
ちょっとここで感動ポイントきました。いったいなんなんだこれはッッ!!
まずは、以下のようにファイルを作成します。
Posts = new Mongo.Collection('posts');
ここであれ?っとなりました。/serverに置いてない・・・。
まずはアプリが起動中だと思うんで別タブを開いて新規Terminalから該当デイレクトリに移動します。
$ cd microscope
$ meteor mongo
これでmeteor専用のmongo環境に入ります。
以下のように
db.posts.insert({title: "A new post"});
,
db.posts.find();
とコマンドを打ってみてください。
> db.posts.insert({title: "A new post"});
> db.posts.find();
{title: "A new post", _id: ObjectId("..")};
次におもむろにブラウザのコンソールを開いてください。
Posts.findOne();
と打ち込みます。
Posts.findOne();
{title: "A new post", _id: LocalCollection._ObjectID};
すごくないですか・・・どういう仕組みかわからないですが、
一気にブラウザまで反映されてます。
もしかして・・・・と思いおもむろに、ブラウザのコンソールから以下を叩きます。
Posts.insert({title:"Second post"});
Posts.insert({title:"Second post"});
"tsCY2Wk4L2p7GvGnn"
なんかIDっぽいのが返ってきた・・・。
Terminalに戻ってコマンドdb.posts.find()
を打ちます。
meteor:PRIMARY> db.posts.find();
{ "_id" : ObjectId("545439d965f913c98bf37b97"), "title" : "A new post" }
{ "title" : "Second post", "_id" : "tsCY2Wk4L2p7GvGnn" }
サーバにも反映された・・・・すごすぎ。
このクライアントサイドで動いているmongoDBはminiMongoというらしいです。
引き続きチュートリアルを続けるため、作ったDBの情報をリセットします。
アプリが起動中のTerminalでctrl+C
で抜けて以下のコマンドを実行します。
$ meteor reset
$ meteor #再起動
以下のファイルを作成
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'
});
}
client/templates/posts/posts_list.js
を書き換えます。
Template.postsList.helpers({
posts: function() {
return Posts.find();
}
});
書き換えた瞬間に変わるのはすごい・・・。
ところで、ちょっと復習なんですが
Posts = new Mongo.Collection('posts');
変数宣言にvar
が付いてないのわかりますでしょうか?
var
をつけると そのファイル内でスコープが閉じられるそうです。このPosts
はアプリ全体で使いたいので、
var
は付けずに変数宣言を行います。
もう一点。
Template.postsList.helpers({
posts: function() {
return Posts.find();
}
});
上記のPosts.find()
では実はArrayではなくcursorが返ります。今流行りのリアクティブプログラミングというものらしいです。cursorはまだこの段階ではDBから値を取得しておらず、テンプレートエンジンでレンダリング直前に取得するようです。
このようにして透過的にサーバからの変更内容をクライアントに通知するわけですね。
なので、ログ出力する場合はPosts.find().fetch()
でArrayに変換してください。
詳細はこちら
CollectionがClientと全共有されている問題の解決
標準では、autopublish
パッケージが有効になっています。
autopublishはclientのCollectionとServerのCollectionがミラーリングされます。
meteor remove autopublish
削除することによってこの機能をOffにします。
代わりに以下のファイル追加によってCollection単位で有効化します。
この辺の詳しい仕組みはdiscovermeteor:Publications and Subscriptionsを読むと良さそうです。(そのうち翻訳したい)
サーバサイドの公開処理
Meteor.publish('posts', function() {
return Posts.find();
});
クライアントサイドの受け入れ処理
Meteor.subscribe('posts');
ルーテイングの設定を行います。
以下のコマンドでルーテイングのパッケージを導入します。
meteor add iron:router
ルーテイングを行うにあたりヘッダやポスト一覧の表示などは
urlが変わっても基本的に同じ構成にしたいです。
まずはmain.htmlファイルを作成します。
今回使うiron-routerはbodyタグの中を作りこんでいきますので、それ以外をmain.htmlに作成します。
<head>
<title>Microsope</title>
</head>
次は"layout" Templateを作成します。
<template name="layout">
<div class="container">
<header class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
</header>
<div id="main" class="row-fluid">
{{> yield}}
</div>
</div>
</template>
ルーテイングの設定で常に上記Templateが呼ばれるように設定します。そしてurlによって{{> yield}}
の箇所が変わるようにします。
以下のファイルを追加
Router.configure({
layoutTemplate: 'layout'
});
Router.route('/', {name: 'postsList'});
name
プロパティに設定したTemplateが{{> yield}}
に入るようになります。
それにしてもrouter.js
はlib
に入れるんですね。/server
か/client
のどちらかに入れると思ってました。
/lib
に入れるのはアプリが何かする前に呼ばれることを保証するためだそうです。でも共用コードに含まれることになるのは気持ちが悪いような・・・・。
ローディングインジケータを表示
ページを最初に開いたとき、Meteor.subscribe('posts')
が実行完了するまで時間がかかるため(サーバからデータを取得するまで完了しないため)空欄表示になってしまいます。
それを防ぐための処理を追加していきます。
Iron Router
の機能を利用します。
まずはmain.js内のMeteor.subscribe('posts');
をこちらに 移動させます。
Router.configure({
layoutTemplate: 'layout',
+ waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
この変更によって、Iron Router
はそのルートへの表示準備が完了したことが
わかるようになったわけです。
さて準備中は組み込みのローディング中表示が出るわけですが、こちらも自分好みに当然修正可能です。
また、lib/router.js
を変更します。
Router.configure({
layoutTemplate: 'layout',
+ loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
ちなみににこの設定は全ルート共通にwaitOn関数を定義しているため、
本アプリに一番最初にアクセスした瞬間にだけ実行される点に注意してください。
ブラウザに記憶されたら、以後実行されません。
loading Template
を設定しましょう。
スピナー表示のためのモジュールを導入します。
$ meteor add sacha:spin
以下のファイルも作成
<template name="loading">
{{>spinner}}
</template>
ですがスピナーの変化わからない・・・
post詳細画面の追加
postの詳細画面を追加してみます。
まずRouteを追加します。
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
+ Router.route('/posts/:_id', {
+ name: 'postPage',
+ data: function(){return Posts.findOne(this.params._id);}
+ });
lib/router.js
のdata
関数で取得した結果が
postPage
Templateのthis
と紐付きます。
/posts/:_id
の記述はよくあるやつですね。
urlの:_id
に対応する文字列がthis.params._id
で取得できると。
対応するTemplateを追加
<template name="postPage">
{{> postItem}}
</template>
全然詳細画面じゃない。postItem Template流用ですね。
ところでthis
とtemplateの対応について補足があります。
{{#each widgets}}
{{> widgetItem}}
{{/each}}
上記の場合widgetsがCollectionで各要素がwidgetItem Templateのthis
に入ります。
これはいつものやつですね。
他にもありまして、
{{#with myWidget}}
{{> widgetPage}}
{{/with}}
と
{{> widgetPage myWidget}}
は全く同じ意味で、 myWidget
がwidgetPage
Templateのthis
に入ります
これでとりあえず詳細画面はできましたが、そこに遷移するためのリンクがないです。
それを作ります。
まずはpostsList画面にDiscussボタンを作りそこからリンクします。
<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 btn-default">Discuss</a>
</div>
</template>
これで動くようになります。・・・
でも実はちょっと変なところがあります。
a href="{{pathFor 'postPage'}}"
って変換されるとa href="/posts"
ですよね・・・。
これで動くって変じゃないですか・・。
これはIron Router
が頭がいいことにrouter設定を元にidが必要だということを読み取って対応してくれたらしいです。
NotFoundページへの遷移を追加
Templateから作成
<template name="notFound">
<div class="not-found jumbotron">
<h2>404</h2>
<p>Sorry, we couldn't find a page at this address.</p>
</div>
</template>
ルーテイング変更
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
+ notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
//...
適当に遷移してみてちゃんと表示されることを確認します。
でもこれだとlocalhost:3000/posts/xyzにアクセスしてもNotFoundにならないです。
データが存在しない場合にもNotFoundに行きたい場合は以下のように追加します。
//一番下に追記
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
これはdata
関数がnull, false, undefined,emptyのいずれかを返した場合に
notFound 扱いにします。
sessionの話
sessionはグローバル変数みたいで嫌うことも多いですが、クライアント固有の情報、
例えば一時的にクライアント上非表示にしているなど、クライアント固有の情報保存には必要です。
使い方は簡単です。
ブラウザのコンソールから以下のように実行してください。
Session.set('pageTitle', 'A different title'); //値の設定
Session.get('pageTitle'); //値の取得
ここですごいSession.set
の話。
セットする値が設定済みの値と同じ場合は関数呼び出しを避ける仕組みになっているそうです。(仕組みが想像できない)
もう一つ、内部的な仕組みが想像できないけどすごい機能です。
Autorun
です。
以下のコードをブラウザコンソールから実行してみてください。
Tracker.autorun( function() { console.log('Value is: ' + Session.get('pageTitle')); } );
Session.set('pageTitle', 'Yet another value');
Value is: Yet another value
設定した瞬間に出力されとる・・・。
autorun
ブロック内のコードが内部のSession.get('pagetitle')
の変更状態を監視して、変更されると自動実行されるというものです。
ドキュメント曰くsessionはreactive data sources
だから可能とのこと。詳細はこちらで
あと最後に補足です。 開発中はリロードボタンを押すとセッション情報消えます。
ユーザ管理
ユーザ管理の使い方です。
bootstrapを使っているので以下のモジュールを導入します。
meteor add ian:accounts-ui-bootstrap-3 #accounts-uiのbootstrap版
meteor add accounts-password
headerを拡張してアカウントログインボタンを組み込みます。
以下のようにファイルを変更
<template name="layout">
<div class="container">
- <header class="navbar navbar-default" role="navigation">
- <div class="navbar-header">
- <a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
- </div>
- </header>
+ {{> header}}
<div id="main" class="row-fluid">
{{> yield}}
</div>
</div>
header Template
を追加
<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 navbar-right">
{{> loginButtons}}
</ul>
</div>
</div>
</nav>
</template>
ポイントは{{> loginButtons}}
の箇所ですね。詳細はこちらで
これで組み込みのaccount-ui画面が出るようになります。
実際にアカウントを作ってログインしてみましょう
アカウントは作成できましたか?
実はaccounts
パッケージを入れることで、自動でCollectionが作成されます。Meteor.users
です。
実際にブラウザコンソールからアクセスしてみましょう。
Meteor.users.findOne();
Object {_id: "foRHJjMWe8L9H4Bdi", profile: Object, username: "abe"}
帰ってきた結果を見てみましょう。
_isとusername,profileくらいしかないのが確認できると思います。
ちなみにMeteor.user()
でも同じ結果が返ります。
サインアウトしてもう一つアカウントを作ってみます。
そのアカウントで再ロングインしてください。
今度は以下のコマンドをブラウザコンソールから実行します。
Meteor.users.find().count();
1
2じゃなくて1が返ってきます。
今度はMongoshell(Terminalからmeteor mongo
)から見てみましょう
meteor:PRIMARY>db.users.count()
2
こっちはちゃんと2が返ってきました。
これはなんでか?
実はautopublish
パッケージを削除したことを覚えていますか?
これを削除することによってサーバ側のCollectionとクライアント側のCollectionのミラーを停止させたはずです。
そしてpublication
とsubscription
の設定を行ったことも。
なのにMeteor.users
Collectionは見えましたね。
実はaccount
パッケージがやってくれたんです。便利ですね。
account
パッケージは現在のユーザ情報だけをpublish
にしてくれたわけです。クライアントからは自分以外の情報が見えてはいけないですからね。
それだけではなくフィールドのフィルタも行ってくれてます。
見てみましょう
meteor:PRIMARY> db.users.findOne()
{
"_id" : "55vyZRn3Na2uietY8",
"createdAt" : ISODate("2014-11-01T13:49:52.625Z"),
//なんかたくさんでる・・
}
}
これでクライアント側はフィールドがフィルタされてるってことがわかったかと思います。
リアクティブプログラミングを学ぶ
リアクティブプログラミングについて調べると、
大抵例としてExcelの話が出ます。Excelの各Cellには値だけでなく関数を記述すると思います。
そして依存しているCellの値を書き換えると瞬時に関連するCellの値が再計算されます。
これをフレームワークとしてプログラミングの世界でも適用しようというのがMetelorの思想になります。
javascriptで値監視をする場合は.observe()
関数を使うと思います。
例えばこんな風に書くとか
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();
}
});
もう見ただけで将来破綻することが目に見えています。
Meteorでは宣言的アプローチを行います。
オブジェクト間の関係性を記述し、自動で値監視を行うようにします。
例えば以下のように記述します。
<template name="postsList">
<ul>
{{#each posts}}
<li>{{title}}</li>
{{/each}}
</ul>
</template>
Template.postsList.helpers({
posts: function() {
return Posts.find();
}
});
上記のように記述しておくことによってMeteorが.observe()
関数をつかって値の変化とHTMLの要素の対応を覚えてくれます。
計算処理についてもmeteorは監視してくれます。
計算処理の中にreactive data source
が含まれていればその計算は該当するreactive data source
の変化に影響を受けることは明らかなので依存対象に自動で含めてくれます。
例を挙げてみます。
Meteor.startup(function() {
Tracker.autorun(function() {
console.log('There are ' + Posts.find().count() + ' posts');
});
});
上記のMeteor.startup()
ブロック内のコードはPost Collectionがロード完了した際に一度だけ実行されます。
見るべきポイントはautorun
ブロックです。
Posts.find()
はreactive data source
であるcursor
を返します。つまりブロック内の計算はPosts.find()
の変化に影響するため、依存対象に含まれます。
結果、posts
の内容が変化するたびに自動で実行されるわけです。
以下実行例を示します。
> Posts.insert({title: 'New Post'});
There are 4 posts.
長くなったので続きはこっちで