-
Notifications
You must be signed in to change notification settings - Fork 39
/
05-routing.md.erb
335 lines (222 loc) · 28 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
---
title: Маршрутизация (Routing)
slug: routing
date: 0005/01/01
number: 5
paragraphs: 72
contents: Изучите маршрутизацию в Meteor.|Создадите страницы обсуждения постов с уникальными URL-ми.|Изучите как делать ссылки с такими URL-ми правильно.
---
Теперь, когда мы имеем список постов (которые в конце концов отправят пользователи), нам нужна отдельная страница для каждого поста, где наши пользователи смогут обсудить его.
Мы бы хотели, чтобы эти страницы были доступны через *постоянную ссылку* (*permalink*) -- URL вида `http://myapp.com/posts/xyz` (где `xyz` -- идентификатор `_id` в MongoDB), которая уникальна для каждого поста.
Для этого нам понадобится какая-нибудь *маршрутизация (routing)*, чтобы адресная строка в браузере правильно соответствовала отображаемому контенту.
### Добавление пакета Iron Router
[Iron Router](https://github.com/EventedMind/iron-router) -- это пакет маршрутизации, который был задуман специально для Meteor приложений.
Он не только помогает в маршрутизации (настройке путей), но может и позаботиться о фильтрации (сопоставлении действий и некоторых путей), и даже управлять подписками (контролировать, какой путь имеет доступ к этим данным). (Примечание: Iron Router частично был разработан Tom Coleman -- соавтором книги *Discover Meteor*.)
Во-первых, установим пакет из Atmosphere:
~~~bash
$ meteor add iron:router
~~~
<%= caption "Terminal" %>
Эта команда скачает и установит пакет Iron Router для использования вашим приложением. Заметьте, что вам иногда нужно будет перезапустить Meteor-приложение (с помощью `ctrl+c` убить процесс, затем `meteor` чтобы запустить его снова) перед тем, как пакет может быть использован.
<% note do %>
### Термины маршрутизации
В этой главе мы коснёмся большинства особенностей маршрутизации. Если вы уже имеете некоторый опыт с фреймворками, такими как Rails, вам будет знакомо большинство из этих концепций. На случай, если это не так, здесь приводится краткий словарик для быстрого ознакомления:
- **Маршруты (Routes)**: Основной строительный блок маршрутизации. Это в основном набор инструкций, которые говорят куда идти и что делать при встрече с данным URL-ом.
- **Пути (Paths)**: URL внутри вашего приложения. Он может быть статичным (`/terms_of_service`) или динамичным (`/posts/xyz`), и даже включать параметры запроса (`/search?keyword=meteor`).
- **Сегменты (Segments)**: Различные части пути, разделенные прямым слэшем (`/`).
- **Обработчики (Hooks)**: Действия, которые вы захотите произвести перед, после, или даже во время процесса маршрутизации. Типичным примером может быть проверка прав пользователя перед отображением страницы.
- **Фильтры (Filters)**: Простые обработчики, которые вы глобально определяете для одного или нескольких маршрутов.
- **Шаблоны маршрутов (Route Templates)**: Каждому маршруту нужно указать шаблон. Если вы его не укажете, маршрутизатор будет искать шаблон с таким же именем, как у маршрута по умолчанию.
- **Макеты (Layouts)**: Вы можете думать о макетах, как о цифровых фоторамках. Они содержат в себе весь html-код, который оборачивается текущим шаблоном, и будут оставаться ими же, даже если шаблон изменится.
- **Контроллеры (Controllers)**: Иногда вы будете понимать, что многие ваши маршруты повторно используют одни и те же параметры. Вместо дублирования кода вы можете наследовать такие маршруты от одного *маршрутного контроллера (routing controller)*, который будет содержать всю логику маршрутизации.
Для более подробной информации об Iron Router смотрите [полную документацию на GitHub](https://github.com/EventedMind/iron-router).
<% end %>
### Маршрутизация: сопоставление URL-ов и шаблонов
До сих пор мы делали сборку нашего макета, используя жёстко заданные вставки шаблонов (такие как `{{>postsList}}`). Таким образом, контент нашего приложения может меняться, но основная структура страницы всегда одинакова: заголовок со списком постов ниже.
Iron Router позволяет нам уйти от этой замшелости взятием на себя отрисовки содержимого html-тега `<body>`. Поэтому мы не будем определять содержимое `<body>` сами, как мы делали это с обычной html-страницей. Вместо этого мы укажем маршрут к специальному макету, который содержит метод шаблона `{{> yield}}`.
Метод `{{> yield}}` определит специальную динамическую зону, которая автоматически будет отрисовывать шаблон, соответствующий текущему маршруту (договоримся, что с этого места такие специальные шаблоны мы будем называть «маршрутными шаблонами» -- «route templates»):
<%= diagram "router-diagram", "Layouts and templates.", "pull-center" %>
Начнем с создания нашего макета и добавления метода `{{> yield}}`. Прежде всего удалим наш html-тег `<body>` из `main.html` и переместим его контент в шаблон `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 `/`, поэтому он просто выдаёт пустой шаблон.
Для начала мы можем вернуть наше старое поведение, сделав соответствие корневого URL `/` шаблону `postsList`. В корне нашего проекта создадим каталог `/lib`, а внутри него - файл `router.js`:
~~~js
Router.configure({
layoutTemplate: 'layout'
});
Router.map(function() {
this.route('postsList', {path: '/'});
});
~~~
<%= caption "lib/router.js"%>
Мы сделали две важные вещи. Во-первых, указали маршрутизатору, какой макет использовать по умолчанию. Во-вторых, определили новый маршрут `postsList`, соответствующий пути `/`.
<% note do %>
### Папка `/lib`
Всё, что находится в папке `/lib`, гарантированно загрузится первым - прежде, чем всё остальное в вашем приложении (за возможным исключением умных пакетов). Это делает папку `/lib` отличным местом для любого вспомогательного кода, который должен быть доступен всё время.
Небольшое предупреждение: заметим, что поскольку папки `/lib` нет внутри папок `/client` или `/server` это означает, что её контент будет доступен для обоих окружений.
<% end %>
### Именованные маршруты
Проясним здесь некоторые моменты. Мы назвали наш маршрут `postsList`, но мы также дали имя `postsList` и *шаблону*. Что же происходит в таком случае?
По умолчанию Iron Router ищет шаблон с таким же именем как и маршрут. По факту он будет даже искать *путь (path)*, основанный на имени маршрута, -- это означает, что мы, не определив свой путь (который мы указываем опцией `path` в нашем описании маршрута), сделали наш шаблон по умолчанию доступным по URL `/postsList`.
Вам может быть интересно, почему вообще маршрутам нужно давать имена. Именованные маршруты позволяют использовать несколько особенностей Iron Router, чтобы облегчить создание ссылок внутри приложения. Один из самых полезных методов в Handlebars -- это `{{pathFor}}`, который возвращает компонент URL путь любого маршрута.
Мы хотим, чтобы наша ссылка на главную страницу указывала на список постов. Вместо того чтобы определить статический URL `/`, мы можем использовать метод в Handlebars. Конечный результат будет такой же, но это даст нам больше гибкости, так как этот метод будет всегда выводить правильный 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." %>
### В ожидании данных
Если провести развертывание текущей версии приложения (или запустите экземпляр, используя ссылку выше), можно заметить, что при загрузке страницы список постов некоторое время отображается пустым. Это происходит, потому что пока подписчик `posts` не закончит забирать данные с сервера, постов для отображения на странице попросту нет.
Было бы намного лучше в таких случаях обеспечить пользователю визуальную обратную связь с происходящим.
К счастью, Iron Router даёт простой способ сделать это -- мы можем воспользоваться подписчиком `waitOn`:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.map(function() {
this.route('postsList', {path: '/'});
});
Router.onBeforeAction('loading');
~~~
<%= caption "lib/router.js" %>
<%= highlight "3,4, 11" %>
Разберемся с этим кодом. Во-первых, мы изменили блок `Router.configure()`, обеспечив маршрутизатор именем загрузочного шаблона (который мы скоро создадим) для перенаправления на него, пока наше приложение ожидает данные.
Во-вторых, мы добавили функцию `waitOn`, которая возвращает нашу `posts`-подписку. Это означает, что подписка `posts` гарантированно подгрузится перед отправкой пользователя по запрошенному маршруту.
Заметим, что поскольку мы определяем нашу функцию `waitOn` глобально на уровне маршрутизации, она сработает, когда пользователь впервые зайдёт в ваше приложение. После этого данные уже будут загружены в память браузера, и маршрутизатору не нужно будет ждать их снова.
Так как мы сейчас позволили маршрутизатору обрабатывать нашу подписку, её можно безопасно удалить из `main.js` (который теперь будет пустым).
Как правило, это хорошая идея -- ожидать ваши подписки -- не только для взаимодействия с пользователем, но и потому что так вы можете быть уверены, что данные всегда будут доступны в шаблоне. Это устраняет необходимость иметь дело с шаблонами, начинающими отрисовку перед тем, как их данные будут доступны.
Также мы добавим фильтр `onBeforeAction`, чтобы запустить встроенный `loading` триггер Iron Router'а и показать шаблон загрузки, пока мы ждем.
Финальным кусочком головоломки будет сам шаблон процесса загрузки. Для создания прекрасного анимированного индикатора загрузки мы воспользуемся пакетом `sacha:spin`. Добавим его командой `meteor add sacha:spin` и создадим шаблон `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 %>
### Первый взгляд на реактивность
Реактивность -- это базовая часть Meteor, и хотя мы еще по настоящему не касались её, наш шаблон загрузки даёт первый взгляд на эту концепцию.
Перенаправление на шаблон загрузки, пока данные загружаются, это, конечно же, хорошо. Но как маршрутизатор узнал, что нужно перенаправить пользователя *обратно* на правильную страницу, как только данные были получены?
На данный момент просто скажем, что здесь однозначно работает реактивность. Но не беспокойтесь, вы скоро изучите её.
<% end %>
### Маршрутизация к указанному посту
Теперь, когда мы увидели как указывать маршрут к шаблону `postsList`, давайте построим маршрут для отображения подробного вида одного поста.
Есть только одна загвоздка: мы не можем определить свой отдельный маршрут для каждого поста -- ведь их будет огромное множество. Вместо этого нам стоит создать один *динамический* маршрут, и заставить его показывать любой пост.
Для начала мы создадим новый шаблон, который просто отрисовывает такой же шаблон поста, что мы использовали ранее в списке постов.
~~~html
<template name="postPage">
{{> postItem}}
</template>
~~~
<%= caption "client/views/posts/post_page.html" %>
Мы добавим больше элементов (таких, как комментарии) к этому шаблону позже, но на данный момент он будет служить простой оболочкой для нашей вставки `{{> postItem}}`.
Теперь создадим другой именованный маршрут -- на этот раз отображение URL-ов вида `/posts/<ID>` к шаблону `postPage`:
~~~js
Router.map(function() {
this.route('postsList', {path: '/'});
this.route('postPage', {
path: '/posts/:_id'
});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "4~6" %>
Специальный синтаксис `:_id` сообщает маршрутизатору две вещи: 1) совпадает любой маршрут формы `/posts/xyz/`, где “xyz” может принимать любое значение; 2) положить всё что найдено на месте “xyz” внутрь свойства `_id` массива `params` маршрутизатора.
Обратите внимание, что мы используем `_id` здесь только ради удобства. Маршрутизатор не имеет возможности узнать, отправляем ли мы актуальный `_id` или просто некоторую случайную строку символов.
Теперь мы имеем маршрут к правильному шаблону, но нам чего-то не хватает. Маршрутизатор знает `_id` поста, который мы бы хотели показать, но шаблон не имеет о нём никакого представления.
К счастью, маршрутизатор имеет неплохое встроенное решение. Он позволяет указать **контекст данных (data context)** для шаблона. Вы можете думать о данных контекста как о начинке в торте, который сделан из шаблонов и макетов. Проще говоря, это то, чем вы заполняете шаблон:
<%= diagram "router-diagram-2", "The data context.", "pull-center" %>
В нашем случае мы получим правильный контекст данных, отыскав наш пост по `_id`, который мы получили из URL:
~~~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` возвращает единственный пост, совпавший с запросом, и ему можно передать только один аргумент `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}}
~~~
<% 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 btn-default">Discuss</a>
</div>
</template>
~~~
<%= caption "client/views/posts/post_item.html"%>
<%= highlight "6" %>
<%= commit "5-3", "Routing to a single post page." %>
Но подождите, как именно маршрутизатор узнает, где взять часть `xyz` в `/posts/xyz`? В конце концов, мы не передаем ему любой `_id`.
Оказывается, Iron Router достаточно умён, чтобы понять это самому. Мы говорим маршрутизатору использовать маршрут `postPage`, и маршрутизатор узнаёт, что этот маршрут требует некий параметр `_id` (ведь он прописан нами в определении маршрута `path`).
Таким образом, маршрутизатор ищет этот `_id` в наиболее логичном из возможных мест: в контексте данных метода `{{pathFor 'postPage'}}`, другими словами в `this`. Выходит наш `this` будет соответствовать посту, который (сюрприз!) действительно обладает свойством `_id`.
В качестве альтернативы вы также можете явно указать маршрутизатору, где бы вы хотели найти свойство `_id`, отправив второй аргумент методу шаблона (т.е. `{{pathFor 'postPage' someOtherPost}}`). На практике этот паттерн можно использовать, например, для получения предыдущих или следующих постов в списке.
Чтобы убедиться, что всё работает правильно, перейдите в браузере к списку постов и кликните по какой-нибудь ссылке 'Discuss'. Вы должны увидеть что-то подобное этому:
<%= screenshot "5-2", "A single post page." %>
<% 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 %>