-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy path05-routing.md.erb
414 lines (277 loc) · 31.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
---
title: Маршрутизація
slug: routing
date: 0005/01/01
number: 5
contents: Навчитесь маршрутизації в Meteor.|Створите сторінки обговорення посту з унікальними URL.|Навчитесь як правильно створювати ссилки на ці сторінки.
paragraphs: 84
---
Тепер, коли ми маємо список постів (які будуть затверджуватись користувачем), нам потрібна окрема сторінка посту, де наші користувачі зможуть обговорювати кожен пост.
Ми хотіли б, щоб ці сторінки були доступні через *постійну лінк* -- URL у вигляді `http://myapp.com/posts/xyz` (де `xyz` -- це MongoDB `_id` ідентифікатор) унікальний для кожного посту.
Це значить, що нам треба деяке *маршрутування* для того, щоб глянути в адресний рядок браузера і видати відповідний правильний контент.
### Додавання пакету Iron Router
[Iron Router](https://github.com/EventedMind/iron-router) -- це пакет маршрутизації, розробленний спеціально для Meteor додатків.
Він не тільки допомагає з маршрутизацією (встановленням шляхів), але також може ставити фільтри (призначаючи деякі дії для цих шляхів) і навіть управляти підписками (контролювати відповідно який шлях має доступ до яких данних). (Цікаво, що: Iron Router був розроблений за участі співавтором книги *Discover Meteor* Томом Коулманом.)
По-перше, давайте встановимо пакет з Atmosphere:
~~~bash
$ meteor add iron:router
~~~
<%= caption "Terminal" %>
Ця команда завантажує і інсталює пакет iron-router в наш додаток, готовий до використання. Зверніть увагу, що інколи вам треба рестартувати ваш Meteor додаток (через `ctrl+c` для вбивства процесу, тоді `meteor` стартує його знову) перед використанням пакету.
Зверніть увагу, що Iron Router це пакет 3-ої сторони, що значить вам треба буде Meteorite для його установки (`meteor add iron-router` не спрацює).
<% note do %>
### Словник маршрутизатора
Ми розглянемо багато різних функцій маршрутизатора в цьому розділі. Якщо у вас вже є певний досвід роботи з таким фреймворком як Rails, то ви вже знайомі з більшістю цих концепцій. Але якщо ні, то ось короткий словник для прискорення навчання:
- **Маршрут**: Маршрут -- це базовий будівельний блок маршрутизації. Він є набором інструкцій, який повідомляє додатку куди йти і що робити, коли він отримує URL.
- **Шляхи**: Шлях -- це URL всередині вашого додатку. Він може бути статичним (`/terms_of_service`) або динамічним (`/posts/xyz`) і навіть включати параметри запиту (`/search?keyword=meteor`).
- **Сегменти**: Різні частини шляху відокремленні косою рискою (`/`).
- **Хуки**: Хуки -- це дії, які вам необхідно виконати до, після або, навіть, під час процесу маршрутизації. Типовим прикладом є перевірка чи має користувач права перед відображенням сторінки.
- **Фільтри**: Фільтри -- це просто хуки, які ви визначаєте глобально для одного чи більше маршрутів.
- **Шаблони маршрутизації**: Кожен маршрут повинен вести до шаблону. Якщо ви не вказали його, то за замовчуванням маршрутизатор буде шукати шаблон з таким же іменем.
- **Макети**: Ви можете думати про макети як, припустимо, про цифрові фоторамки. Вони містять весь HTML код, що обгортає поточний шаблон і буде залишатися таким же, навіть при зміні шаблону.
- **Контролери**: Інколи ви будете усвідомлювати, що багато ваших шаблонів використовують однакові параметри. Щоб не дублювати код, ви можете дозволити всім цим маршрутам наслідовати один *контролер маршруту*, який буде містити всю маршрутну логіку.
Для більшої інформації про Iron Router, перевірте [повну документацію на GitHub](https://github.com/EventedMind/iron-router).
<% end %>
### Маршрутизація: прив’язка URL до шаблонів
Як бачите, ми побудували наш макет використовуючи закодовані шаблонні включення (такі як `{{>postsList}}`). Але хоча контент нашого додатку може змінюватись, базова структура сторінки завжди одна й та ж: шапка зі списком постів знизу.
Iron Router дозволяє нам вирватися з цього болота взявши на себе рендеринг того, що всередині HTML тегу `<body>`. Тому ми не будемо визначати тег контенту, як би ми звичайно робили зі звичайною HTML сторінкою. Замість цього ми направимо маршрутизатор до спеціального макетного шаблону, що містить хелпер шаблону `{{> yield}}`.
Цей `{{> yield}}` хелпер визначає спеціальну динамічну зону, яка автоматично відрендерить те, що шаблон визначив до поточного шляху (домовимося, що тепер ми будемо називати цей спеціальний шаблон як “шаблон маршрутизації):
<%= diagram "router-diagram", "Макети і шаблони.", "pull-center" %>
Ми почнемо з створення нашого макету и добавимо хелпер `{{> yield}}`. По-перше, ми видалимо наш HTML `<body>` тег з `main.html` і перекинемо його зміст в його власний шаблон `layout.html` (який ми помістимо всередину нової `client/templates/application` директорії).
Iron Router потурбується про включення нашого макету в наш зменшенний шаблон `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’ом, тому він просто видає пустий шаблон.
Для початку, нам потрібно відтворити стару поведінку прив’язавши кореневий `/` URL до шаблону `postsList`. Ми створимо директорію `/lib` в корні нашого проекту і всередині її створимо `router.js` :
~~~js
Router.configure({
layoutTemplate: 'layout'
});
Router.route('/', {name: 'postsList'});
~~~
<%= caption "lib/router.js"%>
Ми зробили дві важливі речі. По-перше, ми наказали маршрутизатору використовувати щойно створенний макет `layout` за замовчуванням для всіх маршрутів.
По-друге, ми визначили новий маршрут під назвою `postsList` і зв’язали його до шляху `/`.
<% note do %>
### Каталог `/lib`
Все, що ви кладете всередину теки `/lib` гарантовано завантажиться найпершим перед усим іншим у вашому додатку (можливе виключення - це при використанні смарт-пакетів). Це дуже добре місце для будь-якого коду хелпера, який має бути доступним увесь час.
Тому маленьке застереження: зверніть увагу, що так як тека `/lib` не знаходиться в `/client` або `/server`, то це значить її зміст буде доступний для обох середовищ.
<% end %>
### Маршрути з іменем
Давайте позбавимось тут деякої неоднозначності. Ми назвали наш маршрут `postsList`, але також ми маємо шаблон *template* з назвою `postsList`. Що ж відбувається?
За замовчуванням, Iron Router буде шукати шаблон з таким же іменем, що і маршрут. Насправді, він навіть виведе ім’я з наданого вами *path*. Але це не спрацює в цьому окремому випадку (так як наш шлях `/`), Iron Router не знайшов би правильний шаблон, якщо б він використовував `http://localhost:3000/postsList` як наш шлях.
Ви може будете розмірковувати, навіщо нам спочатку давати імена нашим маршрутам. Іменування маршрутів дозволяє нам використовувати декілька функцій Iron Router, що полегшує побудову лінків всередині нашого додатку. Найбільш корисним є Spacebars хелпер `{{pathFor}}`, який видає URL параметр шляху для будь-якого маршруту.
Нам потрібно, щоб наш лінк головної домашньої сторінки направляв нас назад до списку постів, тому замість визначення специфічного статичного `/` URL, ми можемо використати Spacebar хелпер. Кінцевий результат буде один і той же, але це нам дає більше гнучкості, так як хелпер завжди виводить правильний URL навіть якщо ми змінюємо шлях в маршрутизаторі.
~~~html
<header class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
</header>
//...
~~~
<%= caption "client/views/application/layout.html"%>
<%= highlight "3" %>
<%= commit "5-1", "Дуже проста маршрутизація." %>
### Очікування данних
Якщо ви розмістите поточну версію додатку (або запустите веб-об’єкт використавши лінк вище), то ви помітите, що пустий список з’являється трошки раніше, ніж пости. Це відбувається тому, що спочатку завантажується сторінка і немає постів для відображення поки `posts` підписка не оформиться шляхом отримання данних постів від сервера.
Було б краще для UX спочатку відобразити деяку графічну інформацію про те, що щось відбувається і що користувачу треба трохи зачекати.
На щастя Iron Router дає нам легкий шлях вирішення цього ми можемо попросити його *зачекати* на підписку -- метод `waitOn`.
Почнемо перекинувши нашу `posts` підписку з `main.js` до маршрутизатора:
~~~js
Router.configure({
layoutTemplate: 'layout',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "3" %>
Тут ми вказуємо, що *кожен* маршрут сайту (у нас поки що один, але скоро їх стане більше!) ми підписуємо на `posts` підписку.
Ключовою різницею між цим і тим, що було раніше (коли підписка була в файлі `main.js`, який тепер має бути пустим і його можна видалити), є те, що зараз Iron Router знає коли маршрут "готовий" -- тобто, коли маршрут має дані, що треба відрендерити.
### Завантаження цього
Знання про готовність маршруту `postsList` не зробить нам багато добра, якщо ми збираємось вивести просто пустий шаблон. На щастя, Iron Router іде з вбудованним показом шаблону завантаження `loading` поки не буде готовий виклик маршруту:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "3,4" %>
Зверніть увагу, що так як ми визначили нашу `waitOn` функцію глобально на рівні маршрутизатора, то ця послідовність відбудеться лише раз при першому завантаженні користувачем вашого додатку. Піссля цього, дані вже будуть знаходитись в пам’яті браузера і маршрутизатору не прийдеться знов чекати.
Фінальним куском пазлу є поточний шаблон завантаження. Ми використаємо `spin` пакет для створення прикольного анімованого спінера. Добавте його за допомогою `meteor add sachag:spin`, а далі створіть шаблон завантаження `loading` в директорії `client/templates/includes`:
~~~html
<template name="loading">
{{>spinner}}
</template>
~~~
<%= caption "client/views/includes/loading.html" %>
Зверніть увагу, що `{{>spinner}}` -- це частинка, що знаходиться в пакеті `spin`. І навіть коли ця частинка знаходиться “поза” нашого додатку, ми можемо включати її так само як і будь-який шаблон.
Звичайно хорошею ідеєю є зачекати на ваші підписки, не тільки з точки зору UX, але також це значить, що ви безпечно допускаєте, що даны завжди будуть доступні з середини шаблону. Це дозволяє уникнути потреби мати справу з шаблонами перед тим, як нижчі дані стануть доступні, що в свою чергу вимагає змисленних костилів.
<%= commit "5-2", "Зачекайте підписки на пост." %>
<% note do %>
### Перший погляд на реактивність
Реактивність - це базова частина Meteor і хоча ми ще не розповідали про неї, наш шаблон завантаження дає нам перше уявлення цієї концепції.
Редирект до шаблону завантаження, якщо дані ще не завантажились - це добре, але як маршрутизатор знає, коли перенаправляти користувача *назад* до правильної сторінки, коли дані прийдуть?
Наразі, давайте припустимо, що це саме тей момент, коли стає в нагоді реактивність і залишимо це як є. Але не хвилюйтесь, ви дуже скоро дізнаєтесь більше про це!
<% end %>
### Маршрутизація до певного посту
Тепер, коли ми побачили як направляти до шаблону `postsList`, давайте встановимо маршрут для показу частин окремого посту.
Є одна особливість: ми не можемо поспішати і визначати один маршрут для кожного посту, так як може бути сотні них. Тому нам необхідно встановити єдиний *динамічний* маршрут і зробити так, щоб цей маршрут відображав кожен потрібний пост.
Для початку, створимо новий шаблон, що просто рендерить такий само шаблон посту, який ми використовували раніше для списку постів.
~~~html
<template name="postPage">
{{> postItem}}
</template>
~~~
<%= caption "client/views/posts/post_page.html" %>
Добавимо більше елементів до цього шаблону пізніше (таких як коментарі), але зараз це слугуватиме як оболонка для нашого `{{> postItem}}`.
Ми збираємося створити ще один маршрут з іменем, прив’язавши наші URL шляхи у формі `/posts/<ID>` до шаблону `postPage`:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage'
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "8~10" %>
Спеціальний синтакс `:_id` повідомляє маршрутизатору дві речі: по-перше, співставити будь-який маршрут у формі `/posts/xyz/`, де “xyz” може бути все що завгодно. По-друге, віддати все, що він знайде в цій “xyz” штуці всередині `_id` параметру в масив роутера `params`.
Зверніть увагу, що ми тільки використовуємо `_id` для зручності. Рутер не знає чи ви передаєте дійсно існуючий `_id`, чи просто якийсь випадковий рядок символів.
Тепер ми проведемо маршрут до правильного шаблону, але у нас ще чогось не вистачає: роутер знає `_id` посту, що нам треба вивести, але шаблон поки що не має ключа. Як ми подолаємо цю розбіжність?
На щастя роутер має розумне встроєне рішення: він дозволяє визначити вам **контекст даних шаблону**. Ви можете думати про контекст даних як заповнення всередині чудового тістечка, зробленного з шаблонів і макетів. Просто визначіть, чим вам треба заповнити ваші шаблони:
<%= diagram "router-diagram-2", "Контекст даних.", "pull-center" %>
У нашому випадку, ми можемо отримати відповідний контекст даних пошукавши наш пост, базуючись на `_id`, який ми отримали з URL:
~~~js
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); }
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "10" %>
Кожного разу, коли користувач йде по цьому маршруту, ми знаходимо відповідний пост і передаємо його в шаблон. Пам’ятайте, що метод `findOne` повертає єдиний пост відповідно запиту і далі віддає просто `id` як аргумент, що є скороченним записом `{_id: id}`.
Всередині функції `data` для маршруту, вираз `this` відповідає поточному співставленному маршруту і ми можемо використовувати `this.params` для доступу до іменованих частин маршруту (який ми ідентифікували поставивши префікс `:` всередині нашого `path`).
<% note do %>
### Більше про контексти даних
Встановивши шаблонний *контекст данних*, ви можете управляти значенням `this` всередині хелперів шаблону.
Це звичайно робиться неявно за допомогою `{{#each}}` ітератору, який автоматично встановлює контекст данних для кожної ітерації ітерованого елементу:
~~~html
{{#each widgets}}
{{> widgetItem}}
{{/each}}
~~~
Ми також можемо це робити неявно, використовуючи `{{#with}}`, який просто каже "візьми цей об’єкт і використай для нього цей шаблон". Наприклад, ми можемо написати:
~~~html
{{#with myWidget}}
{{> widgetPage}}
{{/with}}
~~~
З’ясовується, що ви можете досягти того самого результату передавши контекст як *аргумент* до виклику шаблону. Тому попередній блок коду можна переписати наступним чином:
~~~js
{{> widgetPage myWidget}}
~~~
Для більш глибокого дослідження контекстів даних можете [почитати наш пост](https://www.discovermeteor.com/blog/a-guide-to-meteor-templates-data-contexts/).
<% end %>
### Використання хелпера маршруту з динамічними іменами
Нарешті ми створимо нову кнопку "Discuss", що направить нас на окрему сторінку посту. Знову, ми можемо зробити щось подібне на `<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 btn-default">Discuss</a>
</div>
</template>
~~~
<%= caption "client/views/posts/post_item.html"%>
<%= highlight "6" %>
<%= commit "5-3", "Маршрутизація до окремої сторінки посту." %>
Але зачекайте, як же роутер знає, де отримувати `xyz` частину в `/posts/xyz`? Ми ж не передаємо її ніякими `_id`.
З’ясовується, що цей Iron Router достатньо розумний, щоб взати це самому. Ми повідомляємо роутеру, що треба використовувати маршрут `postPage` і роутер знає, що цей маршрут вимагає `_id` чи щось типу того (в залежності від того, як ми визначили наш `path`) .
Тому роутер буде шукати цей `_id` в найбільш логічному для цього місці: в контексті даних в `{{pathFor 'postPage'}}` хелпері, іншими словами - `this`. І так сталось, що наш `this` відповідає посту, який (о диво!) володіє параметром `_id`.
Альтернативно, ви також можете перестрахуватись і повідомити роутер, де б вам хотілося, щоб він шукав `_id` параметр, шляхом надання другого аргументу для хелпера (наприклад, `{{pathFor 'postPage' someOtherPost}}`). Практичне використання цього паттерну - це отримання ссилки на попередній і наступний пости у списку.
Щоб побачити, чи працює воно правильно, перейдіть до списку постів і кляцніть на одну з 'Discuss' лінків. Ви повинні отриматись щось схоже на:
<%= screenshot "5-2", "Окрема сторінка посту." %>
<% note do %>
### HTML5 pushState
Необхідно зрозуміти одну річ -- зміни цих URL стаються шляхом використання [HTML5 pushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history?redirectlocale=en-US&redirectslug=Web%2FGuide%2FDOM%2FManipulating_the_browser_history).
Рутер бере кліки по лінкам, що є внутрішніми для сайту і запобігає тому, щоб браузер йшов від додатку, замість цього робить необхідні зміни в стані додатку.
Якщо все правильно робить, то сторінка має миттєво змінитися. Фактично, речі інколи змінюються так швидко, що може знадобитись якісь візуальний ефект переходу. Це знаходиться поза межами обговорення данного розділу, але тим не менш є дуже цікавим питанням.
<% end %>
### Пост не знайдено
Давайте не забувати, що маршрутизація працює в обидва напрямки: вона може змінювати URL, коли ми заходимо на сторінку, але також вона може відображати нову сторінку, коли ми змінюємо *сам URL*. Тому потрібно з’ясувати що відбувається, коли хтось вводить *неправильний* URL.
На щастя, Iron Router потурбується за нас через опцію `notFoundTemplate`.
По-перше, встановимо новий шаблон для показу простого 404 повідомлення:
~~~html
<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>
~~~
<%= caption "client/templates/application/not_found.html"%>
Далі, ми просто направимо Iron Router до цього шаблону:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
//...
~~~
<%= caption "lib/router.js"%>
<%= highlight "4" %>
Щоб потестувати нашу нову 404 сторінку ви можете набрати довільний URL: `http://localhost:3000/nothing-here`.
Але зачекайте, що якщо хтось введе URL у вигляді `http://localhost:3000/posts/xyz`, де `xyz` це *не* валідний `_id` посту? Це як і раніше валідний маршрут, але він не веде до якихось даних.
На щастя, Iron Router достатньо розумний, щоб це з’ясувати і нам треба додати спеціальний `dataNotFound` хук в кінці `router.js`:
~~~js
//...
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
~~~
<%= caption "lib/router.js"%>
<%= highlight "4" %>
Це наказує Iron Router показати “not found” сторінку не тільки для невалідних маршрутів, але також для `postPage` маршруту, коли функція `data` повертає "хибні" (наприклад `null`, `false`, `undefined` або пусті) об’єкти.
<%= commit "5-4", "Added not found template." %>
<% note do %>
### Чому "Iron"?
Ви можете роздумувати про історію імені "Iron Router". Згідно автора Iron Router Кріса Мета (Chris Mather), він виходив з того факту, що метеори створенні в основному з заліза.
<% end %>