Enrutando

5

Porcentaje completado

En este capítulo:

  • 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.
  • 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 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:

    meteor add iron:router
    
    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.

    Vocaulario 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.

    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”):

    Plantillas y layouts.
    Plantillas y layouts.

    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í:

    <head>
      <title>Microscope</title>
    </head>
    
    client/main.html

    Mientras que el nuevo fichero layout.html, contendrá ahora el diseño exterior de la aplicación:

    <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>
    
    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:

    Router.configure({
      layoutTemplate: 'layout'
    });
    
    Router.route('/', {name: 'postsList'});
    
    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 /.

    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.

    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.

    <header class="navbar navbar-default" role="navigation">
      <div class="navbar-header">
        <a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
      </div>
    </header>
    
    //...
    
    client/templates/application/layout.html

    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:

    Router.configure({
      layoutTemplate: 'layout',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.route('/', {name: 'postsList'});
    
    lib/router.js

    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):

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.route('/', {name: 'postsList'});
    
    lib/router.js

    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:

    <template name="loading">
      {{>spinner}}
    </template>
    
    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.

    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!

    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.

    <template name="postPage">
      <div class="post-page page">
        {{> postItem}}
      </div>
    </template>
    
    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:

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.route('/', {name: 'postsList'});
    Router.route('/posts/:_id', {
      name: 'postPage'
    });
    
    lib/router.js

    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:

    El contexto de datos.
    El contexto de datos.

    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:

    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); }
    });
    
    lib/router.js

    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).

    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:

    {{#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:

    {{#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:

    {{> widgetPage myWidget}}
    

    Para una exploración con profundidad sobre los contextos de datos sugerimos leer nuestro blog sobre este tema.

    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'}}:

    <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>
    
    client/templates/posts/post_item.html

    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:

    La página para un sólo post.
    La página para un sólo post.

    HTML5 pushState

    Una cosa que hay que tener en cuenta es que estos cambios en las URLs suceden gracias a HTML5 pushState.

    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.

    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:

    <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>
    
    client/templates/application/not_found.html

    Después, sencillamente le decimos a Iron Router que use esta plantilla:

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    //...
    
    lib/router.js

    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:

    //...
    
    Router.onBeforeAction('dataNotFound', {only: 'postPage'});
    
    lib/router.js

    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 postPagecuando la función data devuelva un objeto falso (o null, false, undefined o vació).

    Commit 5-4

    Añadida la plantilla de no encontrado.

    ¿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.