-
Notifications
You must be signed in to change notification settings - Fork 30
/
12-pagination.md.erb
541 lines (405 loc) · 23.5 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
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
---
title: Paginación
slug: pagination
date: 0012/01/01
number: 12
points: 10
photoUrl: http://www.flickr.com/photos/ikewinski/8625379401/
photoAuthor: Mike Lewinski
contents: Aprenderemos más sobre las suscripciones y cómo usarlas para controlar los datos.|Implementaremos la paginación de estilo infinito|Usaremos el paquete iron-router-progress para implementar una bonita barra de progreso al estilo iOS.|Crearemos una suscripción especial para tratar con los enlaces a las páginas de posts.
paragraphs: 67
---
Nuestra aplicación va tomando forma y podemos esperar un gran éxito cuando todo el mundo la conozca.
Así que quizás debamos pensar un poco sobre cómo afectará al rendimiento el gran número de nuevos posts que vamos a recibir.
Hemos visto antes cómo una colección en el cliente pude contener un subconjunto de los datos en el servidor y lo hemos usado para nuestras notificaciones y comentarios.
Ahora pensemos en que todavía estamos publicando todos nuestros posts de una sola vez a todos los usuarios conectados. Si se publicaran miles de enlaces, esto sería un problema. Para solucionarlo tenemos que paginar nuestros posts.
### Añadiendo unos cuantos posts
En primer lugar, vamos a cargar los suficientes posts para que la paginación tenga sentido:
~~~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: new Date(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: new Date(now - i * 3600 * 1000),
commentsCount: 0
});
}
}
~~~
<%= caption "server/fixtures.js" %>
<%= highlight "15~24" %>
Después de ejecutar `meteor reset` e iniciar la aplicación de nuevo, deberíamos ver algo como esto:
<%= screenshot "12-1", "Mostrando un montón de datos." %>
<%= commit "12-1", "Añadidos suficientes posts para hacer necesaria la paginación." %>
### Paginación infinita
Vamos a implementar una paginación de estilo "infinito". Lo que queremos decir con esto es que primero mostramos, por ejemplo, 10 posts, con un enlace de "cargar más" en la parte inferior. Al hacer clic en este enlace se cargarán 10 más, y así _hasta el infinito y más allá_. Esto significa que podemos controlar todo nuestro sistema de paginación con un solo parámetro que representa el número de posts que mostraremos en pantalla.
Vamos a necesitar entonces una forma de pasar este parámetro al servidor para que sepa la cantidad de mensajes que debe enviar al cliente. Se da la circunstancia de que ya estamos suscritos a la publicación `posts` en el router, así que vamos a aprovecharlo y a dejar que el router maneje también la paginación.
La forma más fácil de configurar esto es hacer que el parámetro límite forme parte de la ruta, quedando las URL de la forma `http://localhost:3000/25`. Una ventaja añadida de utilizar la URL en vez de otros métodos es que si estamos viendo 25 posts y resulta que se recarga la página por error, todavía seguiremos viendo 25 posts.
Para hacer esto correctamente, tenemos que cambiar la forma en que nos suscribimos a los posts. Al igual que hicimos en el capítulo _Comentarios_, tendremos que mover nuestro código de suscripción desde el nivel de router al nivel de ruta.
Parece demasiado para hacerlo todo de una sola vez, pero se verá más claro escribiendo el código.
En primer lugar, vamos a dejar de suscribirnos a la publicación `posts` en el bloque `Router.configure()`. Simplemente elimina `Meteor.subscribe('posts')`, dejando solo la suscripción `notifications`:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('notifications')]
}
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "6" %>
A continuación, añadiremos el parámetro `postsLimit` al path de la ruta. Si añadimos un `?` después del parámetro, lo hacemos opcional. De forma que nuestra ruta no solo coincidirá con `http://localhost:3000/50`, sino también con `http://localhost:3000`.
~~~js
//...
Router.route('/:postsLimit?', {
name: 'postsList',
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "3" %>
Es importante señalar que un path de la forma `/:parameter?` coincide con todos los path posibles. Dado que cada ruta se analiza en orden secuencial para comprobar si coincide con la ruta actual, tenemos que asegurarnos que organizamos bien nuestras rutas con el fin de disminuir la especificidad.
En otras palabras, las rutas más específicas como `/posts/:_id` deben ir primero, y nuestra ruta `postsList` debería ir **en la parte inferior** del grupo de rutas para que todo coincida correctamente.
Es el momento de abordar el difícil problema de suscribirse y encontrar los datos correctos. Tenemos que lidiar con el caso en el que el parámetro `postsLimit` no está presente, por lo que vamos a asignarle un valor predeterminado. Usaremos "5", que nos dará suficiente espacio para jugar con la paginación.
~~~js
//...
Router.route('/:postsLimit?', {
name: 'postsList',
waitOn: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
}
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "5~8" %>
Ya te habrás dado cuenta de que estamos pasando un objeto JavaScript ({sort: {submitted: -1}, limit: postsLimit}) junto con el nombre de nuestra publicación `posts`. Este objeto servirá como parámetro `options` en la llamada a `Posts.find()` en el lado del servidor. Vamos a cambiar nuestro código en el servidor para implementarlo:
~~~js
Meteor.publish('posts', function(options) {
check(options, {
sort: Object,
limit: Number
});
return Posts.find({}, options);
});
Meteor.publish('comments', function(postId) {
check(postId, String);
return Comments.find({postId: postId});
});
Meteor.publish('notifications', function() {
return Notifications.find({userId: this.userId});
});
~~~
<%= caption "server/publications.js" %>
<%= highlight "1~7" %>
<% note do %>
### Paso de parámetros
Nuestro código de publicaciones está diciendo al servidor que puede confiar en cualquier objeto JavaScript enviado por el cliente (en nuestro caso, `{limit: postsLimit})` para servir como opciones para `find()`. Esto hace posible que los usuarios envíen cualquier opción a través de la consola del navegador.
En nuestro caso, esto es relativamente inofensivo, ya que todo lo que un usuario podría hacer es reordenar los mensajes de manera diferente, o cambiar el límite (que es lo que queremos hacer). ¡De todas formas una aplicación del mundo real debería probablemente limitar el límite!
Afortunadamente, usando `check()` sabemos que los usuarios no podrán inyectar opciones adicionales (como la opción `fields`, que en algunos casos podría exponer datos privados en los documentos).
De todas formas, un patrón más seguro para asegurarnos el control de nuestros datos podría ser pasar los parámetros de forma individual en lugar de todo el objeto:
~~~js
Meteor.publish('posts', function(sort, limit) {
return Posts.find({}, {sort: sort, limit: limit});
});
~~~
<% end %>
Ahora que nos suscribimos a nivel de ruta, tiene sentido establecer el contexto de datos en ese mismo lugar. Vamos a desviarnos un poco de nuestro patrón anterior y hacer que la función `data` devuelva un objeto JavaScript en lugar de simplemente devolver un cursor. Esto nos permite crear un contexto de datos con nombre que llamaremos `posts`.
Lo que significa es que en lugar de disponer implícitamente de los datos en `this` dentro de la plantilla, estará disponible también como `posts`. Aparte de este pequeño elemento, el código debe sernos familiar:
~~~js
//...
Router.route('/:postsLimit?', {
name: 'postsList',
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" %>
Ahora que hemos establecido el contexto de datos a nivel de router podemos deshacernos del ayudante de plantilla `posts` del archivo `posts_list.js` borrando el contenido de este archivo.
Y como hemos llamado `posts` al contexto de datos (igual que en el ayudante), ¡ni siquiera necesitamos tocar la plantilla `postsList`!
Recapitulemos. Así es como ha quedado nuestro `router.js`:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('notifications')]
}
});
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return Meteor.subscribe('comments', this.params._id);
},
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/posts/:_id/edit', {
name: 'postEdit',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
Router.route('/:postsLimit?', {
name: 'postsList',
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})
};
}
});
var requireLogin = function() {
if (! Meteor.user()) {
if (Meteor.loggingIn()) {
this.render(this.loadingTemplate);
} else {
this.render('accessDenied');
}
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "6,25~37" %>
<%= commit "12-2", "Aumentada la ruta `postsList` para que tenga un límite." %>
Vamos a probar nuestro nuevo sistema de paginación. Ahora podemos mostrar un número arbitrario de posts en la página principal simplemente cambiando el parámetro en la URL. Por ejemplo, intenta acceder a `http://localhost:3000/3`. Deberías ver algo como esto:
<%= screenshot "12-2", "Controlando el número de posts en la página principal. " %>
<% note do %>
### ¿Y porqué no usamos páginas?
¿Por qué usamos el enfoque "paginación infinita" en lugar de mostrar páginas sucesivas con 10 posts cada uno, como hace Google para en sus resultados de búsqueda? En realidad se debe al paradigma de tiempo real que utiliza Meteor.
Imaginemos que paginamos nuestra colección `Posts` utilizando el patrón de resultados Google, y que estamos en la página 2, que muestra los mensajes de 10 a 20. ¿Qué pasa si otro usuario elimina una de las 10 entradas anteriores?
Como nuestra aplicación es en tiempo real, nuestra base de datos cambiaría. El post 10 se convertiría en el 9, y desaparecería de nuestra vista y ahora veríamos el 11. El resultado sería que el usuario vería aparecer y desaparecer posts sin razón aparente.
Incluso si toleramos esta peculiaridad, la paginación tradicional también es difícil de implementar por razones técnicas.
Volvamos a nuestro ejemplo anterior. Estamos publicando los posts 10-20 de la colección `Posts`, pero ¿cómo los encontramos en el cliente? No se pueden seleccionar los mensajes 10 a 20 porque solo hay diez posts en total en el conjunto de datos del lado del cliente.
Una solución sería publicar esos 10 mensajes en el servidor y, a continuación, hacer un `Posts.find()` en el lado del cliente para recoger todos los posts publicados.
Esto funciona si solo tienes una suscripción. Pero, ¿y si tenemos más de una?
Digamos que una suscripción pide los posts 10 a 20, y otra 30 a 40. Ahora tenemos 20 mensajes cargados del lado del cliente en total, y no hay forma de saber cuáles pertenecen a cada suscripción.
Por todas estas razones, la paginación tradicional simplemente no tiene mucho sentido cuando se trabaja con Meteor.
<% end %>
### Creando un controlador de rutas
Te habrás dado cuenta de que repetimos dos veces la línea `var limit = parseInt(this.params.postsLimit) || 5;`. Además, codificar el número "5" no es lo ideal. No te preocupes, no es el fin del mundo, pero como siempre es mejor seguir el principio DRY (Don't Repeat Yourself), vamos a ver cómo podemos refactorizar un poco las cosas.
Introduciremos un nuevo aspecto de Iron Router, los _controladores de ruta_. Un controlador de ruta es simplemente una forma de agrupar en un paquete reutilizable, características de enrutamiento que puede heredar cualquier ruta. Ahora solo lo utilizaremos para una sola ruta, pero en el próximo capítulo veremos que esta característica es muy útil.
~~~js
//...
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.postsLimit()};
},
waitOn: function() {
return Meteor.subscribe('posts', this.findOptions());
},
data: function() {
return {posts: Posts.find({}, this.findOptions())};
}
});
//...
Router.route('/:postsLimit?', {
name: 'postsList'
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "3~18, 25" %>
Vamos a verlo paso a paso. En primer lugar, creamos nuestro controlador extendiendo `RouteController`. A continuación, establecemos la propiedad `template` tal y como hicimos antes, y luego una nueva propiedad `increment`.
A continuación, definimos una nueva función `postsLimit` que devolverá el límite actual, y una función `findOptions` que devolverá un objeto con las opciones. Esto puede parecer un paso extra, pero vamos a hacer uso de él en el futuro.
A continuación, definimos nuestras funciones `waitOn` y de `data` igual que antes, excepto que ahora usan la nueva función `findOptions`.
Como nuestro controlador se llama `PostsListController` y nuestra ruta se llama `postsList`, Iron Router usará el controlador automáticamente. Así que sólo necesitamos eliminar `waitOn` y `data` de la definicíon de la ruta (ya que es el controlador quien los gestiona ahora). Si necesitáramos utilizar un controlador con un nombre diferente, lo podemos hacer usando la opción `controller` (veremos un ejemplo de esto en el siguiente capítulo).
<%= commit "12-3", "postLists refactorizado en un controlador de rutas" %>
### Añadiendo un botón "Load more"
Ya tenemos funcionando la paginación. Solo hay un problema: no hay manera de utilizarla realmente si no escribimos en la URL manualmente. Desde luego, no parece una gran experiencia de usuario, así que vamos a arreglarlo.
Lo que queremos hacer es bastante simple. Vamos a añadir un botón "Load more" en la parte inferior de nuestra lista de posts, que incrementará en 5 el número de posts que se muestran. Así que si estamos en la URL `http://localhost:3000/5`, haciendo clic en "Load more" debería llevarnos a `http://localhost:3000/10`. Si has llegado hasta aquí, ¡confiamos en que podrás manejar un poco de aritmética!
Al igual que antes, vamos a añadir nuestra lógica de paginación en la ruta. ¿Recuerdas que nombramos explícitamente el contexto de datos en lugar de usar un cursor anónimo? Bueno, pues no hay ninguna regla que diga que la función `data` solo puede pasar cursores, de modo que usaremos la misma técnica para generar la URL del botón "Load more".
~~~js
//...
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.postsLimit()};
},
waitOn: function() {
return Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
return Posts.find({}, this.findOptions());
},
data: function() {
var hasMore = this.posts().count() === this.postsLimit();
var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
return {
posts: this.posts(),
nextPath: hasMore ? nextPath : null
};
}
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "15~25" %>
Echemos un vistazo en profundidad a este pequeño truco de magia que hemos puesto en el router. Recuerda que la ruta `postsList` (que se hereda del controlador `PostsListController` con el que estamos trabajando) toma un parámetro `postsLimit`.
Cuando alimentamos `{postsLimit: this.postsLimit() + this.increment}` a `this.route.path()`, le estamos diciendo a la ruta `postsList` que construya su propio path utilizando ese objeto JavaScript como contexto de datos.
En otras palabras, es exactamente lo mismo que usar el ayudante Spacebars `{{pathFor 'postsList'}}`, salvo que reemplazamos el `this` implícito por nuestro propio contexto de datos a medida.
Estamos cogiendo ese path y añadiéndolo al contexto de datos de nuestra plantilla, pero _sólo_ si hay más posts que mostrar. La forma de hacerlo es un poco complicada.
Sabemos que `this.limit()` devuelve el número actual de posts que nos gustaría mostrar, que puede ser el valor de la URL actual, o el valor por defecto (5) si la URL no contiene ningún parámetro.
Por otro lado, `this.posts` se refiere al cursor actual, de modo que `this.posts.count()` es el número de mensajes que hay en el cursor.
Así que lo que estamos diciendo es que si pedimos `n` posts y obtenemos `n`, seguiremos mostrando el botón "Load more". Pero si pedimos `n` y tenemos _menos_ de `n`, significa que hemos llegado al límite y deberíamos dejar de mostrarlo.
Con todo esto, nuestro sistema falla en un caso: cuando el número de posts en nuestra base de datos es _exactamente_ `n`. Si eso ocurre, el cliente pedirá `n` posts y obtendrá `n` por lo que seguirá mostrando el botón "Load more", sin darse cuenta de que ya no quedan más elementos.
Lamentablemente, no hay soluciones sencillas para este problema, así que por ahora vamos a tener que conformarnos con esto.
Todo lo que queda por hacer es añadir el botón "Load more" en la parte inferior de nuestra lista de posts, asegurándonos de mostrarlo solo si tenemos más posts que cargar:
~~~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/templates/posts/posts_list.html" %>
<%= highlight "7~9" %>
Así es como se debería ver la lista ahora:
<%= screenshot "12-3", "El botón “Load more”." %>
<%= commit "12-4", "Añadido nextPath() al controlador para desplazarnos por la lista de posts" %>
### Mejorando la experiencia de usuario
La paginación funciona correctamente, pero tiene una peculiaridad algo molesta: cada vez que se hace clic en "Load more" y el router pide más posts, nos envía a la plantilla de la carga mientras esperamos los nuevos datos. El resultado es que cada vez, nos envía a la parte superior de la página y tenemos que desplazarnos hasta el final para reanudar la navegación.
Así que primero, tenemos que decirle a Iron Router que no espere (`waitOn`) la suscripción. En su lugar, definiremos nuestras suscripciones en el hook `subscriptions`.
También estamos pasando una variable `ready` que hace referencia a `this.postsSub.ready` como parte de nuestro contexto de datos, que informará a la plantilla cuando se haya terminado de cargar la suscripción a los posts.
~~~js
//...
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.postsLimit()};
},
subscriptions: function() {
this.postsSub = Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
return Posts.find({}, this.findOptions());
},
data: function() {
var hasMore = this.posts().count() === this.postsLimit();
var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
return {
posts: this.posts(),
ready: this.postsSub.ready,
nextPath: hasMore ? nextPath : null
};
}
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "12~14, 23" %>
Comprobaremos esta variable `ready` en la plantilla para mostrar un spinner al final de la lista de posts mientras estemos cargando el nuevo conjunto de posts:
~~~html
<template name="postsList">
<div class="posts">
{{#each posts}}
{{> postItem}}
{{/each}}
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{else}}
{{#unless ready}}
{{> spinner}}
{{/unless}}
{{/if}}
</div>
</template>
~~~
<%= caption "client/templates/posts/posts_list.html" %>
<%= highlight "9~12" %>
<%= commit "12-5", "Añadir un spinner para hacer la pagición mas atractiva." %>
### Accediendo a cualquier post
Por defecto, cargamos los cinco últimos posts, pero ¿qué pasa si vamos a la página de un post individual?
<%= screenshot "12-4", "Una plantilla vacía." %>
Si lo pruebas, se mostrará un error "no encontrado". En realidad, tiene sentido: le hemos dicho al router que se suscriba a la publicación `posts` cuando carga la ruta `postsList`, pero no le hemos dicho qué debe hacer con la ruta `postpage`.
Pero hasta el momento, lo único que sabemos es suscribirnos a una lista de los `n` últimos posts. ¿Cómo pedimos al servidor un solo post? Te contaré un pequeño secreto: ¡podemos usar más de una publicación para cada colección!
Así que para volver a ver los posts perdidos, crearemos una nueva y separada publicación `singlePost` que solo publica un post, identificado por `_id`.
~~~js
Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});
Meteor.publish('singlePost', function(id) {
check(id, String)
return Posts.find(id);
});
//...
~~~
<%= caption "server/publications.js" %>
<%= highlight "5~8" %>
Ahora, vamos a suscribirnos a los posts correctos en el lado del cliente. Ya estamos suscritos a la publicación `comments` en la función `waitOn` de la ruta `postPage`, por lo que simplemente podemos añadir ahí la suscripción a `singlePost`. Sin olvidarnos de añadir la suscripción a la ruta `postEdit`, que también necesita los mismos datos:
~~~js
//...
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return [
Meteor.subscribe('singlePost', this.params._id),
Meteor.subscribe('comments', this.params._id)
];
},
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/posts/:_id/edit', {
name: 'postEdit',
waitOn: function() {
return Meteor.subscribe('singlePost', this.params._id);
},
data: function() { return Posts.findOne(this.params._id); }
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "6~9,16~18" %>
<%= commit "12-6"," Usando una sola suscripción a los posts para asegurarnos de que siempre se puede ver y editar el post correcto." %>
Terminada la paginación, nuestra aplicación ya no sufre de problemas de escalado, y los usuarios pueden contribuir con muchos más enlaces que antes. ¿No estaría bien tener una forma de clasificarlos? Si no lo sabías, ¡este es precisamente el tema del siguiente capítulo!