-
Notifications
You must be signed in to change notification settings - Fork 30
/
05-routing.md.erb
416 lines (281 loc) · 21.7 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
---
title: Enrutando
slug: routing
date: 0005/01/01
number: 5
points: 5
photoUrl: http://www.flickr.com/photos/ikewinski/9517814403/
photoAuthor: Mike Lewinski
contents: Conoceremos cómo se gestionan las rutas en Meteor.|Crearemos páginas de discusión para los posts con URLs únicas.|Aprenderemos a enlazar apropiadamente esas URLs.
paragraphs: 72
---
Ahora que tenemos una lista de posts (que eventualmente serán enviados por los usuarios), necesitamos una página individual donde nuestros usuarios puedan discutir sobre cada post.
Nos gustaría que estas páginas fueran accesibles a través de un enlace con una URL _permanente_ de la forma `http://myapp.com/posts/xyz` (donde `xyz` es un identificador `_id` de MongoDB) que sea única para cada post.
Esto significa que necesitaremos algún tipo de _enrutamiento_ o _routing_ para analizar lo que hay dentro de la barra de direcciones del navegador y mostrar el contenido correcto.
### Añadiendo el paquete Iron Router
[Iron Router](https://github.com/EventedMind/iron-router) es un paquete de enrutado que ha sido concebido específicamente para aplicaciones Meteor.
No solo ayuda con el enrutamiento (creación de rutas), sino también puede hacerse cargo de filtros (asignar acciones a algunas de estas rutas) e incluso administrar suscripciones (control de qué ruta tiene acceso a qué datos). (Nota: Iron Router ha sido desarrollado en parte por Tom Coleman, coautor de este libro).
En primer lugar, vamos a instalar el paquete desde Atmosphere:
~~~bash
meteor add iron:router
~~~
<%= caption "Terminal" %>
Este comando descarga e instala el paquete iron-router dentro de nuestra aplicación. Hay que tener en cuenta que a veces puede ser necesario reiniciar la aplicación (con `ctrl+c` para parar y `meteor` para iniciar de nuevo) antes de poder usar algunos paquetes.
<% note do %>
### Vocabulario del Router
En este capítulo vamos a tocar un montón de características del Router. Si tienes experiencia con un framework como Rails, ya estarás familiarizado con la mayoría de estos conceptos. Si no, aquí hay un glosario para ponerte al día:
- **Routes**: Una ruta es la pieza de construcción básica del enrutamiento. Es básicamente el conjunto de instrucciones que le dicen a la aplicación a dónde ir y qué hacer cuando se encuentra con una URL.
- **Paths**: Un path es una dirección URL dentro de la aplicación. Puede ser estática (`/terms_of_service`) o dinámica (`/posts/xyz`), e incluso puede incluir parámetros de consulta (`/search?Keyword=meteor`).
- **Segments**: Las diferentes partes de un Path, delimitadas por barras inclinadas (`/`).
- **Hooks**: Son acciones que nos gustaría realizar antes, después o incluso durante el proceso de enrutamiento. Un ejemplo típico sería comprobar si el usuario tiene las credenciales adecuadas antes de mostrar una página.
- **Filters**: Son simplemente Hooks o acciones que se definen de forma global para una o más rutas.
- **Route Templates**: Cada ruta debe apuntar a una plantilla. Si no se especifica una, el router buscará una plantilla con el mismo nombre que la ruta por defecto.
- **Layouts**: Puedes pensar en los layouts como si fueran “marcos” para tu contenido. Contienen todo el código HTML que envuelve la plantilla actual, y seguirá siendo el mismo, aunque la plantilla cambie.
- **Controllers**: Algunas veces, nos daremos cuenta de que muchas de nuestras plantillas utilizan los mismos parámetros. En lugar de duplicar el código, podemos dejar que todas estas rutas se hereden desde un solo _controlador de enrutamiento_ que contendrá toda la lógica necesaria.
Para obtener más información acerca de Iron Router, echa un vistazo a la [documentación completa en GitHub](https://github.com/EventedMind/iron-router).
<% end %>
### Enrutando: Mapeando URLs a plantillas
Hasta ahora, hemos construido nuestro diseño usando una plantilla fija (como `{{> postsList}}`). Así que, aunque el contenido de nuestra aplicación puede cambiar, la estructura básica de la página es siempre la misma: una cabecera, con una lista de posts debajo de ella.
Iron Router nos permite romper este molde al tomar el control de lo que se muestra en el interior de la etiqueta HTML `<body>`. Por eso no vamos a definir el contenido como lo haríamos con una página HTML normal. En vez de eso, vamos a indicar al router que apunte a una plantilla especial que contiene un ayudante `{{> yield}}`.
El ayudante `{{> yield}}` definirá una zona dinámica especial que mostrará automáticamente lo que corresponde a la ruta actual (a modo de convención, llamaremos a esta plantilla especial "route template" o "plantilla de ruta"):
<%= diagram "router-diagram", "Plantillas y layouts.", "pull-center" %>
Empezaremos creando nuestro layout y añadiendo el ayudante {{> yield}}. En primer lugar, vamos a eliminar la etiqueta `<body>` del fichero `main.html`, y movemos su contenido a su propia plantilla, `layout.html` (que colocaremos dentro del directorio `client/templates/application`).
Iron Router se ocupará de insertar nuestro layout en nuestro `main.html` adelgazado, que ahora quedará así:
~~~html
<head>
<title>Microscope</title>
</head>
~~~
<%= caption "client/main.html" %>
Mientras que el nuevo fichero `layout.html`, contendrá ahora el diseño exterior de la aplicación:
~~~html
<template name="layout">
<div class="container">
<header class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<a class="navbar-brand" href="/">Microscope</a>
</div>
</header>
<div id="main">
{{> yield}}
</div>
</div>
</template>
~~~
<%= caption "client/templates/application/layout.html" %>
Te habrás dado cuenta de que hemos cambiado la inclusión de la plantilla `postsList` con una llamada al ayudante `yield`.
Después de este cambio, nuestra pestaña del navegador mostrará la página de ayuda de Iron Router. Esto es debido a que no le hemos dicho al router qué debe hacer con la URL `/`, por lo que simplemente sirve una plantilla vacía.
Para comenzar, podemos recuperar el comportamiento anterior mapeando la URL raíz `/` a la plantilla `postsList`. Vamos a crear un nuevo fichero `router.js` dentro del directorio `/lib` en la raíz de nuestro proyecto:
~~~js
Router.configure({
layoutTemplate: 'layout'
});
Router.route('/', {name: 'postsList'});
~~~
<%= caption "lib/router.js"%>
Hemos hecho dos cosas importantes. En primer lugar, le hemos dicho al router que utilice el `layout` que hemos creado como diseño predeterminado para todas las rutas.
En segundo lugar, hemos definido una nueva ruta llamada `postsList` y la hemos mapeado a `/`.
<% note do %>
### El directorio `/lib`
Meteor garantiza que cualquier cosa que pongamos dentro de la carpeta `/lib` se cargará antes que cualquier otra cosa de la aplicación (con la posible excepción de los smart packages). Esto hace que sea un gran lugar para poner cualquier código auxiliar que debe estar disponible en todo momento.
Solo una pequeña advertencia: ten en cuenta que, dado que la carpeta `/lib` no está dentro ni de `/client` ni de `/server`, sus contenidos estarán disponibles para ambos entornos.
<% end %>
### Rutas con nombre
Vamos a aclarar un poco las cosas. Hemos llamado a nuestra ruta `postsList`, pero también tenemos una plantilla llamada `postsList`. Entonces, ¿qué está pasando?
De forma predeterminada, Iron Router buscará una plantilla con el mismo nombre que la ruta. De hecho, incluso intentará buscar un camino basado en el nombre de la *url* que proporciones. Aunque no funcionará en este caso particular (ya que nuestra ruta es `/`), Iron Router podría encontrar la plantilla correcta si usamos `http://localhost:3000/postsList` como nuestra url.
Te estarás preguntando por qué necesitamos nombrar nuestras rutas. Hacerlo nos permite utilizar algunas características del Iron Router que hacen que sea más fácil construir enlaces dentro de nuestra aplicación. La más útil es el ayudante de Spacebars `{{pathFor}}`, que devuelve los componentes del `path` de cualquier ruta.
Queremos que el enlace principal apunte de nuevo a la lista de mensajes, así que en vez de especificar una URL estática `/`, podemos utilizar el ayudante Spacebars. El resultado final será el mismo, pero tendremos más flexibilidad porque el ayudante siempre obtendrá la dirección URL correcta, incluso si posteriormente cambiamos el path de la ruta en la configuración del router.
~~~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/templates/application/layout.html"%>
<%= highlight "3" %>
<%= commit "5-1", "Enrutado básico." %>
### Esperando a los datos
Si despliegas la versión actual de la aplicación (o lanzas la instancia mediante el enlace anterior), te darás cuenta de que la lista aparece vacía durante unos instantes antes de que aparezcan los posts. Esto es porque cuando la página se carga por primera vez, no hay posts para mostrar hasta que se completa la suscripción `posts` obteniendo los datos enviados desde el servidor.
Tendríamos una mejor experiencia de usuario si proporcionáramos alguna información visual de que algo está pasando, y que el usuario debe esperar un poco.
Por suerte, Iron Router proporciona una forma fácil de hacerlo: podemos decirle que espere (`waitOn`) a la suscripción.
Empezaremos moviendo nuestra suscripción `posts` desde `main.js` hasta el router:
~~~js
Router.configure({
layoutTemplate: 'layout',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "2,3" %>
Lo que estamos diciendo aquí es que para *cualquier* ruta del sitio (ahora mismo solo tenemos una, ¡pero pronto vendrán más!), queremos suscribirnos a la subscripción `posts`.
La diferencia clave entre esto y lo que teníamos antes (cuando la suscripción estaba en `main.js`, que **ahora debería estar vacío y lo podemos eliminar**), es que ahora Iron Router sabe cuando la ruta está "preparada" ("ready") -- esto es, cuando la ruta tiene los datos que necesita para renderizarse.
### Cargando cosas
Saber cuando la ruta `postsList` está lista no nos sirve de mucho si de todas formas vamos a estar mostrando una plantilla vacía. Afortunadamente, Iron Router proporciona una forma de retrasar el renderizado de una plantilla hasta que la ruta esté preparada, y mostrar una plantilla de cargando en su lugar (`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" %>
Fíjate que como hemos definido nuestra función `waitOn` de forma global a nivel del router, esto solo ocurrirá una sola vez cuando el usuario acceda por primera vez a la aplicación. Después de esto, los datos ya estarán cargados en la memoria del navegador y el router no necesitará volver a esperar de nuevo.
La pieza final del rompecabezas es la plantilla de carga. Vamos a utilizar el paquete `spin` para crear un buen efecto de carga animada. Lo añadimos con `meteor add sacha:spin`, y luego creamos la plantilla `loading` de carga en el directorio `client/templates/includes`:
~~~html
<template name="loading">
{{>spinner}}
</template>
~~~
<%= caption "client/templates/includes/loading.html" %>
Ten en cuenta que `{{>spinner}}` está contenido en el paquete `spin`. A pesar de que proviene de "fuera" de nuestra aplicación, podemos incluirlo como cualquier otra plantilla.
Por lo general es una buena idea esperar a las suscripciones, no solo por la experiencia de usuario, sino también porque significa que podemos asumir con seguridad que los datos estarán siempre disponibles dentro de una plantilla. Esto elimina la necesidad de enredarse con plantillas que se muestran antes de que los datos que usan estén disponibles, cosa que a menudo requiere soluciones difíciles.
<%= commit "5-2", "Esperando a la suscripción." %>
<% note do %>
### Un primer vistazo a la reactividad
La reactividad es una parte fundamental de Meteor, y aunque todavía queda un poco para conocerla, nuestra plantilla de carga nos da un primer vistazo a este concepto.
Redireccionar a una plantilla de carga de datos si no se ha cargado todavía está muy bien, pero ¿cómo sabe el router cuándo redirigir al usuario una vez han llegado los datos?
Por ahora, solo diremos que aquí es exactamente donde entra en juego la reactividad. Pero no te preocupes, aprenderás más sobre ella muy pronto!
<% end %>
### Enrutando a un post específico
Ahora que hemos visto cómo enrutar hacia la plantilla `postsList`, vamos a configurar una ruta para mostrar los detalles de un solo post.
Solo hay un problema: no podemos continuar definiendo rutas una por una para cada post, ya que podría haber cientos de ellos. Así que tendremos que crear una ruta _dinámica_ y hacer que se vea esta nos muestre cualquier post que queremos.
Para empezar, vamos a crear una nueva plantilla `post_page.html` que simplemente muestra la misma plantilla para un post que hemos utilizado anteriormente en la lista de posts.
~~~html
<template name="postPage">
<div class="post-page page">
{{> postItem}}
</div>
</template>
~~~
<%= caption "client/templates/posts/post_page.html" %>
Más adelante añadiremos más elementos a esta plantilla (como los comentarios), pero, por ahora, solo la vamos a usar para mostrar `{{> PostItem}}`.
Ahora vamos a crear otra ruta con nombre, esta vez, mapeando URLs de la forma `/posts/<ID>` hacia la plantilla `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" %>
La sintaxis especial `:_id` le dice dos cosas al router: primero, que encuentre cualquier ruta de la forma `/posts/xyz/`, donde "xyz" puede ser cualquier cadena. En segundo lugar, poner lo que encuentra dentro de una propiedad `_id` en el vector de parámetros del router.
Ten en cuenta que usamos el `_id` como cadena porque así lo queremos. El router no tiene manera de saber si le pasamos un `_id` real, o simplemente una cadena de caracteres al azar.
Ya enrutamos a la plantilla correcta, pero todavía nos falta algo: el router conoce el `_id` del post que nos gustaría ver, pero la plantilla todavía no tiene ni idea. Entonces, ¿cómo solucionamos este problema?
Afortunadamente, el router integra una solución inteligente: permite especificar el **contexto de datos** de una plantilla. Puedes pensar en el contexto de datos como lo que rellena un delicioso pastel hecho de plantillas y diseños. En pocas palabras, son los datos con los que rellenamos la plantilla:
<%= diagram "router-diagram-2", "El contexto de datos.", "pull-center" %>
En nuestro caso, podemos obtener el contexto de datos correcto mediante la búsqueda de nuestro post basado en el `_id` que recibimos de la 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 "9,10" %>
De esta forma, cada vez que un usuario accede a esta ruta, encontraremos el post adecuado y lo pasaremos a la plantilla. Recuerda que `findOne` devuelve un solo post, el que coincide con la consulta, y que proporcionar solo un `id` como argumento es una abreviatura de `{_id: id}`.
Dentro de la función `data` de una ruta, `this` se corresponde con la ruta actual, y podemos usar `this.params` para acceder a las propiedades de la ruta (que habíamos indicado con el prefijo `:` dentro de nuestro `path`).
<% note do %>
### Más acerca de los contextos de datos
Al establecer el contexto de datos de una plantilla, se puede controlar el valor de `this` dentro de los ayudantes de la plantilla.
Esto se hace implícitamente con el iterador `{{#each}}`, que ajusta automáticamente el contexto de datos de cada iteración para el elemento que se está iterando:
~~~html
{{#each widgets}}
{{> widgetItem}}
{{/each}}
~~~
Pero también podemos hacerlo explícitamente utilizando `{{#with}}`, que simplemente dice "toma este objeto, y le aplicas la siguiente plantilla". Por ejemplo, se puede escribir:
~~~html
{{#with myWidget}}
{{> widgetPage}}
{{/with}}
~~~
Resulta que se consigue el mismo resultado pasando el contexto como un argumento en la llamada a la plantilla. Así que el bloque de código anterior se puede reescribir como:
~~~js
{{> widgetPage myWidget}}
~~~
Para una exploración con profundidad sobre los contextos de datos sugerimos [leer nuestro blog](https://www.discovermeteor.com/blog/a-guide-to-meteor-templates-data-contexts/) sobre este tema.
<% end %>
### Usando nuestro enrutador dinámico
Por último, crearemos un nuevo botón “Discuss” que enlazará a nuestra página invidual del post. De nuevo, podríamos hacer algo como `<a href="/posts/{{_id}}">`, pero es mucho más fiable utilizar un ayudante de ruta.
Hemos llamado a la ruta al post `postPage`, así que podemos usar el ayudante `{{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/templates/posts/post_item.html"%>
<%= highlight "6" %>
<%= commit "5-3", "Ruta para un único post." %>
Pero espera, ¿cómo sabe el router dónde conseguir la parte `xyz` en `/posts/xyz`? Después de todo, no le hemos pasado ninguna `_id`.
Resulta que Iron Router es lo suficientemente inteligente como para averiguarlo por sí mismo. Le estamos diciendo que use la ruta `postPage`, y el router sabe que esta ruta requiere un `_id` de algún tipo (así es como hemos definido nuestro `path`).
Así que el router buscará este `_id` en el lugar más lógico: el contexto de datos del ayudante `{{pathFor 'postPage'}}`, en otras palabras: `this`. Y da la casualidad de que nuestro `this` corresponde a un post, que (¡sorpresa!) tiene una propiedad `_id`.
De forma alternativa, se puede especificar el lugar donde tiene que buscar el `_id`, pasando un segundo argumento al ayudante (es decir, `{{pathFor 'postPage' someOtherPost}}`). Un uso práctico sería, por ejemplo, conseguir los enlaces a los posts anterior y siguiente en una lista.
Para ver si todo funciona correctamente, navega a la lista de posts y haz clic en uno de los enlaces 'Discuss'. Deberías ver algo como esto:
<%= screenshot "5-2", "La página para un sólo post." %>
<% note do %>
### HTML5 pushState
Una cosa que hay que tener en cuenta es que estos cambios en las URLs suceden gracias a [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).
El router recoge los clics en URL internas, y evita que el navegador salga fuera de la aplicación haciendo los cambios necesarios en su estado.
Si todo funciona correctamente la página debería cambiar instantáneamente. De hecho, a veces las cosas cambian tan rápido que podría ser necesario añadir algún tipo de transición. Esto está fuera del alcance de este capítulo, aunque, no obstante, es un tema interesante.
<% end %>
### Post no encontrado
No olvidemos que el enrutamiento funciona de ambas formas: podemos cambiar la URL cuando visitamos una página, pero también podemos mostrar una página cuando cambiemos *la URL*. Por lo que tenemos que pensar que pasa si alguien introduce una URL *errónea*.
Menos mal que Iron Router se preocupa por esto a través de la opción `notFoundTemplate`.
Primero, crearemos una plantilla que muestre un simple error 404:
~~~html
<template name="notFound">
<div class="not-found page 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"%>
Después, sencillamente le decimos a Iron Router que use esta plantilla:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
//...
~~~
<%= caption "lib/router.js"%>
<%= highlight "4" %>
Para probar la nueva página de error, puedes intentar introducir una URL aleatoria como `http://localhost:3000/nothing-here`.
Pero un momento, ¿qué pasa si alguien introduce una URL de la forma `http://localhost:3000/posts/xyz`, donde `xyz` *no* es un identificador `_id` de post válido? Esto es una ruta válida, pero no apunta a ningún dato.
Afortunadamente, Iron Router es lo suficientemente inteligente para saber esto si definimos un *hook* especial `dataNotFound` al final de `router.js`:
~~~js
//...
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
~~~
<%= caption "lib/router.js"%>
<%= highlight "4" %>
Esto le dice a Iron Router que muestre la página de no encontrado, no solo cuando la ruta sea inválida, si no también para la ruta `postPage`cuando la función `data` devuelva un objeto falso (o `null`, `false`, `undefined` o vació).
<%= commit "5-4", "Añadida la plantilla de no encontrado." %>
<% note do %>
### ¿Por qué “Iron”?
Te sorprenderías sobre la historia detrás del nombre “Iron Router”. Según el autor Chris Mather, viene del hecho de que los meteoritos están compuestos principalmente de hierro.
<% end %>