-
Notifications
You must be signed in to change notification settings - Fork 11
/
05-routing.md.erb
429 lines (312 loc) · 22.6 KB
/
05-routing.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
---
title: Routing
slug: routing
date: 0005/01/01
number: 5
contents: Meteorでのルーティングを学びます。.| 固有のURLで投稿ディスカッションページを作ります。(???)|どうやって適切にURLをリンクするのか学習します。
paragraphs: 72
version: 1.7
---
今のところ、投稿の一覧表ができたので(最終的にはユーザーが投稿するようにします)、
ユーザー同士で投稿を議論できる個別の投稿ページが必要です。
このページをパーマリンクを通じて、アクセスできるようにしたいと思います。
パーマリンクとは http://myapp.com/posts/xyz の形をした URL で、それぞれの投稿は固有のものです。( xyz の部分は MongoDB の _id 識別子です)
ブラウザーの URL バーの中に何があるのか調べてそれに対応して正しいコンテンツを表示するために、何らかのルーティングが必要となります。
### Iron Router パッケージを入れる
Iron Router は Meteor アプリのルーティングに特化したパッケージです。
Iron Router はルーティング(パスの設定)に役立つだけではなく、
設定をしたパスにアクションを割り当てるフィルターも処理します。
さらに、どのパスがどんなデータにアクセスするのか制御するサブスクリプションも扱います。
(注釈:Iron Router の一部は Discover Meteor の著者の一人であるトム・コールマンによって開発されました。)
最初に、Atmosphere からパッケージをインストールしましょう。
~~~bash
$ mrt add iron-router
~~~
<%= caption "Terminal" %>
このコマンドで iron-router パッケージをアプリにダウンロードしてインストールをしたので、すぐに使える状態となります。
パッケージを使えるようになる前に Meteor アプリを再起動する必要があるかもしれません。
(ctrl+c でプロセスを中止して、再び起動するために meteor を入力します。)
<% note do %>
### ルーティング用語集
この章では、ルーティングに関する様々な機能を学びます。
もしあなたが Rails などのフレームワークの経験があるなら、
ルーティングの概念についてすでに知っていることでしょう。
もしそうでなければ、ここでちょっとした用語集を用意しました。
- ルート:ルートとは、ルーティングの基本的な構成要素です。
アプリが URL を見つけたら、どこに行って、何をしたらよいかといった基本的な命令がセットになったものがルートです。
- パス:パスとはアプリ内での URL のことです。パスは静的な(/terms_of_service) や動的な(/posts/xyz)、 (/search?keyword=meteor)といったクエリパラメータもあります。
- セグメント:パスの様々なパーツのことで、前のスラッシュ(/)で区切られています。
- フック:フックとはルーティングプロセスの前や後、あるいはルーティングプロセス中に実行させるアクションです。
代表的な例として、ページを表示する前にユーザーが適切な権利を持っているか照合することが挙げられます。
- フィルター:フィルターとは、一つ以上のルートをグローバルに定義するフックです。
- ルートテンプレート: 各ルートはテンプレートを指し示す必要があります。
テンプレートを指定していない場合、ルーターはデフォルトでルートと同じ名前のテンプレートを探します。
- レイアウト:レイアウトとは、デジタルフォトフレームのようなものだと捉えることができます。
レイアウトはカレントテンプレートをラップするすべての HTML コードを組み入れ、
テンプレートが変化しても同じ状態を保ちます。
- コントローラ:時々、多くのテンプレートが同じパラメータで再利用できることに気づくでしょう。
コードを重複させるのではなく、ルーティングロジックをすべて含んだ1つのルーティングコントローラで
ルートを継承することができます。
Iron Router に関する詳細は、GitHub 上のドキュメンテーションで全文を見ることができます。
<% end %>
### Routing: テンプレートに URL を割り当てる
今まで、{{>postsList}} のようにハードコードされたテンプレートのインクルードを使ってレイアウトを作ってきました。
そのため、アプリの内容は変化できますが、ページの基本的な構造は常に同じです:
つまり、ヘッダーと投稿の一覧表です。
Iron Router が HTML の <body> タグの中をレンダリングしたものを引き継ぐことで、私たちはこの骨組みから抜け出すことができます。
そのため、私たちは通常の HTML ページのようにタグの中を自ら定義をしません。
その代わりに、{{> yield}} テンプレートヘルパーを持つ特別なレイアウトテンプレートをルーターに指定します。
この {{> yield}} ヘルパーは特別な動的な範囲を定義します。
動的な部分ではカレントルートに対応するあらゆるテンプレートに自動的にレンダリングします。
(私たちは今後からこの特別なテンプレートを「ルートテンプレート」と指定します。)
<%= diagram "router-diagram", "Layouts and templates.", "pull-center" %>
ではレイアウトを作って {{> yield} } ヘルパーを追加してみましょう。
はじめに、main.html から HTML の <body> タグの中身を layout.htmlの中のテンプレートに移動させます。
すると、スリムになった main.html は現在このようになっています。
~~~html
<head>
<title>Microscope</title>
</head>
~~~
<%= caption "client/main.html" %>
新しく作った layout.html は現在、アプリの外側のレイアウトを組み入れています。
~~~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>
~~~
<%= caption "client/views/application/layout.html" %>
postsList テンプレートを yield ヘルパーの呼び出しに差し替えたことに気づいたことでしょう。
また、その変更後はスクリーン上になにもないことにお気づきかと思います。
これはまだルーターに / URL で何を処理するか指定していないためです。
そのため、(it=ルーター?)は空っぽのテンプレートを出力しています。
はじめに、postsList テンプレートに/ URL ルートを割り当てて、以前と同じ状況にします。。
ルートのプロジェクトで /lib directory を作って、その中に router.js を作ります。
~~~js
Router.configure({
layoutTemplate: 'layout'
});
Router.map(function() {
this.route('postsList', {path: '/'});
});
~~~
<%= caption "lib/router.js"%>
これで2つの大事なことを行いました。
1つ目は、私たちが作ったデフォルトのレイアウトをすべてのルートで使うことをルーターに指定しました。
2つ目は、postsList という名前の新しいルートを定義して、/ パスを割り当てました。
<% note do %>
### The `/lib` folder
/lib フォルダ内に置いたものは、何よりも前に最初に読み込まれることが保証されています。
(スマートパッケージは例外です)
そのため、/lib フォルダはヘルパーコードを置く場所として最適です。ヘルパーコードは常に有効とする必要があるためです。
しかし、注意点が1つあります。 /lib フォルダは /client 、/server 内のどちらにもないので、
/lib フォルダの中のコードはどちらの環境でも有効化されるということに気をつけましょう。
<% end %>
### Named Routes
ここで少しあいまいな点をクリアにしましょう。
私たちはルートに postsList と名付けましたが、postsList という名前のテンプレートもあります。
すると、どんなことが起こるでしょうか。
デフォルト状態だと、 Iron Router はルートと同じ名前のテンプレートを探します。
実際には、ルートの名前に基づいてパスを探します。
ルートの定義にパスのオプションを与えたカスタムパスを定義していない場合、
テンプレートはデフォルトで /postsList の URL でアクセスできるということです。
そもそも、なぜでルートに名前をつける必要があるのでしょうか。
それはルートに名前を付けることで、簡単にアプリ内のリンクを作る Iron Router の機能を使うためです。
最も役立つ機能は {{pathFor}} という Spacebars ヘルパーです。これはあらゆるルートの URL パスの構成要素を返します。
メインホームリンクが投稿のリストを返すようにしたいので、
静的な / URL を指定する代わりに、Spacebars ヘルパーを使います。
最終的な結果は同じとなりますが、ルーターでルートのパスを変えたとしてもヘルパーは常に正しい URL を出力するため、
私たちはフレキシビリティを得ることができます。
~~~html
<header class="navbar">
<div class="navbar-inner">
<a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
</header>
//...
~~~
<%= caption "client/views/application/layout.html"%>
<%= highlight "3" %>
<%= commit "5-1", "Very basic routing." %>
### Waiting on Data
もし現在の状態のアプリをデプロイしたら(あるいは上記のリンクを使ってインスタンスをローンチしたら)、
投稿が現れる前のわずかな間はリストの中身がないことに気づくことだと思います。
これはページが最初に読み込まれるときは posts サブスクリプションがサーバーから投稿データを取ってくるまでの間に表示する投稿がないためです。
ユーザーに何が起きているのかという視覚的なフィードバックを与えることができれば、よりよいユーザーエクスペリエンスとなって、ユーザーは少しの間待ってくれることでしょう。
幸運なことに、Iron Router にはそうするための簡単な方法があります。 waitOn サブスクリプションです。
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.map(function() {
this.route('postsList', {path: '/'});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "3,4" %>
ひとつひとつ見ていきましょう。
最初に、loading template(これからすぐに作ります。)の名前をルーターに与えるために、私たちは Router.configure() ブロックを修正しました。
loading template はアプリがデータを待っている間にリダイレクトを行います。
次に、posts サブスクリプションを返す waitOn関数を加えました。
最後に、私たちはローディングフックを作りました。
これはユーザーがリクエストしたルートにユーザーを送る前に、
ルーターが posts サブスクリプションを読み込むようにします。
私たちは waitOn関数をルーターレベルでグローバルに定義しているので、
このシーケンスはユーザーが初めてアプリにアクセスした際に一度だけ起こります。
その後、このデータはブラウザーのメモリにすでに読み込まれ、ルーターは再び待つ必要がなくなります。
これでルーターがサブスクリプションを処理するようにしたので、
main.js のコードを安全に削除することができます。(main.js は現在何も入っていない状態です。)
サブスクリプションを待つことは良いアイデアです。それは単にユーザーエクスペリエンスのためだけではなく、データが常にテンプレート内から利用できると問題なく想定できるからです。
基礎的な情報を利用する前に、レンダリングしたテンプレートを処理する必要がなくなります。
そうするにはたいて巧妙な回避方法が必要です。
また、onBeforeAction フィルターを加えて、Iron Router に組み込まれた loading フックを動作させます。
そして、データを待っている間に loading テンプレートを表示するようにします。
~~~html
<template name="loading">
{{>spinner}}
</template>
~~~
<%= caption "client/views/includes/loading.html" %>
{{>spinner}} は spin パッケージに含まれている部分です。
この部分はアプリの「外側」から来ているわけですが、通常のテンプレートと同様にインクルードすることができます。
<%= commit "5-2", "Wait on the post subscription." %>
<% note do %>
### A First Glance At Reactivity
リアクティビリティは Meteor の根幹をなす部分ですが、これまで触れてきませんでした。ローディングテンプレートでこのコンセプトを垣間見ることができます。
データがロードされていないかどうかローディングテンプレートにリダイレクトすることは良いことですが、ユーザーを正しいページにリダイレクトする時をルーターはどうして分かるのでしょうか。(???)
今のところはとりあえずリアクティビリティがその役割をしているとだけ言っておきましょう。
このことについてはこの後すぐに学びますので、心配ご無用です!
<% end %>
### 特定の投稿にルーティングする
ここまでで、どのように postsList テンプレートにルーティングするのか見てきました。
ここでは1つ1つの投稿の詳細を表示するためのルートをセットアップしましょう。
(There's just one catch?):1つの投稿ごとにルートの定義をするとなると、数百の投稿に対して同じことをすることになり、先に進めなくなってしまいます。
そのため、1つの動的なルートをセットアップして、ルートに私たちが見たい投稿を表示させる必要があります。
まず初めに、テンプレートを単純にレンダリングするだけの新しいテンプレートを作ります。これは以前、私たちが投稿リストで使ったテンプレートと同じです。
~~~html
<template name="postPage">
{{> postItem}}
</template>
~~~
<%= caption "client/views/posts/post_page.html" %>
後でこのテンプレートにはコメントなどの要素を入れていきますが、今のところ {{> postItem}} インクルードのシェルのような役目を果たしています。
もう一つ指定したルートを作っていきましょう。
今回は、/posts/<ID> の形で URL パスを postPage テンプレートに割り当てます。
~~~js
Router.map(function() {
this.route('postsList', {path: '/'});
this.route('postPage', {
path: '/posts/:_id'
});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "4~6" %>
この特別な :_id 構文はルーターに2つのことを指定します:
1つ目は、/posts/xyz/ の形のルートをマッチさせます。/posts/xyz/ の “xyz” はどんなものでも問題ありません。
2つ目は、ルーターの params配列で _id プロパティ内の “xyz” 地点を見つけたものはどんなものでも中に入れます。
ここでは便宜上のために _id だけを使っていることに留意してください。
このルーターはあなたが実際の _id を渡しているのか、ランダムの文字列を渡しているのか判断することはできません。
私たちは今、正しいテンプレートにルーティングしていますが、まだ何か見逃しています:
ルーターは私たちが表示したい投稿の _id を識別しますが、テンプレートはまだ何もわからない状態です。
では、どのようにしてこのギャップを埋めるのでしょうか。
ありがたいことに、Iron Router には解決策が組み込まれています:
Iron Router は、テンプレートのデータコンテキストを指定するのです。
これはテンプレートとレイアウトで作られたおいしいケーキの中をデータコンテキストが満たしていると考えることができます。
簡単に言うと、 テンプレートの中をいっぱいに満たしているものがデータコンテキストです。
<%= diagram "router-diagram-2", "The data context.", "pull-center" %>
この場合、私たちは URL から得た _id を基にした投稿を探して正しいデータコンテキストを取得しています。
~~~js
Router.map(function() {
this.route('postsList', {path: '/'});
this.route('postPage', {
path: '/posts/:_id',
data: function() { return Posts.findOne(this.params._id); }
});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "4~7" %>
そのため、ユーザーがこのルートにアクセスするたびに、
私たちは適切な投稿を見つけて、その情報をテンプレートに引き渡します。
findOne がクエリにマッチした1つの投稿を返して、引数としての id を {_id: id} と略した表現であることを覚えておきましょう。
ルートの data関数内で、this は現在マッチしているルートに対応しています。
私たちは指定したルート部分にアクセスするために this.params を使うことができます。
(そのことを、私たちは path の中で:を前に置くことで表現しています。)(???)
<% note do %>
### More About Data Contexts
テンプレートのデータコンテキストを設定することで、テンプレートヘルパー内の this の値をコントロールすることができます。
これは {{#each}} イテレータで非明示的に処理されます。
{{#each}} イテレータは 自動的に現在繰り返し処理されるアイテムそれぞれに繰り返し処理のデータコンテキストを設定します。
~~~html
{{#each widgets}}
{{> widgetItem}}
{{/each}}
~~~
しかし、{{#with}} を使うことで同じことを明示的に行うことができます。
{{#with}} は単純に「このオブジェクトを取ってきて、次のテンプレートにそれを適用しろ」と示しています。
例えば、このように書くことができます。
~~~html
{{#with myWidget}}
{{> widgetPage}}
{{/with}}
~~~
テンプレートの呼び出しに、コンテキストを引数として引き渡すことで同じ結果を得ることができることがわかります。
そのため先ほどのコードのブロックはこのように書きなおすことができます:
~~~js
{{> widgetPage myWidget}}
~~~
<% end %>
### 動的に指定したルートヘルパーを使う
最終的に、私たちは個別の投稿にリンクをしたい時は常に正しい場所を指し示す必要があります。
再び、私たちは <a href="/posts/{{_id}}"> とすることができますが、ルートヘルパーを使った方がより確実です。
私たちは投稿ルートを postPage と指定したので、{{pathFor 'postPage'}} ヘルパーを使います。
~~~html
<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>
~~~
<%= caption "client/views/posts/post_item.html"%>
<%= highlight "6" %>
<%= commit "5-3", "Routing to a single post page." %>
でも待ってください。 /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' ボタンをクリックしましょう。
きっとこのように表示されるはずです。
<%= screenshot "5-2", "A single post page." %>
<% note do %>
### HTML5 pushState
もう一つ理解することは、URL の変化が HTML5 の pushState を使うことで起こっていということです。
Iron Router はアプリの状態に必要な変化をさせるだけでなく、
サイト内の URL クリックを取得して、
ブラウザーがアプリから(browsing away ?)することを防ぎます。
もしすべてが正しく動いているなら、ページは瞬時に変化します。
実際には、変化するのが早過ぎるため、ある種のページ移行が必要になります。
この点はこの章では範囲外ですが、興味深いトピックです。
<% end %>