forked from DiscoverMeteor/DiscoverMeteor_Ru
-
Notifications
You must be signed in to change notification settings - Fork 0
/
12-pagination.md.erb
513 lines (382 loc) · 34.3 KB
/
12-pagination.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
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
---
title: Разбиение на страницы
slug: pagination
date: 0012/01/01
number: 12
contents: Узнаете больше о подписках Meteor, и как мы можем их использовать для контроля данных.|Создадите страницу с подгружаемыми данными по мере прокрутки страницы.|Используете пакет `iron-router-progress` для создания индикатора загрузки в стиле iOS.|Создадите особенную подписку для прямых ссылок на страницу постов.
paragraphs: 67
---
Наше приложение Microscope продвигается ударными темпами, и оно определенно станет хитом когда мы его запустим.
По этой причине стоит задуматься о быстродействии приложения, и о том как поток запросов повлияет на производительность, когда сотни и тысячи пользователей ринутся создавать новые посты.
Ранее мы говорили о том, как коллекция на клиенте должна хранить только часть данных, доступных на сервере. Мы даже создали такие коллекции для уведомлений и комментариев.
Не смотря на это, мы все еще публикуем все наши посты за раз, для всех подключенных пользователей. Когда количество постов станет измеряться тысячами, это станет большой проблемой. Чтобы избежать ее, нам надо начать разбивать посты на отдельные страницы.
### Добавляем больше постов
Для начала давайте создадим больше тестовых постов, чтобы было что разбивать на страницы.
~~~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: 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: now - i * 3600 * 1000,
commentsCount: 0
});
}
}
~~~
<%= caption "server/fixtures.js" %>
<%= highlight "15~24" %>
После запуска команды `meteor reset` вы должны получить примерно такую картину:
<%= screenshot "12-1", "Displaying dummy data. " %>
<%= commit "12-1", "Добавили достаточно постов чтобы их можно было разбивать на страницы." %>
### Бесконечные страницы
В лучших традициях современных веб-приложений мы создадим механизм, который будет подгружать новые посты по мере прокрутки страницы вниз. Для начала мы загрузим, скажем, 10 постов, а внизу высветим ссылку "Загрузить еще". По щелчку на этой ссылке мы подгрузим еще 10 постов, и так *до бесконечности*. Таким образом мы сможем контролировать всю нашу систему разбиения данных на страницы с помощью одного единственного параметра, означающего количество постов, единовременно выводимых на экран.
Нам надо придумать способ сообщить об этом параметре серверу, чтобы тот знал, сколько постов посылать клиенту. У нас уже есть подписка на публикацию `posts` на маршрутизаторе. Мы воспользуемся ей, чтобы дать маршрутизатору возможность управлять нашими страницами.
Самый простой способ передать параметр на сервер будет через URL. Например, в таком формате - `http://localhost:3000/25` - здесь мы передаем значение `25`, про которое сервер догадается, что оно означает количество постов. Дополнительной фишкой будет то, что если пользователь случайно (или намеренно) перезагрузит страницу в браузере, он снова получит то же самое количество постов, что и ранее.
Для этого нам понадобится изменить способ подписки на посты. Точно так же, как и в главе *Комментарии*, мы переместим код подписки с уровня *маршрутизатора* на уровень *маршрута*.
Если вы уже запутались - не пугайтесь. Сейчас все станет яснее, когда мы начнем писать код.
Сначала мы уберем подписку на публикацию `posts` в блоке `Router.configure()`. Удалите `Meteor.subscribe('posts')` и оставьте только подписку на уведомления - `notifications`:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() {
return [Meteor.subscribe('notifications')]
}
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "5" %>
Затем мы добавим параметр `postsLimit` в адрес маршрута. Символ `?` означает, что параметр необязательный. Таким образом наш маршрут будет совпадать не только с `http://localhost:3000/50`, но и с обычным `http://localhost:3000`.
~~~js
Router.map(function() {
//...
this.route('postsList', {
path: '/:postsLimit?'
});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "5" %>
Стоит особенно отметить что маршрут в виде `/:parameter?` будет совпадать со всеми возможными маршрутами. Так как каждый маршрут будет последовательно проверен на совпадение с текущим адресом, стоит уделить особенное внимание объявлению маршрутов в порядке уменьшения конкретности.
Другими словами, более точные маршруты вроде `/posts/:id` должны быть объявлены в начале, а наш маршрут `postsList` стоит переместить ближе к концу файла, так как он будет совпадать практически с любым адресом.
Настало время бросить вызов серьезной проблеме подписки и нахождения верных данных. Определим значение по-умолчанию для случая когда параметр `postsLimit` отсутствует. Пусть это будет "5" - такое значение позволит нам сгенерировать множество страниц для списка постов.
~~~js
Router.map(function() {
//..
this.route('postsList', {
path: '/:postsLimit?',
waitOn: function() {
var postsLimit = parseInt(this.params.postsLimit) || 5;
return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: postsLimit});
}
});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "6~9" %>
Обратите внимание на то, как мы передаем JavaScript объект `{limit: postsLimit}` вместе с именем нашей публикации `posts`. Этот объект послужит параметром `options`, когда сервер вызовет `Posts.find()` чтобы получить порцию постов. Давайте переключимся на код сервера и воплотим это:
~~~js
Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});
Meteor.publish('comments', function(postId) {
return Comments.find({postId: postId});
});
Meteor.publish('notifications', function() {
return Notifications.find({userId: this.userId});
});
~~~
<%= caption "server/publications.js" %>
<%= highlight "1~3" %>
<% note do %>
### Передаем параметры
Наш код публикаций в свою очередь сообщает серверу, что он может доверять всем объектам JavaScript, которые посылает клиент (в нашем случае, `{limit: postsLimit}`). Доверие сервера настолько велико, что он может использовать этот объект в качестве параметра вызова `find()`. Это позволяет пользователям посылать любые опции запроса через консоль браузера.
В нашем случае это вполне безобидно, так как все что пользователь может сделать это поменять посты местами, или изменить значение параметра `limit` (чего мы и добиваемся).
Подобного подхода стоит избегать, когда у объектов есть секретные неопубликованные поля с данными частного характера. Пользователь запросто сможет получить данные из этих полей, слегка подправив содержимое объекта `fields`. По той же причине объект запроса не стоит использовать напрямую как параметр вызова `find()`.
Безопаснее будет передавать отдельные параметры вместо целого объекта - чтобы избежать передачи ненужных полей:
~~~js
Meteor.publish('posts', function(sort, limit) {
return Posts.find({}, {sort: sort, limit: limit});
});
~~~
<% end %>
Теперь, когда мы подписываемся на данные на уровне маршрутизатора, стоит установить контекст данных. Мы слегка изменим наш традиционный подход, заставив функцию `data` вернуть объект JavaScript вместо курсора на данные в Mongo. Это позволит создать именной контекст данных, который мы назовем `posts`.
Традиционно контекст данных был доступен как `this` внутри шаблона, но теперь он будет доступен через `posts`. В остальном, следующий код должен быть уже знаком:
~~~js
Router.map(function() {
this.route('postsList', {
path: '/:postsLimit?',
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})
};
}
});
//..
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "8~13" %>
Теперь когда мы задаем контекст данных на уровне маршрутизатора, можно окончательно избавиться от метода шаблона `posts` в файле `posts_list.js`. И так как мы назвали наш контекст `posts` (точно так же, как и метод), нам даже не нужно трогать шаблон `postsList`.
Файл маршрутизатора `router.js` теперь должен выглядеть так:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() {
return [Meteor.subscribe('notifications')]
}
});
Router.map(function() {
//...
this.route('postsList', {
path: '/:postsLimit?',
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})
};
}
});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "5, 11~21" %>
<%= commit "12-2", "Поправили маршрут postsList чтобы он принимал параметр limit." %>
Давайте опробуем нашу новенькую систему разбития результатов на страницы в действии. Изменяя параметр в URL мы можем задавать количество постов, выводимых на главную страницу. Попробуйте открыть `http://localhost:3000/3`. Вы должны увидеть что-то вроде такого:
<%= screenshot "12-2", "Контролируем количество постов на главной странице. " %>
<% note do %>
### Почему не отдельные страницы?
Почему мы решили подгружать новые посты по мере прокрутки, а не отдельные страницы по 10 постов на каждой? Ведь так, например, делает Google. Все дело в природе реального времени, на которой построен Meteor.
Давайте представим что мы разбиваем коллекцию `Posts` на страницы, как это делает Google с результатами поиска. Мы перешли на вторую страницу, которая высвечивает посты с 10 по 20. Что произойдет, если другой пользователь удалит любой из предыдущих 10 постов?
Так как наше приложение работает в реальном времени, наши данные тут же изменятся. Пост номер 10 превратится в 9 и пропадет со страницы, в то время как пост номер 11 займет его место. В конце-концов результатом окажется то, что на глазах у пользователя посты поменяют места без видимой на то причины.
Даже если бы наш UX дизайн перетерпел бы подобное поведение интерфейса, традиционное разбиение на страницы вовсе нетривиально в техническом воплощении.
Вернемся к предыдущему примеру. Мы опубликовали посты от 10 до 20 из коллекции `Posts`, но как же вы найдете эти посты на клиенте? Вы не можете выбрать посты от 10 до 20, так как их всего 10 в коллекции на клиенте.
Одним из решений было бы опубликовать эти 10 постов на сервере, и затем вызвать `Posts.find()` на клиенте чтобы выбрать для отображения *все* опубликованные посты.
Это сработает только если у вас одна единственная подписка. Но что если у вас появится больше одной подписки на посты, как у нас вскоре и произойдет?
Представим что одна подписка запрашивает посты от 10 до 20, а вторая от 30 до 40. Теперь у вас загружено 20 постов на клиенте, и ни малейшего представления какой из постов принадлежит которой подписке.
Из-за всех этих причин традиционный способ разбиения коллекций на страницы плохо работает вместе с Meteor.
<% end %>
### Создаем Контроллер для Маршрутизатора
Вы могли заметить что мы дважды повторили линию кода `var limit = parseInt(this.params.postsLimit) || 5;`. Вдобавок, использование предопределенных величин в коде, вроде этого числа "5", является плохой практикой. Мир от этого не рухнет, но код стоит немного реорганизовать согласно принципам DRY - Don't Repeat Yourself - "Не повторяйтесь".
Открываем новую сторону маршрутизатора Iron Router - *Контроллер Маршрутизатора* - *Route Controller*. Это удобный способ сгруппировать несколько фишек маршрутизатора в один пакет, который легко используется другими маршрутами. В этот раз мы используем его для одного единственного маршрута, но уже в следующей главе вы увидите, насколько он облегчит нам жизнь.
~~~js
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
limit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.limit()};
},
waitOn: function() {
return Meteor.subscribe('posts', this.findOptions());
},
data: function() {
return {posts: Posts.find({}, this.findOptions())};
}
});
Router.map(function() {
//...
this.route('postsList', {
path: '/:postsLimit?',
controller: PostsListController
});
});
~~~
<%= caption "lib/router.js" %>
Рассмотрим код. Сначала мы создали контроллер наследуя его от `RouteController`. Затем был инициализирован параметр `template`, а также новый параметр - `increment`.
Дальше мы задали функцию `limit` которая вернет значение текущего ограничения на количество постов на странице. Функция `findOptions` возвращает объект с параметрами поиска `options`. Возможно, сейчас она вам покажется лишней, но уже скоро она нам понадобится.
Затем мы определили функции `waitOn` и `data` - теперь они используют нашу функцию `findOptions`.
Напоследок через параметр `controller` мы сообщили маршруту `postsList` использовать наш новый контроллер.
<%= commit "12-3", "Изменили маршрут postsLists чтобы он перенаправлял на контроллер маршрутизатора." %>
### Добавляем ссылку "Загрузить еще"
Разбиение на страницы работает, и наш код выглядит просто отлично. Осталась одна проблема - переход по страницам пока что работает только, если вы вручную будете менять параметр количества постов в адресной строке. Давайте сделаем все чуть проще и приятнее в использовании.
Нам понадобится кнопка в конце списка с постами - "Загрузить еще постов". Каждый раз когда пользователь ее нажмет, количество постов на странице увеличится на 5. Если наш текущий URL `http://localhost:3000/5`, нажатие на кнопке должно изменить его на `http://localhost:3000/10`.
Как и ранее, мы добавим логику разбиения на страницы в маршрут. Помните как мы передали контекст данных именной переменной, вместо того чтобы использовать анонимный курсор? Точно так же не существует правила, по которому функция `data` может передавать одни курсоры. Мы воспользуемся той же техникой чтобы сгенерировать URL для кнопки "Загрузить еще постов".
~~~js
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
limit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.limit()};
},
waitOn: function() {
return Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
return Posts.find({}, this.findOptions());
},
data: function() {
var hasMore = this.posts().fetch().length === this.limit();
var nextPath = this.route.path({postsLimit: this.limit() + this.increment});
return {
posts: this.posts(),
nextPath: hasMore ? nextPath : null
};
}
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "16~23" %>
Давайте внимательнее взглянем на это волшебство в маршрутизаторе. Как вы помните, маршрут `postsList` (который наследуется от контроллера `PostsListController`, над которым мы как раз работаем) принимает параметр `postsLimit`.
Когда мы передаем объект `{postsLimit: this.limit() + this.increment}` вызову функции `this.route.path()`, мы говорим маршруту `postsList` создать новый путь, используя этот объект JavaScript как контекст данных.
Другими словами, это то же самое что и использование метода Handlebars `{{pathFor 'postsList'}}`, за исключением того, что мы заменяем непосредственное `this` на наш собственный контекст данных.
Мы берем этот новый путь и добавляем его в контекст данных шаблона, но *только* если еще остались посты. Как это работает на практике?
Вызов `this.limit()` возвращает количество постов, отображаемых на странице. Это будет либо значение в текущем URL, либо значение по-умолчанию (5) - если в URL нет этого параметра.
С другой стороны `this.posts` ссылается на текущий курсор базы данных, и `this.posts.count()` сосчитает количество постов в этом курсоре.
Теперь, если мы запросим `n` количество постов и получим ровно столько постов, можно оставить кнопку "Загрузить еще постов" на странице. Но если мы запросим `n` постов, а получим *меньше* чем `n` в ответ, значит посты закончились и кнопку "Загрузить еще" можно спрятать.
Однако, есть еще один момент. Что если количество постов в базе данных *равно* `n`? В этом случае клиент запросит `n` постов, получит в ответ ровно `n`, и кнопка "Загрузить еще" останется на виду.
К сожалению здесь нет простого решения, и на данный момент мы оставим все как есть.
Все что осталось это добавить сама ссылка "Загрузить еще" после списка постов на странице, и добавить немного логики чтобы эта ссылка отображалась только, если еще остались посты:
~~~html
<template name="postsList">
<div class="posts">
{{#each posts}}
{{> postItem}}
{{/each}}
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{/if}}
</div>
</template>
~~~
<%= caption "client/views/posts/posts_list.html" %>
<%= highlight "7~10" %>
Теперь список постов должен выглядеть так:
<%= screenshot "12-3", "Кнопка “Загрузить еще” на странице" %>
<%= commit "12-4", "Добавили nextPath() в контроллер и используем его для разбиения постов на страницы." %>
### Улучшаем индикатор прогресса
Разбиение на страницы работает, но одна проблема сильно портит впечатление. Каждый раз когда кто-то нажимает ссылку "Загрузить еще" и маршрутизатор запрашивает больше постов, приложение переходит на шаблон `loading` пока данные запрашиваются. В результате страница перепрыгивает на самый верх, и приходится прокручивать вниз чтобы продолжить чтение постов.
Было бы гораздо лучше, если бы мы оставались на той же странице пока грузятся данные, одновременно показывая какой-либо индикатор что данные действительно грузятся. Именно для этих целей и существует пакет `iron-router-progress`.
Этот пакет позволит нам добавить индикатор загрузки наверху экрана, в стиле браузера Safari на iOS или сайтов вроде Medium и YouTube. Все что нужно сделать это установить сам пакет:
~~~bash
meteor add mrt:iron-router-progress
~~~
<%= caption "Командная консоль bash" %>
Благодаря волшебству умных пакетов `smart packages`, индикатор прогресса тут же заработает в нашем приложении. Он будет активирован для каждого маршрута, и автоматически спрятан как только все данные для маршрута будут загружены.
Из-за того что мы хотим показать список постов даже если мы переходим между страницами, мы не хотим, чтобы триггер загрузка запускался для наших страниц `PostListController` -- мы можем это добиться если не будем ждать подписок:
~~~js
PostsListController = RouteController.extend({
// ...
onBeforeAction: function() {
this.postsSub = Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
return Posts.find({}, this.findOptions());
},
data: function() {
var hasMore = this.posts().count() === this.limit();
return {
posts: this.posts(),
ready: this.postsSub.ready,
nextPath: hasMore ? this.nextPath() : null
};
}
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "3~5,13" %>
Вместо подписки в `waitOn` блоке, мы подписываемся в `onBeforeAction`, держа ссылку на подписку (subscription handle) в `this.postsSub`, таким образом мы сообщаем о "готовности" шаблону.
Затем, в шаблоне, мы можем показать бегунок загрузки в конце списка постов, когда мы подгружаем новый набор постов. Для этого мы проверяем состояние "готовности", которые мы передали ранее:
~~~html
<template name="postsList">
<div class="posts">
{{#each postsWithRank}}
{{> postItem}}
{{/each}}
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{else}}
{{#unless ready}}
{{> spinner}}
{{/unless}}
{{/if}}
</div>
</template>
~~~
<%= caption "client/views/posts/posts_list.html" %>
<%= highlight "10~12" %>
Сделаем еще одну небольшую микро-поправку. Отключим `iron-router-progress` для маршрута `postSubmit`, так как ему не нужно ждать никаких данных (в конце-концов это просто пустая форма на станице):
~~~js
Router.map(function() {
//...
this.route('postSubmit', {
path: '/submit',
disableProgress: true
});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "7" %>
<%= commit "12-5", "Используем пакет iron-router-progress для индикатора прогресса загрузки." %>
### Доступ к любому посту
На данный момент по-умолчанию мы загружаем пять самых новых постов. Но что произойдет если кто-то откроет страницу одного из постов?
<%= screenshot "12-4", "Пустой шаблон." %>
Если вы попробуете открыть один из постов, приложение нарисует шаблон с пустым постом. Здесь есть определенный смысл: мы сообщили маршрутизатору подписаться на публикации `posts` когда загружается маршрут `postsList`. Но мы не сообщили ему что делать с маршрутом `postPage`.
Пока что мы умеем подписываться только на лист из `n` последних постов. Как запросить у сервера один конкретный пост? Оказывается, здесь есть один секрет: у коллекции может быть одновременно несколько публикаций!
Чтобы вернуть наши потерявшиеся посты, мы добавим новую публикацию `singlePost`, которая будет публиковать один пост согласно запрошенному параметру `_id`.
~~~js
Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});
Meteor.publish('singlePost', function(id) {
return id && Posts.find(id);
});
~~~
<%= caption "server/publications.js" %>
<%= highlight "5~7" %>
Теперь мы можем подписаться на посты на клиенте. У нас уже есть подписка на публикацию `comments` в функции `waitOn` маршрута `postPage`. Мы просто добавим здесь еще одну подписку на `singlePost`. Не забудьте добавить подписку в маршрут `postEdit`, ведь ему потребуются те же самые данные:
~~~js
Router.map(function() {
//...
this.route('postPage', {
path: '/posts/:_id',
waitOn: function() {
return [
Meteor.subscribe('singlePost', this.params._id),
Meteor.subscribe('comments', this.params._id)
];
},
data: function() { return Posts.findOne(this.params._id); }
});
this.route('postEdit', {
path: '/posts/:_id/edit',
waitOn: function() {
return Meteor.subscribe('singlePost', this.params._id);
},
data: function() { return Posts.findOne(this.params._id); }
});
/...
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "7~12,18~20" %>
<%= commit "12-6","Подписываемся на посты по-отдельности, чтобы можно было загружать индивидуальные страницы с постами." %>
Теперь, когда у нас работает разбиение гигантского количества постов на страницы, у приложения не будет проблем с обилием данных, и пользователи смогут запостить еще больше ссылок. Было бы здорово как-то сортировать все эти ссылки и дать пользователям возможность голосовать за лучшие из них. Об этом и будет следующая глава.