Discover Meteor - Building Real-Time JavaScript Web Apps - Sacha Greif & Tom Coleman
Discover Meteor - Building Real-Time JavaScript Web Apps - Sacha Greif & Tom Coleman
Discover Meteor - Building Real-Time JavaScript Web Apps - Sacha Greif & Tom Coleman
www.discovermeteor.com
Introduccin
Hagamos un pequeo experimento mental. Imaginemos que abrimos dos ventanas del explorador de
archivos de nuestro ordenador mostrando la misma carpeta.
Ahora borramos un archivo en una de las dos ventanas. Habr desaparecido en la otra?
Cuando modificamos algo en nuestro sistema de archivos local, el cambio se aplica en todas partes
sin necesidad de refrescos o callbacks. Simplemente sucede.
Ahora, vamos a pensar qu pasara en la web en esta misma situacin. Por ejemplo, digamos que
abrimos el mismo WordPress en dos ventanas del navegador y creamos un post en una de ellas. A
diferencia del escritorio, la otra ventana no reflejar el cambio a menos que la recargues.
Nos hemos acostumbrado a la idea de que un sitio web es algo con lo que solo te comunicas como a
rfagas separadas.
Meteor es parte de una nueva ola de frameworks y tecnologas que buscan desafiar el statu quo
haciendo webs reactivas y en tiempo real.
Qu es Meteor?
Meteor es una plataforma para crear aplicaciones web en tiempo real construida sobre Node.js.
Meteor se localiza entre la base de datos de la aplicacin y su interfaz de usuario y se encarga que las
dos partes estn sincronizadas.
Como Meteor usa Node.js, se utiliza JavaScript en el cliente y en el servidor. Y ms an, Meteor es
capaz de compartir cdigo entre ambos entornos.
El resultado es una plataforma muy potente y muy sencilla ya que Meteor abstrae muchas de las
molestias y dificultades que nos encontramos habitualmente en el desarrollo de aplicaciones web.
Por qu Meteor?
Por qu dedicar tiempo a aprender Meteor en lugar de cualquier otro framework web? Dejando a un
lado las caractersticas de Meteor, creemos que todo se reduce a una sola cosa: Meteor es full stack y
es fcil de aprender.
Meteor permite crear una aplicacin web en tiempo real en cuestin de horas. Y si ya hemos hecho
desarrollo web, estaremos familiarizados con JavaScript, y ni siquiera tendremos que aprender un
nuevo lenguaje.
Meteor podra ser la plataforma ideal para nuestras necesidades, o, quizs no. Pero por qu no
probarlo y descubrirlo por nosotros mismos?
Uno de nuestros objetivos al escribir este libro es mantener las cosas accesibles y fciles de entender,
as que cualquiera debera ser capaz de seguirlo, aunque no tenga experiencia con Meteor, Node.js, o
frameworks MVC, o incluso con la programacin en general en el lado del servidor.
Por otro lado, se asume cierta familiaridad con los conceptos y la sintaxis bsica de JavaScript. Si
alguna vez has hackeado algo de cdigo jQuery o jugado un poco con la consola de desarrollo del
navegador, vers como no tendrs problemas en seguirlo.
Si an no te sientes cmodo usando JavaScript, te sugerimos que eches un vistazo a nuestra entrada
JavaScript primer for Meteor de nuestro blog, antes de seguir con el libro.
Por otro lado, las barras laterales profundizan en los entresijos de Meteor, y nos ayudarn a
comprender mejor lo que realmente ocurre entre bastidores.
As que, si nos consideramos principiantes, deberamos de saltarnos las barras laterales en una
primera lectura, y volver a ellas ms tarde una vez que hayamos jugado un poco con Meteor.
Commit 11-2
Mostrar las notificaciones en la cabecera.
Ver en GitHub
Lanzar instancia
Solo una cosa, ten en cuenta que el hecho de que ofrezcamos estos commits, no significa que tengas
que ir de un checkout al siguiente. Aprenders mucho ms si dedicas el tiempo necesario a escribir el
cdigo de tu aplicacin!
Otros recursos
Si quieres aprender ms acerca de un aspecto particular de Meteor, la documentacin oficial de
Meteor es el mejor sitio al que ir para empezar.
Tambin te recomendamos Stack Overflow para solucionar problemas y dudas, y el canal IRC
#meteor si necesitas ayuda directa.
Necesito Git?
Estar familiarizado con el control de versiones Git no es estrictamente necesario para seguir
este libro, pero lo recomendamos encarecidamente.
Si quieres ponerte al da, te recomendamos Git Is Simpler Than You Think de Nick Farina.
Si eres principiante, tambin te recomendamos la app GitHub for Mac, que te permite
administrar repositorios sin utilizar la lnea de comandos. O SourceTree (Mac OS &
Windows), los dos gratuitos.
Contacto
Si deseas ponerte en contacto con nosotros, puedes enviarnos un correo electrnico a
hello@discovermeteor.com.
Adems, si encuentras un error tipogrfico o cualquier otro error en el contenido del libro,
puedes reportarlo en este repositorio de GitHub.
Si encuentras un problema en el cdigo de Microscope, puedes enviarlo al repositorio de
Microscope.
Por ltimo, para cualquier otra pregunta, puedes dejarnos un comentario en el panel lateral de
esta aplicacin.
Empezando
Las primeras impresiones son las que cuentan. La instalacin de Meteor debera ser muy sencilla y, en
la mayora de los casos, slo cuesta 5 minutos ponerlo en marcha.
Para empezar, si estamos usando Mac OS o GNU/Linux, podemos instalar Meteor con el siguiente
comando desde la consola:
curl https://install.meteor.com | sh
Si ests usando Windows, echa un vistazo a la guia oficial de instalacin: install instructions en la
web de Meteor.
Se instalar el ejecutable meteor en nuestro sistema y lo dejar listo para empezar a usar Meteor.
Este comando crea un proyecto bsico listo para usar. Cuando termina, deberamos ver un directorio
llamado microscope/ , que contiene lo siguiente:
.meteor
microscope.css
microscope.html
microscope.js
La aplicacin que se ha creado es una aplicacin bsica que demuestra slo algunas sencillas pautas.
A pesar de que nuestra aplicacin no hace casi nada, ya podemos ejecutarla. Para hacerlo, volvemos
al terminal y escribimos:
cd microscope
meteor
Commit 2-1
Un proyecto bsico.
Ver en GitHub
Lanzar instancia
Enhorabuena! ya tenemos nuestra primera aplicacin Meteor funcionando. Por cierto, para parar la
aplicacin, todo lo que hay que hacer es abrir la pestaa terminal donde se ejecuta y pulsar ctrl+c .
Si ests utilizando Git, este sera un buen momento para iniciar el repositorio con git init .
Aadir un paquete
Ahora vamos a usar el sistema de paquetes de Meteor para incluir el framework Bootstrap en nuestro
proyecto:
Esto no es distinto de aadir Bootstrap de la forma habitual, incluyendo manualmente los ficheros
CSS y JavaScript, excepto en que confiamos en el mantenedor del paquete para que lo mantenga
actualizado para nosotros.
Ya que estamos, aadiremos tambin el paquete Underscore. Underscore es una librera de utilidades
JavaScript, y es muy til cuando necesitemos manipular estructuras de datos.
El paquete bootstrap lo mantiene el usuario twbs , por lo que el nombre completo del paquete es
twbs:bootstrap
El paquete underscore forma parte de los paquetes oficiales incluidos en Meteor, lo que quiere
decir que no hay que incluir el nombre del autor:
Fjate que estamos aadiendo Bootstrap 3. Algunas de las capturas de este libro estn tomadas de
una versin antigua de Microscope con Bootstrap 2, por lo que podran parecer ligeramente
diferentes.
Commit 2-2
Aadido el paquete bootstrap.
Ver en GitHub
Lanzar instancia
Tan pronto como agregues el paquete Bootstrap, deberas notar un cambio en el aspecto de nuestra
aplicacin:
Con Boostrap.
Al contrario de la forma tradicional en la que incluimos recursos externos, no tenemos que agregar
enlaces a ningn fichero CSS o JavaScript, porque Meteor lo hace por nosotros! Esta es slo una de
Ahora, crearemos dos archivos vacos main.html y main.js dentro de /client . Por ahora, no te
preocupes si esto rompe completamente la app, empezaremos a rellenar los nuevos ficheros en el
siguiente captulo.
Debemos mencionar que algunos de los directorios que hemos creado son especiales y Meteor tiene
reglas para ellos:
El cdigo de /server se ejecuta en el servidor.
El cdigo de /client se ejecuta en el cliente.
Todo lo dems se ejecuta en las dos partes, cliente y servidor.
Las cosas estticas (fuentes, imgenes, etc.) van en el directorio /public .
Y tambin es til saber como Meteor decide en que orden cargan los ficheros:
Los archivos de /lib se cargan antes que nada.
Los archivos con nombre main.* se cargan despus que todos los dems.
Todo se carga por orden alfabtico segn el nombre del fichero.
Ten en cuenta que aunque Meteor tiene todas estas reglas, en realidad no nos obliga a utilizar una
estructura de archivos predefinida. As que la estructura que sugerimos es slo nuestra forma de
hacer las cosas, no son reglas inamovibles.
Os animamos a echar un vistazo a la documentacin oficial Meteor para conocer ms detalles acerca
de la estructura de las aplicaciones.
Meteor es MVC?
Si hemos usado otros frameworks, como Ruby on Rails, puede que nos preguntemos si las
aplicaciones de Meteor adoptan el patrn MVC (Model View Controller).
La respuesta corta es no. A diferencia de Rails, Meteor no impone ninguna estructura
predefinida para su aplicacin. As que en este libro vamos a exponer cdigo de la forma que
ms sentido tenga para nosotros, sin preocuparnos demasiado por las siglas.
Pblico?
Bueno, mentimos. En realidad no vamos a necesitar public/ por la sencilla razn de que Microscope
no utiliza ningn archivo esttico. Pero como en la mayora de aplicaciones se van a incluir al menos
un par de imgenes, pensamos que era importante incluirlo.
Por cierto, te puedes haber dado cuenta de que se ha creado un directorio oculto llamado .meteor .
Aqu es donde Meteor almacena su propio cdigo. Cambiar cosas aqu dentro es, en general, una muy
mala idea con las nicas excepciones de los archivos .meteor/packages y .meteor/release , que se
utilizan, respectivamente, para listar nuestros paquetes y para establecer la versin de Meteor que
queremos utilizar.
Underscores vs CamelCases
Lo nico que vamos a decir sobre el viejo debate del guin bajo ( my_variable ) contra el
camelCase ( myVariable ) es que en realidad no importa el que elijas, siempre y cuando lo
adoptes en todo el proyecto.
En este libro, utilizamos camelCase porque es la forma habitual en JavaScript (despus de
todo, es JavaScript no java_script!).
Las nicas excepciones a esta regla son los nombres de los archivos, para los que se van a
utilizar guiones bajos ( my_file.js ), y las clases CSS, para las que usaremos guiones ( .myclass
color: #666666; }
#main {
position: relative;
}
.page {
position: absolute;
top: 0px;
width: 100%;
}
.navbar {
margin-bottom: 10px; }
/* line 32, ../sass/style.scss */
.navbar .navbar-inner {
border-radius: 0px 0px 3px 3px; }
#spinner {
height: 300px; }
.post {
/* For modern browsers */
/* For IE 6/7 (trigger hasLayout) */
*zoom: 1;
position: relative;
opacity: 1; }
.post:before, .post:after {
content: "";
display: table; }
.post:after {
clear: both; }
.post.invisible {
opacity: 0; }
.post.instant {
-webkit-transition: none;
-moz-transition: none;
-o-transition: none;
transition: none; }
.post.animate{
-webkit-transition: all 300ms 0ms;
-moz-transition: all 300ms 0ms ease-in;
-o-transition: all 300ms 0ms ease-in;
transition: all 300ms 0ms ease-in; }
.post .upvote {
display: block;
margin: 7px 12px 0 0;
float: left; }
.post .post-content {
float: left; }
.post .post-content h3 {
margin: 0;
line-height: 1.4;
font-size: 18px; }
.post .post-content h3 a {
display: inline-block;
margin-right: 5px; }
.post .post-content h3 span {
font-weight: normal;
font-size: 14px;
display: inline-block;
color: #aaaaaa; }
.post .post-content p {
margin: 0; }
.post .discuss {
display: block;
float: right;
margin-top: 7px; }
.comments {
list-style-type: none;
margin: 0; }
.comments li h4 {
font-size: 16px;
margin: 0; }
.comments li h4 .date {
font-size: 12px;
font-weight: normal; }
.comments li h4 a {
font-size: 12px; }
.comments li p:last-child {
margin-bottom: 0; }
.dropdown-menu span {
display: block;
padding: 3px 20px;
clear: both;
line-height: 20px;
color: #bbb;
white-space: nowrap; }
.load-more {
display: block;
border-radius: 3px;
background: rgba(0, 0, 0, 0.05);
text-align: center;
height: 60px;
line-height: 60px;
margin-bottom: 10px; }
.load-more:hover {
text-decoration: none;
background: rgba(0, 0, 0, 0.1); }
.posts .spinner-container{
position: relative;
height: 100px;
}
.jumbotron{
text-align: center;
}
.jumbotron h2{
font-size: 60px;
font-weight: 100;
}
@-webkit-keyframes fadeOut {
0% {opacity: 0;}
10% {opacity: 1;}
90% {opacity: 1;}
100% {opacity: 0;}
}
@keyframes fadeOut {
0% {opacity: 0;}
10% {opacity: 1;}
90% {opacity: 1;}
100% {opacity: 0;}
}
.errors{
position: fixed;
z-index: 10000;
padding: 10px;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
pointer-events: none;
}
.alert {
animation: fadeOut 2700ms ease-in 0s 1 forwards;
-webkit-animation: fadeOut 2700ms ease-in 0s 1 forwards;
-moz-animation: fadeOut 2700ms ease-in 0s 1 forwards;
width: 250px;
float: right;
clear: both;
margin-bottom: 5px;
pointer-events: auto;
}
client/stylesheets/style.css
Commit 2-3
Estructura de ficheros reorganizada.
Ver en GitHub
Lanzar instancia
Despliegue
SIDEBAR
2.5
A algunos les gusta trabajar silenciosamente en un proyecto hasta que queda perfecto. Otros quieren
mostrarlo al mundo lo ms pronto posible.
Si eres de los primeros y prefieres desarrollar a nivel local, no dudes en saltarte este captulo. Pero si
prefieres aprender a desplegar tu aplicacin Meteor en la Web, ahora te explicamos cmo hacerlo.
Vamos a aprender a desplegar una aplicacin Meteor de diferentes formas. Eres libre de utilizar
cualquiera de ellas en cualquier etapa del desarrollo, ya sea trabajando en Microscope o en otra
aplicacin. Vamos a empezar!
Por supuesto que tienes que tener cuidado de reemplazar myapp con un nombre de tu eleccin, y
preferiblemente uno que no est en uso.
Si es la primera vez que despliegas una aplicacin, te pedir crear una cuenta en Meteor. Y si todo va
bien, despus de unos segundos podrs acceder a la aplicacin desde http://myapp.meteor.com .
Puedes mirar la documentacin oficial para obtener ms informacin sobre cosas como el acceso a
la base de datos de la instancia, o cmo configurar un dominio personalizado.
Meteor Up
Aunque todos los das aparecen nuevas soluciones en la nube, a menudo vienen con su propia cuota
de problemas y limitaciones. As que, actualmente, el despliegue en tu propio servidor sigue siendo la
mejor manera de poner una aplicacin Meteor en produccin. El problema es que, hacerlo uno mismo
no es tan sencillo, especialmente si lo que ests buscando es un despliegue de calidad.
Meteor Up (o mup , para abreviar) es un intento de solucionar este problema con una utilidad de lnea
de comandos que se encarga por nosotros de la instalacin y el despliegue. As que veamos cmo
desplegar Microscope utilizando Meteor Up.
Antes de nada, vamos a necesitar un servidor. Recomendamos o bien Digital Ocean, desde $5 al mes,
o bien AWS, que proporciona Micro instancias gratuitas (con las que rpidamente tendremos
problemas de escala, aunque deberan ser suficientes si solo buscamos empezar a jugar con Meteor).
Sea cual sea el servicio que elijas, debes obtener tres cosas: la direccin IP del servidor, un inicio de
sesin (normalmente root o ubuntu ), y una contrasea. Guarda estas cosas en un lugar seguro,
pronto las necesitaremos!.
Inicializando Meteor Up
Para empezar, necesitamos instalar Meteor Up va npm :
mkdir ~/microscope-deploy
cd ~/microscope-deploy
mup init
Configuracin de Meteor Up
Cuando inicializamos un nuevo proyecto, Meteor Up crea dos archivos: mup.json y settings.json .
mup.json
contendr todos los ajustes relacionados con la aplicacin (tokens OAuth, tokens para anlisis, etc.)
El siguiente paso es configurar el archivo mup.json . Aqu est el archivo mup.json que mup init
genera por defecto. Todo lo que hay que hacer es rellenar los espacios en blanco:
mup.json
gestin.
Si has decidido usar Compose, configura setupMongo como false y aade la variable de entorno
MONGO_URL
le decimos cmo llegar hasta nuestra aplicacin. Slo hay que introducir la ruta local completa,
que se puede obtener con el comando pwd de la terminal, cuando estamos dentro del directorio de
la aplicacin.
Environment Variables
Dentro del bloque env podemos especificar todas las variables de entorno de nuestra la aplicacin
(por ejemplo, ROOT_URL , MAIL_URL , MONGO_URL , etc.).
Configuracin y despliegue
Antes de que podamos desplegar, tendremos que configurar el servidor para que est listo para alojar
aplicaciones Meteor. La magia de Meteor Up encapsula este complejo proceso en un solo comando!
mup setup
Esto llevar un tiempo dependiendo del rendimiento del servidor y la conectividad de la red. Despus
de que la instalacin termine correctamente, por fin podemos desplegar nuestra aplicacin con:
mup deploy
Mostrando logs
Los registros son muy importantes y Meteor Up proporciona una forma fcil de manejarlos, emulando
el comando tail -f . Solo tienes que escribir:
mup logs -f
Aqu termina nuestro resumen de lo que puede hacer Meteor Up. Para ms informacin, le sugerimos
visitar el repositorio GitHub de Meteor Up.
Estas tres formas de desplegar aplicaciones Meteor deberan ser suficiente para la mayora de los
casos de uso. Por supuesto, sabemos que algunos de vosotros preferirais tener el control y configurar
un servidor Meteor desde cero. Eso, lo dejaremos para otro da o tal vez para otro libro!
Plantillas
Para introducirnos de manera sencilla en el desarrollo con Meteor, adoptaremos un enfoque de afuera
hacia adentro, es decir, primero construiremos el envoltorio exterior y luego lo conectaremos al
funcionamiento interno de la aplicacin.
Esto implica que, en este captulo, solo utilizaremos el directorio /client .
Si todava no lo has hecho, crea un nuevo archivo main.html dentro del directorio client ,
rellenndolo con el siguiente cdigo:
<head>
<title>Microscope</title>
</head>
<body>
<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">
{{> postsList}}
</div>
</div>
</body>
client/main.html
Esta ser la plantilla principal de la aplicacin. Como se puede ver, todo es HTML excepto la etiqueta
{{> postsList}}
La aplicacin que estamos construyendo va a ser una red social de noticias que estar compuesta de
mensajes (en adelante, posts) organizados en listas, y as es como organizaremos nuestras plantillas.
Vamos a crear el directorio /templates dentro de /client . Aqu pondremos todas nuestras
plantillas, pero adems, para mantener las cosas ordenadas creamos el directorio /posts dentro de
/templates
Ya estamos listos para nuestra segunda plantilla. Dentro de client/templates/posts , crea el fichero
posts_list.html
<template name="postsList">
<div class="posts">
{{#each posts}}
{{> postItem}}
{{/each}}
</div>
</template>
client/templates/posts/posts_list.html
Y post_item.html :
<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
</div>
</div>
</template>
client/templates/posts/post_item.html
Fjate en el atributo name="postsList" del elemento template. Este ser el nombre que Meteor
usar para saber donde va cada plantilla (fjate que el nombre del fichero no es importante).
Es el momento de introducir Spacebars el sistema de plantillas de Meteor. Spacebars es simplemente
HTML mas tres cosas: inclusiones (tambin llamadas partials o plantillas parciales), expresiones
(expressions) y bloques de ayuda (block helpers).
Las inclusiones usan la sintaxis {{> templateName}} y simplemente le dicen a Meteor que reemplace
la inclusin por la plantilla del mismo nombre (en nuestro caso postItem ).
Las expresiones como {{title}} pueden, o bien llamar a una propiedad del objeto actual o bien, al
valor de retorno de un ayudante (helper) como el que definiremos ms adelante en nuestro gestor de
plantilla.
Los bloques de ayuda son tags especiales para mantener el control del flujo de la plantilla, por
ejemplo {{#each}}{{/each}} o {{#if}}{{/if}} .
Ir ms lejos
Si quieres saber ms sobre Spacebars puedes consultar la documentacin oficial.
Con estos conocimientos, ya podemos entender cmo van a funcionar nuestras plantillas:
Primero, en la plantilla postsList iteramos sobre un objeto posts usando un bloque {{#each}}
{{/each}}
Pero, de dnde viene el objeto posts ?. Buena pregunta. Es un ayudante de plantilla, y puedes
pensar en ellos como un cajn o hueco para valores dinmicos.
La plantilla postItem es bastante sencilla. Solo usa tres expresiones: {{url}} y {{title}}
devuelven propiedades, y {{domain}} llama a un ayudante.
Ayudantes de plantillas
Hasta ahora hemos estado tratando con Spacebars, que es poco ms que HTML con algunas etiquetas
extra. A diferencia de otros lenguajes como PHP (o pginas HTML con JavaScript), Meteor mantiene
las plantillas y su lgica separadas, de forma que nuestras plantillas por s mismas no hacen casi
nada.
Para que una plantilla tenga vida, necesita ayudantes. Puedes pensar en ellos como los cocineros
que toman los ingredientes (tus datos) y los preparan, antes de entregar el plato terminado (las
plantillas) al camarero, que, finalmente, los entrega.
En otras palabras, mientras la funcin de las plantillas es mostrar o iterar sobre variables, los
ayudantes son los que hacen el trabajo pesado asignando un valor a cada variable.
Controladores?
Puede ser tentador pensar que los ficheros que contienen estos ayudantes son una especie
de controlador. Pero eso sera ambiguo, ya que los controladores (al menos en el sentido
de controladores MVC) normalmente tienen un papel diferente.
As que hemos decido apartarnos de esa terminologa, y simplemente nos referimos a
ayudantes de plantillas o lgica de plantilla cuando hablamos del cdigo JavaScript que
acompaa a las plantillas.
Para mantener las cosas ordenadas, adoptaremos la convencin de nombrar al fichero que contiene
la plantilla con el mismo nombre, pero con la extensin .js. As que vamos a crear un fichero
posts_list.js
var postsData = [
{
title: 'Introducing Telescope',
url: 'http://sachagreif.com/introducing-telescope/'
},
{
title: 'Meteor',
url: 'http://meteor.com'
},
{
title: 'The Meteor Book',
url: 'http://themeteorbook.com'
}
];
Template.postsList.helpers({
posts: postsData
});
client/templates/posts/posts_list.js
Estamos haciendo dos cosas. Primero, creamos algunos datos prototipo en postsData .
Normalmente, estos datos vienen de la base de datos, pero como no hemos visto cmo hacerlo
todava (espera al siguiente captulo), hacemos trampa mediante el uso de datos estticos.
Segundo, usamos la funcin Template.postsList.helpers() para definir un ayudante de plantilla
llamado posts que, sencillamente devuelve nuestros datos creados en postsData .
Y si recuerdas, estamos usando el ayudante posts en nuestra plantilla postsList :
<template name="postsList">
<div class="posts page">
{{#each posts}}
{{> postItem}}
{{/each}}
</div>
</template>
client/templates/posts/posts_list.html
Al definir el ayudante posts , conseguimos que est disponible para usarlo en la plantilla, as que
nuestra plantilla ser capaz de recorrer el array postData pasando la plantilla postItem para cada
uno de sus elementos.
Commit 3-1
Aadimos una plantilla bsica para recorrer los posts y d
Ver en GitHub
El ayudante
Lanzar instancia
domain
Template.postItem.helpers({
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
}
});
client/templates/posts/post_item.js
Esta vez el valor de nuestro ayudante domain , no son datos sino una funcin annima. Este patrn es
mucho ms comn (y ms til) en comparacin con nuestros ejemplos de datos ficticios.
El ayudante domain coge una URL y devuelve su dominio a travs de un poco de magia JavaScript.
Pero, de dnde saca esa url la primera vez?.
Para responder a esta pregunta tenemos que volver a nuestra plantilla posts_list.html . El bloque
{{#each}}
no solo itera nuestros datos, sino que tambin establece el valor de this dentro del
Commit 3-2
Establecemos un ayudante `domain` en `postItem`.
Ver en GitHub
Lanzar instancia
Magia JavaScript
Aunque esto no es especfico de Meteor, he aqu una breve explicacin de la magia de
JavaScript. En primer lugar, estamos creando un elemento HTML ancla ( a ) vaco y lo
almacenamos en la memoria.
A continuacin, establecemos el atributo href para que sea igual a la URL del post actual
(como acabamos de ver, en un ayudante, this es el objeto que se est usando en este
momento).
Por ltimo, aprovechamos de la propiedad hostname del elemento a para devolver el
nombre de dominio del post sin el resto de la URL.
Si todo ha ido bien deberamos ver una lista de posts en el navegador. Esta lista son solo datos
estticos, lo que, por el momento, no nos permite aprovechar las caractersticas de tiempo real de
Meteor. Aprenderemos cmo hacerlo en el prximo captulo!
Recarga automtica
Probablemente ya habrs notado que no es necesario recargar la ventana del navegador
cada vez que cambiamos un archivo de cdigo.
Esto se debe a que Meteor hace un seguimiento de todos los archivos en el directorio del
proyecto, y los actualiza automticamente en el navegador cada vez que detecta un cambio
en alguno de ellos.
La recarga automtica es bastante inteligente, ya que incluso, conserva el estado de la
aplicacin entre dos refrescos!
SIDEBAR
3.5
Commits
El bloque bsico de trabajo de un repositorio git es el commit o confirmacin de cdigo. Puedes pensar
en un commit como en una instantnea del estado de tu cdigo en un momento dado.
En lugar de darte el cdigo final de microscope, hemos ido tomando instantneas en cada paso del
proceso de construccin, y las hemos compartido todas en GitHub.
Por ejemplo, este es el ltimo commit del captulo anterior y se ve as:
Lo que ves es el di (de diferencia) del archivo post_item.js , es decir, los cambios introducidos
por este commit. En este caso, hemos creado el archivo post_item.js a partir de cero, por lo que
todo su contenido se destaca en verde.
Vamos a compararlo con un ejemplo de ms adelante:
Modificando cdigo.
Codigo eliminado.
Como ves, tienes acceso al repo tal y como estaba en ese commit especfico:
GitHub no nos da muchas pistas visuales de que estamos viendo un commit en particular, pero si lo
comparramos con la rama mster, veremos que la estructura de archivos es muy diferente:
github_microscope
el directorio microscope , solo tienes que elegir cualquier otro nombre (no tiene que tener el mismo
nombre que el repositorio GitHub).
Ahora entramos con cd en el repositorio recin descargado y estamos listos para usar la utilidad
de lnea de comando
de git:
cd github_microscope
Cuando clonamos el repositorio de GitHub, descargamos todo el cdigo, lo que significa que lo que
vemos es el ltimo commit (HEAD) de la aplicacin.
Afortunadamente, hay una forma de volver atrs en el tiempo y revisar (check out) un commit
especfico sin afectar a los dems. Vamos a probarlo:
Git nos est informando de que estamos aislados de HEAD, lo que significa que ahora podremos ver
commits del pasado pero no podremos cambiarlos, como si los viramos a travs de una bola de
cristal.
(Ten en cuenta que Git tiene comandos que permiten cambiar commits del pasado. Esto sera como
viajar en el tiempo y pisar una mariposa, pero queda fuera del alcance de esta breve introduccin).
Hemos sido capaces de escribir tan solo chapter3-1 porque todos los commits de Microscope estn
etiquetados con el nmero de captulo correcto. Si este no fuera el caso, tendras que encontrar el
hash o identificador nico del commit que quieres ver.
Una vez ms, GitHub nos lo pone fcil. Podemos encontrar el hash de cada commit en la esquina
inferior derecha de la cabecera azul como se puede ver a continuacin:
Y por ltimo, qu pasa si queremos dejar de mirar la bola mgica y volver al presente? Pues le
decimos a Git que queremos volver a la rama master:
Recuerda que, en cualquier momento del proceso, puedes ejecutar la aplicacin usando el comando
meteor
, aunque el repositorio no est en HEAD. Puede ser necesario ejecutar un meteor update
primero si Meteor se queja de que faltan paquetes, ya que el cdigo de los paquetes no est incluido
en el repositorio.
Ahora, dispones de una lista ordenada de todos los commits que afectaron a este archivo en
particular:
Esta vista ordenada nos muestra lnea por lnea quin modific el archivo en cada commit (en otras
palabras, a quin hay que echar la culpa si las cosas ya no funcionan):
Git, al igual que GitHub, son herramientas complejas, por lo que no podemos pretender conocerlas
con profundidad en un solo captulo. De hecho, apenas hemos araado la superficie de lo que es
posible hacer con ellas. Pero, lo poco que hemos visto te va a permitir seguir el resto del libro sin
problemas.
Colecciones
lib/collections/posts.js
Commit 4-1
Coleccin Posts
Ver en GitHub
Lanzar instancia
Almacenando datos
Las aplicaciones web tienen a su disposicin bsicamente tres formas de almacenar datos, cada una
desempeando un rol diferente:
La memoria del navegador: cosas como las variables JavaScript son almacenadas en la
memoria del navegador, lo que implica que no son permanentes: son datos locales a la pestaa
del navegador y desaparecern tan pronto como la cierres.
El almacn del navegador: los navegadores tambin pueden almacenar datos de forma
permanente usando cookies o Local Storage. Aunque estos datos sean permanentes de una
sesin a otra, son locales al usuario actual (pero disponible entre las pestaas) y no se puede
compartir de forma sencilla con otros usuarios.
La base de datos del servidor: el mejor lugar para almacenar datos de forma permanente para
que puedan estar disponibles para ms de un usuario es una base de datos (Siendo MongoDB la
solucin por defecto para las aplicaciones Meteor).
Meteor hace uso de estas tres formas, y a veces sincroniza los datos de un lugar a otro (como veremos
pronto). Dicho esto, la base de datos permanece como la fuente de datos cannica que contiene la
copia maestra de los datos.
Cliente y Servidor
El cdigo dentro de las carpetas que no sean client/ ni server/ se ejecutar en ambos contextos.
Por lo que la coleccin Posts estar disponible en el lado cliente y servidor. Sin embargo, lo que
La Terminal
Prompt: $ .
Tambin se conoce como: Shell, Bash
Consola del navegador
La consola de Meteor
La consola de Mongo
Para mirar el interior de la base de datos Mongo, abrimos una segunda ventana de terminal (mientras
Meteor se est ejecutando en la primera), vamos al directorio de la aplicacin y ejecutamos el
comando meteor mongo para iniciar una shell de Mongo, en la que podemos escribir los comandos
estndares de Mongo (y como de costumbre, salir con ctrl+c ). Por ejemplo, vamos a insertar un
nuevo post:
meteor mongo
> db.posts.insert({title: "A new post"});
> db.posts.find();
{ "_id": ObjectId(".."), "title" : "A new post"};
Consola de mongo
Mongo en Meteor.com
Debemos saber que cuando alojamos nuestra aplicacin en *.meteor.com, tambin
podemos acceder a la consola de Mongo usando meteor mongo myApp .
Y ya que estamos, tambin podemos obtener los logs de nuestra aplicacin escribiendo
meteor logs myApp
La sintaxis de Mongo es familiar, ya que utiliza una interfaz JavaScript. No vamos a hacer ningn tipo
de manipulacin de datos adicional en la consola de Mongo, pero podemos echar un vistazo de vez
en cuando solo para ver lo que pasa por ah.
navegador de la coleccin real de Mongo. Cuando decimos que las colecciones del lado del cliente son
una cach, queremos decir que contiene un subconjunto de los datos, y ofrece un acceso muy
rpido.
Es importante entender este punto, ya que es fundamental para comprender la forma en la que
funciona Meteor. En general, una coleccin del lado del cliente consiste en un subconjunto de todos
los documentos almacenados en la coleccin de Mongo (por lo general, nunca querremos enviar toda
nuestra base de datos al cliente).
En segundo lugar, los documentos se almacenan en la memoria del navegador, lo que significa que el
acceso a ellos es prcticamente instantneo. As que, cuando se llama, por ejemplo, a Posts.find()
desde el cliente, no hay caminos lentos hasta el servidor o a la base de datos, ya que los datos ya
estn precargados.
Introduciendo MiniMongo
La implementacin de Mongo en el lado del cliente de Meteor se llama MiniMongo. Todava
no est implementada por completo y es posible que podamos encontrar algunas
caractersticas de Mongo que no funcionan en MiniMongo. Sin embargo, todas las que
cubrimos en este libro funcionan de manera similar.
Comunicacin cliente-servidor
La parte ms importante de todo esto es cmo se sincronizan los datos de la coleccin del cliente con
la coleccin del mismo nombre (en nuestro caso posts ) del servidor.
Mejor que explicarlo en detalle, vamos a verlo.
Empezaremos abriendo dos ventanas del navegador, y accediendo a la consola en cada uno de ellos.
A continuacin, abrimos la consola de Mongo en la lnea de comandos.
En este punto, deberamos ser capaces de encontrar el nico documento que hemos creado antes
desde la consola de Mongo (ten en cuenta que el interfaz de nuestra aplicacin estar mostrando
todava los tres posts de prueba anteriores. Ignralos por ahora).
> db.posts.find();
{title: "A new post", _id: ObjectId("..")};
Consola de Mongo
Posts.findOne();
{title: "A new post", _id: LocalCollection._ObjectID};
Creemos un nuevo post en una de las ventanas del navegador ejecutando un insert:
Posts.find().count();
1
Posts.insert({title: "A second post"});
'xxx'
Posts.find().count();
2
Como era de esperar, el post aparece en la coleccin local. Ahora vamos a comprobar Mongo:
db.posts.find();
{title: "A new post", _id: ObjectId("..")};
{title: "A second post", _id: 'yyy'};
Consola de Mongo
Como puedes ver, el post ha viajado hasta la base de datos sin escribir una sola lnea de cdigo para
enlazar nuestro cliente hasta el servidor (bueno, en sentido estricto, hemos escrito una sola lnea de
cdigo: new Mongo.Collection("posts") ). Pero eso no es todo!
Escribamos esto en la consola del segundo navegador:
Posts.find().count();
2
El post est ah tambin! A pesar de que no hemos refrescado ni interactuado con el segundo
navegador, y desde luego no hemos escrito cdigo para insertar actualizaciones. Todo ha sucedido
meteor reset
El comando reset borra completamente la base de datos Mongo. Es til en el desarrollo cundo hay
bastantes posibilidades de que nuestra base de datos caiga en un estado inconsistente.
Vamos a inciar nuestra aplicacin Meteor de nuevo:
meteor
Ahora que la base de datos est vaca, podemos aadir lo siguiente a server/fixtures.js para
cargar tres posts cuando el servidor arranca y encuentra la coleccin Posts vaca:
if (Posts.find().count() === 0) {
Posts.insert({
title: 'Introducing Telescope',
url: 'http://sachagreif.com/introducing-telescope/'
});
Posts.insert({
title: 'Meteor',
url: 'http://meteor.com'
});
Posts.insert({
title: 'The Meteor Book',
url: 'http://themeteorbook.com'
});
}
server/fixtures.js
Commit 4-2
Datos para la coleccin de posts.
Ver en GitHub
Lanzar instancia
Hemos ubicado este archivo en el directorio /server , por lo que no se cargar en el navegador de
ningn usuario. El cdigo se ejecutar inmediatamente cuando se inicia el servidor, y har tres
llamadas a insert para agregar tres sencillos posts en la coleccin de Posts.
Ahora ejecutamos nuevamente el servidor con meteor , y estos tres posts se cargarn en la base de
datos.
Datos dinmicos
Si abrimos una consola de navegador, veremos los tres mensajes cargados desde MiniMongo:
Posts.find().fetch();
Para ver estos mensajes renderizados en HTML, podemos utilizar un ayudante de plantilla.
En el Captulo 3 vimos cmo Meteor nos permite enlazar un contexto de datos a nuestras plantillas
Spacebars para construir vistas HTML a partir de estructuras de datos simples. Bien, pues, de la
misma forma vamos a enlazar los datos de nuestra coleccin. Simplemente reemplazamos el objeto
JavaScript esttico postsData por una coleccin dinmica.
A propsito, no dudes en borrar el cdigo de postsData . As es cmo debe quedar
client/templates/posts/posts_list.js
Template.postsList.helpers({
posts: function() {
return Posts.find();
}
});
client/templates/posts/posts_list.js
Commit 4-3
Conexin entre la coleccin Posts y la plantilla `postList`.
Ver en GitHub
Lanzar instancia
Ahora, en lugar de cargar una lista de mensajes como un array esttico desde una variable, ahora
estamos devolviendo un cursor a nuestro ayudante posts (aunque la cosa no parece muy diferente
puesto que estamos devolviendo exactamente los mismos datos):
Nuestro ayudante {{#each}} ha recorrido todos nuestros Posts , y los ha mostrado en la pantalla.
La coleccin del lado del servidor ha tomado los posts de Mongo, los ha pasado a nuestra coleccin
del lado del cliente, y nuestro ayudante Spacebars los ha pasado a la plantilla.
Ahora iremos un paso ms all, y vamos a aadir otro post a travs de la consola del navegador:
Posts.insert({
title: 'Meteor Docs',
author: 'Tom Coleman',
url: 'http://docs.meteor.com'
});
Acabas de ver la reactividad en accin por primera vez. Cuando le pedimos a Spacebars que recorra el
cursor Posts.find() , l ya sabe cmo monitorizar este cursor en busca de cambios, y de esa forma,
alterar el cdigo HTML para mostrar los datos correctos en la pantalla.
. Si queremos asegurarnos de que esto es realmente lo que ocurre, solo tenemos que
abrir el inspector DOM del navegador y seleccionar el <div> correspondiente a uno de los
posts existentes.
Ahora, desde la consola, insertamos otro post. Cuando volvemos de nuevo al inspector,
podremos ver un <div> , correspondiente al nuevo post, pero seguirs teniendo el mismo
<div>
seleccionado. Esta es una manera til de saber cundo han vuelto a ser renderizados
Esto tiene un efecto instantneo. Si miramos ahora el navegador, veremos que todos nuestros posts
han desaparecido! Esto se debe a que confibamos en autopublish para asegurarnos de que
nuestra coleccin del lado del cliente era una rplica de todos los posts de la base de datos.
Con el tiempo vamos a necesitar asegurarnos de que solo trasferimos los posts que el usuario
realmente necesita ver (teniendo en cuenta cosas como la paginacin). Pero, por ahora, lo vamos a
configurar para que la coleccin Posts se publique en su totalidad (tal y como lo tenamos hasta
ahora).
Para ello, creamos una funcin publish() que devuelve un cursor que referencia a todos los posts:
Meteor.publish('posts', function() {
return Posts.find();
});
server/publications.js
Meteor.subscribe('posts');
client/main.js
Commit 4-4
`autopublish` eliminado y configurada una publicacin bs
Ver en GitHub
Lanzar instancia
Si comprobamos el navegador de nuevo, veremos que nuestros posts estn de vuelta. Uf!
Conclusin
Entonces, qu hemos logrado? Bueno, a pesar de que no tenemos interfaz de usuario, lo que
tenemos es una aplicacin web completamente funcional. Podramos desplegar esta aplicacin en
Internet, y (mediante la consola del navegador) empezar a publicar nuevas historias y verlas aparecer
en los navegadores de otros usuarios de todo el mundo.
Publicaciones y suscripciones
SIDEBAR
4.5
Las publicaciones y las suscripciones son dos de los conceptos ms importantes de Meteor, pero
puede que sean difciles de comprender si acabas de empezar.
Esto ha acarreado una gran cantidad de malentendidos, como la creencia de que Meteor es inseguro,
o que las aplicaciones no pueden manejar grandes cantidades de datos.
La magia que hace Meteor es la razn ms importante de que ocurra esto al principio. Aunque la
magia es, en ltima instancia muy til, puede ocultar lo que realmente est pasando entre bastidores
(como suele pasar con la magia). As que vamos a desenvolver las capas de dicha magia para tratar de
entender lo que est pasando.
Por ltimo, la aplicacin coge el cdigo HTML y lo enva hacia el navegador. El trabajo de la aplicacin
ha terminado, y puede relajarse tomando una cerveza mientras espera la siguiente solicitud.
As que lo que ocurre es que el empleado de la tienda, no solo encuentra el libro, sino que adems te
sigue a casa para lertelo por la noche (admitiremos que suena un poco raro).
Esta arquitectura permite a Meteor hacer cosas muy interesantes, la ms importante es lo que Meteor
llama base de datos en todas partes. En pocas palabras, Meteor tomar un subconjunto de la base
de datos y la copiar en el cliente.
Esto tiene dos grandes implicaciones: la primera es que en lugar de enviar cdigo HTML, una
aplicacin Meteor enva datos actuales en bruto al cliente y deja que el cliente se ocupe de ellos
(data on the wire). Lo segundo es que sers capaz de acceder, e incluso modificar esos datos
instantneamente sin tener que esperar al servidor (latency compensation).
Publicaciones
Una base de datos de una aplicacin puede contener decenas de miles de documentos, algunos de
los cuales podran ser privados o confidenciales. As que, obviamente, por razones de seguridad y
escalabilidad, no deberamos sincronizar toda la base de datos en el cliente.
Por lo tanto, vamos a necesitar una forma de decirle a Meteor qu subconjunto de los datos se
pueden enviar al cliente, y lo podremos hacer utilizando las publicaciones.
Volvamos a Microscope. Aqu estn todos los posts de nuestra aplicacin que hay en la base de datos:
Aunque esta funcin no exista realmente en Microscope, imaginemos que algunos de nuestros posts
se han marcado como entradas con lenguaje abusivo. Aunque queramos mantenerlos en nuestra
base de datos, no deben ponerse a disposicin de los usuarios (es decir, enviarse a los clientes).
Nuestra primera tarea ser decirle a Meteor qu datos queremos enviar. Le diremos que solo
queremos publicar posts sin marcar:
// on the server
Meteor.publish('posts', function() {
return Posts.find({flagged: false});
});
Esto asegura que no hay manera posible de que el cliente pueda acceder a un post marcado. Esta es
la forma de hacer una aplicacin segura con Meteor: basta con asegurarse de que solo publicas los
datos a los que el cliente actual debe tener acceso.
DDP
Puedes pensar en el sistema de publicaciones/suscripciones como un embudo que trasfiere
datos desde una coleccin en el servidor (origen) a una en el cliente (destino).
El protocolo que se habla dentro del embudo se llama DDP (que significa Protocolo de Datos
Distribuidos). Para aprender ms sobre DDP, puedes ver esta charla de la conferencia en
Real-time de Matt DeBergalis (uno de los fundadores de Meteor), o este screencast de Chris
Mather que te gua a travs de este concepto con un poco ms de detalle.
Suscripciones
A pesar de que no vamos a poner a disposicin de los clientes los posts marcados, pueden quedar
miles que no debemos enviar de una sola vez. Necesitamos una forma de que los clientes
especifiquen qu subconjunto necesitan en un momento determinado, y aqu es exactamente donde
entran las suscripciones.
Cualquier dato que se suscribe, se refleja en el cliente gracias a Minimongo, la implementacin de
MongoDB en el lado del cliente que provee Meteor.
Por ejemplo, digamos que estamos viendo la pgina del perfil de Bob Smith, y que solo queremos ver
sus posts.
En primer lugar, podramos modificar nuestra publicacin para que acepte un parmetro:
// on the server
Meteor.publish('posts', function(author) {
return Posts.find({flagged: false, author: author});
});
Entonces podramos definir ese parmetro cuando nos suscribimos a esa publicacin desde el cliente:
// on the client
Meteor.subscribe('posts', 'bob-smith');
Esta es la forma de hacer escalable una aplicacin Meteor: en lugar de suscribirse a todos los datos
disponibles, solo escogemos las piezas que necesitamos en un momento dado. De esta manera,
evitaremos sobrecargar la memoria del navegador, sin importar si el tamao de la base de datos del
servidor es enorme.
Bsquedas
Ahora resulta que los mensajes de Bob tienen varias categoras (por ejemplo: JavaScript, Ruby, y
Python). Tal vez todava queremos cargar todos los Mensajes de Bob en la memoria, pero en este
momento solo queremos mostrar los de la categora JavaScript. Aqu es donde la bsqueda entra
en juego
Al igual que hicimos en el servidor, vamos a utilizar la funcin Posts.find () para seleccionar un
subconjunto de estos datos:
// on the client
Template.posts.helpers({
posts: function(){
return Posts.find({author: 'bob-smith', category: 'JavaScript'});
}
});
Ahora que tenemos una buena comprensin de cmo funcionan las publicaciones y suscripciones,
vamos a profundizar un poco ms, repasando los patrones de diseo ms comunes.
Autopublicacin
Si creas un proyecto Meteor desde cero (es decir, usando meteor create ), el paquete autopublish
se habilitar automticamente. Como punto de partida, vamos a hablar acerca de lo que hace
exactamente este paquete.
El objetivo de autopublish es que sea muy fcil empezar a desarrollar y lo hace reflejando todos los
datos del servidor en el cliente, lo que permite olvidarse de publicaciones y suscripciones y empezar
directamente a escribir el cdigo de la aplicacin.
Autopublish
Y cmo funciona? Supn que tienes una coleccin en el servidor llamada 'posts' . Entonces
autopublish
buscar todos los posts que haya en la base de datos Mongo y los enviar
Por esta razn, autopublish solo es apropiado cuando estamos empezando, cundo todava no se
ha pensado en las publicaciones.
Meteor.publish('allPosts', function(){
return Posts.find();
});
Todava publicamos colecciones completas, pero al menos ahora tenemos control sobre qu
colecciones publicamos. En este caso, publicamos la coleccin Posts pero no Comments .
Meteor.publish('somePosts', function(){
return Posts.find({'author':'Tom'});
});
Entre bastidores
Si has ledo la documentacin de Meteor sobre publicaciones, tal vez te habrs sentido
abrumado al or hablar de added() y ready() para establecer los atributos de los registros
en el cliente, y te habr costado cuadrarlo con aplicaciones basadas en Meteor que hayas
podido ver y que nunca usan esos mtodos.
La razn es que Meteor ofrece una comodidad muy importante: el mtodo
_publishCursor()
, .changed() y .removed() ).
As, en el ejemplo anterior, podemos asegurar que el usuario solo tiene en la cach, los posts
en los que est interesado (los escritos por Tom).
Meteor.publish('allPosts', function(){
return Posts.find({}, {fields: {
date: false
}});
});
Por supuesto, tambin podemos combinar ambas tcnicas. Por ejemplo, si quisiramos devolver
todos los posts de Tom, dejando de lado sus fechas, escribiramos:
Meteor.publish('allPosts', function(){
return Posts.find({'author':'Tom'}, {fields: {
date: false
}});
});
Recapitulando
Hemos visto cmo pasar de publicar todas las propiedades de todos los documentos de todas las
colecciones (con autopublish ) a publicar solo algunas propiedades de algunos documentos de
algunas colecciones.
Esto cubre los fundamentos de lo que se puede hacer con las publicaciones en Meteor, y estas
tcnicas tan sencillas deberan servir para la gran mayora de casos de uso.
An as, en ocasiones, tendrs que ir ms all combinando, vinculando, o fusionando publicaciones.
Todo esto lo veremos ms adelante en uno de los captulos del libro!
Enrutando
Ahora que tenemos una lista de posts (que eventualmente sern enviados por los usuarios),
necesitamos una pgina individual donde nuestros usuarios puedan discutir sobre cada post.
Nos gustara que estas pginas fueran accesibles a travs 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 algn tipo de enrutamiento o routing para analizar lo que hay dentro
de la barra de direcciones del navegador y mostrar el contenido correcto.
Terminal
Este comando descarga e instala el paquete iron-router dentro de nuestra aplicacin. Hay que tener
en cuenta que a veces puede ser necesario reiniciar la aplicacin (con ctrl+c para parar y meteor
Plantillas y layouts.
Empezaremos creando nuestro layout y aadiendo 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
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 diseo exterior de la aplicacin:
<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 habrs dado cuenta de que hemos cambiado la inclusin de la plantilla postsList con una
llamada al ayudante yield .
Despus de este cambio, nuestra pestaa del navegador mostrar la pgina 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 vaca.
Para comenzar, podemos recuperar el comportamiento anterior mapeando la URL raz / a la
plantilla postsList . Vamos a crear un nuevo fichero router.js dentro del directorio /lib en la
raz 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 diseo 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 aplicacin (con la posible excepcin de los smart
packages). Esto hace que sea un gran lugar para poner cualquier cdigo auxiliar que debe
estar disponible en todo momento.
Solo una pequea advertencia: ten en cuenta que, dado que la carpeta /lib no est dentro
ni de /client ni de /server , sus contenidos estarn disponibles para ambos entornos.
client/templates/application/layout.html
Commit 5-1
Enrutado bsico.
Ver en GitHub
Lanzar instancia
Por suerte, Iron Router proporciona una forma fcil de hacerlo: podemos decirle que espere ( waitOn )
a la suscripcin.
Empezaremos moviendo nuestra suscripcin 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 vendrn ms!), queremos suscribirnos a la subscripcin posts .
La diferencia clave entre esto y lo que tenamos antes (cuando la suscripcin estaba en main.js , que
ahora debera estar vaco 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 vaca. 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
Fjate que como hemos definido nuestra funcin waitOn de forma global a nivel del router, esto solo
ocurrir una sola vez cuando el usuario acceda por primera vez a la aplicacin. Despus de esto, los
datos ya estarn 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 aadimos 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 aplicacin, 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 tambin porque significa que podemos asumir con seguridad que los datos estarn siempre
disponibles dentro de una plantilla. Esto elimina la necesidad de enredarse con plantillas que se
muestran antes de que los datos que usan estn disponibles, cosa que a menudo requiere soluciones
difciles.
Commit 5-2
Esperando a la suscripcin.
Ver en GitHub
Lanzar instancia
<template name="postPage">
<div class="post-page page">
{{> postItem}}
</div>
</template>
client/templates/posts/post_page.html
Ms adelante aadiremos ms 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 parmetros 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 todava nos falta algo: el router conoce el _id del post que
nos gustara ver, pero la plantilla todava no tiene ni idea. Entonces, cmo solucionamos este
problema?
Afortunadamente, el router integra una solucin 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 diseos. En pocas palabras, son los datos con los que rellenamos la plantilla:
El contexto de datos.
En nuestro caso, podemos obtener el contexto de datos correcto mediante la bsqueda 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 funcin 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 habamos indicado con el prefijo :
{{#each widgets}}
{{> widgetItem}}
{{/each}}
{{#with myWidget}}
{{> widgetPage}}
{{/with}}
Para una exploracin con profundidad sobre los contextos de datos sugerimos leer nuestro
blog sobre este tema.
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.
Ver en GitHub
Lanzar instancia
Pero espera, cmo sabe el router dnde conseguir la parte xyz en /posts/xyz ? Despus 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 algn
tipo (as es como hemos definido nuestro path ).
As que el router buscar este _id en el lugar ms lgico: el contexto de datos del ayudante
{{pathFor 'postPage'}}
prctico sera, 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. Deberas ver algo como esto:
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
aplicacin haciendo los cambios necesarios en su estado.
Si todo funciona correctamente la pgina debera cambiar instantneamente. De hecho, a
veces las cosas cambian tan rpido que podra ser necesario aadir algn tipo de transicin.
Esto est fuera del alcance de este captulo, 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 pgina, pero tambin podemos mostrar una pgina cuando cambiemos la URL. Por lo
que tenemos que pensar que pasa si alguien introduce una URL errnea.
Menos mal que Iron Router se preocupa por esto a travs de la opcin 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
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
//...
lib/router.js
Para probar la nueva pgina de error, puedes intentar introducir una URL aleatoria como
http://localhost:3000/nothing-here
//...
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
lib/router.js
Esto le dice a Iron Router que muestre la pgina de no encontrado, no solo cuando la ruta sea
invlida, si no tambin para la ruta postPage cuando la funcin data devuelva un objeto falso (o
null
Commit 5-4
Aadida la plantilla de no encontrado.
Ver en GitHub
Lanzar instancia
Por qu Iron?
Te sorprenderas sobre la historia detrs del nombre Iron Router. Segn el autor Chris
Mather, viene del hecho de que los meteoritos estn compuestos principalmente de hierro.
La sesin
SIDEBAR
5.5
Meteor es un framework reactivo. Esto significa que cuando cambian los datos, cambian cosas de la
aplicacin sin tener que hacer nada de forma explcita.
Ya lo hemos visto en accin viendo cmo cambian nuestras plantillas cuando cambian los datos y las
rutas.
En captulos posteriores veremos ms en profundidad cmo funciona todo esto, pero ahora, nos
gustara introducir algunas caractersticas bsicas de la reactividad, que son muy tiles en todas las
aplicaciones.
La sesin en Meteor
Ahora mismo, en Microscope, el estado actual de la aplicacin est contenido en su totalidad en la
URL que se est mostrando (y en la base de datos).
Pero en muchos casos, necesitars almacenar valores de estado que solo son relevantes en la versin
de la aplicacin para usuario actual (por ejemplo, si algn elemento se muestra o est oculto). Usar la
Sesin es una forma conveniente de hacerlo.
La sesin es un almacn global de datos reactivo. Es global en el sentido de que es un objeto global:
solo hay una sesin, y esta es accesible desde todas partes. Las variables globales se suelen ver como
algo malo, pero en este caso la sesin se utiliza como un bus de comunicacin central para diferentes
partes de la aplicacin.
Modificando la Sesin
La sesin est disponible en todas partes en el cliente como el objeto Session . Para establecer un
valor en la sesin, puedes llamar a:
Puedes leer los datos de nuevo con Session.get('mySessionProperty'); . Hemos dicho que la
Sesin es una fuente de datos reactiva, lo que significa que si lo pones en un ayudante, veras un
cambio en el navegador cuando cambia la variable de sesin.
Para probarlo, aade el siguiente cdigo a la plantilla layout:
client/templates/application/layout.html
Template.layout.helpers({
pageTitle: function() { return Session.get('pageTitle'); }
});
client/templates/application/layout.js
La recarga automtica de Meteor (recarga de cdigo en caliente o HCR, de Hot Code Reload)
preserva las variables de sesin, por lo que ahora debes ver A dierent title en la barra de
navegacin. Si no es as, solo tienes que escribir el comando Session.set() de nuevo.
Si lo cambiamos de nuevo (desde la consola del navegador), debemos ver que se visualiza otro ttulo:
La sesin est disponible a nivel global, por lo que los cambios se pueden hacer desde cualquier lugar
de la aplicacin. Esto nos da una gran cantidad de potencia y flexibilidad, pero tambin puede ser una
trampa si se usa demasiado.
De todas formas, es importante apuntar que el objeto de Session no es compartido entre distintos
usuarios, ni siquiera entre distintas pestaas del navegador. Esto es por lo que si abres tu aplicacin
en una nueva pestaa, te encontrars con una pgina en blanco.
Cambios idnticos
Si modificas una variable de sesin con Session.set() pero la estableces al mismo valor,
Meteor es lo suficientemente inteligente como para eludir la cadena reactiva, y evitar las
llamadas a mtodos innecesarios.
Presentamos a Autorun
Hemos visto un ejemplo de una fuente de datos reactiva, y la hemos visto en accin dentro de un
ayudante de plantilla. Pero mientras que algunos contextos en Meteor (como ayudantes de plantilla)
son inherentemente reactivos, la mayora de cdigo de la aplicacin sigue siendo el viejo y simple
JavaScript.
Supongamos que tenemos el siguiente fragmento de cdigo en algn lugar de nuestra aplicacin:
helloWorld = function() {
alert(Session.get('message'));
}
A pesar de que llamamos a una variable de sesin, el contexto en el que se hace no es reactivo, lo que
significa que no vamos a ver alerts cada vez que cambia su valor.
Aqu es dnde entra en juego Autorun. Como su nombre indica, el cdigo dentro de un bloque
autorun
se ejecutar automticamente cada vez que cambien las fuentes de datos reactivas
utilizadas dentro.
Prueba a escribir esto en la consola del navegador:
Como era de esperar, el bloque de cdigo situado en el interior de autorun se ejecuta una vez,
mostrando los datos por la consola. Ahora, vamos a intentar cambiar el ttulo:
Magia! Al cambiar el valor, autorun sabe que tiene que ejecutar su contenido de nuevo, volviendo a
mostrar el nuevo valor por la consola.
Volviendo a nuestro ejemplo anterior, si queremos activar una nueva alerta cada vez que cambien
variables de sesin, lo nico que tenemos que hacer es envolver nuestro cdigo en un bloque
autorun
Tracker.autorun(function() {
alert(Session.get('message'));
});
Como acabamos de ver, los autorun pueden ser muy tiles para rastrear fuentes de datos reactivas y
reaccionar inmediatamente ante ellos.
Si recargramos la pgina manualmente se perderan las variables de sesin (porque que estaramos
creando una nueva). Pero si provocamos una recarga en caliente (por ejemplo, guardando uno de
nuestros archivos de cdigo), la pgina volver a cargar, pero todava tendremos el valor de la
Session.get('pageTitle');
'A brand new title'
As que si utilizamos variables de sesin para hacer un seguimiento de lo que est haciendo el
usuario, el HCR debe ser prcticamente trasparente para el usuario, ya que preserva el valor de todas
las variables de sesin. Esto nos permite desplegar nuevas versiones de nuestra aplicacin, ya en
produccin, con la seguridad de que no molestaremos mucho a nuestros usuarios.
Considera esto un momento. Si podemos llegar a mantener el estado entre la URL y la sesin,
podemos cambiar de forma trasparente el cdigo que est corriendo por debajo con una mnima
interrupcin.
Ahora vamos a comprobar lo que pasa cuando refrescamos la pgina de forma manual:
Session.get('pageTitle');
null
Browser console
Hemos perdido la sesin. En un HCR, Meteor guarda la sesin en el almacenamiento local del
navegador y lo carga de nuevo tras la recarga. Esto no significa que el comportamiento de recarga
explcita no tenga sentido: si un usuario recarga la pgina, es como si navega de nuevo a la misma
URL, y el estado, debe restablecerse al que vera cualquier usuario que visita esa URL.
Las lecciones ms importantes en todo esto son:
1. Guarda siempre el estado en la sesin o en la URL para no molestar mucho a los usuarios
Aadiendo usuarios
Hasta el momento, hemos logrado crear y mostrar algunos datos estticos y conectarlo todo en un
prototipo simple.
Incluso hemos visto cmo nuestra interfaz de usuario es sensible a los cambios en los datos, y que los
cambios aparecen inmediatamente cuando se insertan o cambian los datos. An as, nuestro sitio
est limitado por el hecho de que no podemos introducir datos. De hecho, ni siquiera tenemos
usuarios todava!
Veamos cmo arreglamos esto.
Terminal
Estos dos comandos hacen accesibles las plantillas para cuentas de forma que podemos incluirlas en
nuestro sitio usando el ayudante {{> loginButtons}} . Un consejo muy til: se puede controlar en
qu lado aparece el desplegable de inicio de sesin con el atributo align (por ejemplo: {{>
loginButtons align="right"}})
Vamos a aadir los botones a nuestra cabecera. Y puesto que la cabecera est empezando a crecer,
vamos a darle ms espacio en su propia plantilla (la pondremos en el directorio
client/templates/includes/
<template name="layout">
<div class="container">
{{> header}}
<div id="main">
{{> yield}}
</div>
</div>
</template>
client/templates/application/layout.html
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>
client/templates/includes/header.html
Podemos usarlos para iniciar sesin, solicitar un cambio de contrasea, y todo lo que se necesita para
gestionar cuentas basadas en contraseas.
Para decirle a nuestro sistema de cuentas que queremos que los usuarios accedan al sistema a travs
de solo un nombre de usuario, simplemente aadimos un bloque de configuracin en un nuevo
archivo config.js dentro de client/helpers/
Accounts.ui.config({
passwordSignupFields: 'USERNAME_ONLY'
});
client/helpers/config.js
Commit 6-1
Aadidas cuentas de usuario y una plantilla en la cabecera
Ver en GitHub
Lanzar instancia
Meteor.users.findOne();
La consola devuelve un objeto que representa el usuario. Si lo inspeccionamos un poco, veremos que
contiene nuestro nombre de usuario, as como un identificador nico _id . Tambin se puede
obtener el usuario que ha iniciado sesin con Meteor.user() .
Ahora, nos registramos con otro usuario y ejecutamos lo siguiente en la consola del navegador:
Meteor.users.find().count();
1
La consola devuelve 1. No debera haber 2? Se ha borrado el primero? Si intentas acceder con ese
usuario, vers que no es as.
Vamos a mirar en la base de datos del servidor ( meteor mongo en la terminal):
> db.users.count()
2
Consola de Mongo
, dejamos de enviar todos los datos procedentes del servidor a las colecciones locales
de cada cliente conectado. Tuvimos que crear parejas de publicaciones y suscripciones para
intercambiar datos.
Sin embargo, no hemos creado ninguna clase de publicacin para usuarios. As que cmo es que
podemos ver esos datos?
La respuesta es que el paquete accounts , autopublica los datos bsicos de la cuenta del usuario
actual, de otra forma, el usuario no podra acceder nunca al sitio.
El hecho de que el paquete accounts slo publica el usuario actual explica porqu un usuario no
puede ver los detalles de la cuenta de los otros usuarios.
As que solo se publica un objeto de usuario por usuario conectado (y ninguno cuando no est
autenticado).
Es ms, los documentos en el navegador no parecen contener los mismos campos que en el servidor.
En Mongo, un usuario tiene un montn de datos. Para verlo, vuelve a la terminal de Mongo y escribe:
> db.users.find()
{
"_id": "H5kKyxtbkLhmPgtqs",
"createdAt": ISODate("2015-02-10T08:26:48.196Z"),
"profile": {},
"services": {
"password": {
"bcrypt": "$2a$10$yGPywo3/53IHsdffdwe766roZviT03YBGltJ0UG"
},
"resume": {
"loginTokens": [{
"when": ISODate("2015-02-10T08:26:48.203Z"),
"hashedToken": "npxGH7Rmkuxcv098wzz+qR0/jHl0EAGWr0D9ZpOw="
}]
}
},
"username": "sacha"
}
Consola de Mongo
Por otro lado, en el navegador el objeto de usuario tiene muchos menos campos, como se puede ver
escribiendo el comando equivalente:
Meteor.users.findOne();
Object {_id: "kYdBd9hr3fWPGPcii", username: "tmeasday"}
Este ejemplo nos muestra como una coleccin local puede ser un subconjunto seguro de la base de
datos real. El usuario conectado solo ve lo necesario para poder hacer el trabajo (en este caso, el
nombre de usuario). Este es un patrn que debemos aprender porque nos ser muy til ms adelante.
Eso no significa que no podamos hacer pblicos ms datos de usuario. Si lo necesitamos, podemos
consultar la documentacin de Meteor para ver cmo se hace.
Reactividad
SIDEBAR
6.5
Si las colecciones son la caracterstica principal de Meteor, podemos decir que la reactividad es lo
que hace til esta caracterstica.
Las colecciones trasforman radicalmente la forma en que la aplicacin maneja cambios en los datos.
En lugar de tener que comprobarlo manualmente (por ejemplo, con una llamada AJAX) y actualizar
los cambios en el cdigo HTML, con Meteor, los cambios pueden llegar en cualquier momento y
aplicarse a la interfaz de usuario sin ms complicaciones.
Vamos a verlo con detenimiento: entre bastidores, Meteor es capaz de cambiar cualquier parte de la
interfaz de usuario cuando se actualiza una coleccin subyacente.
La forma imperativa (o a mano) de hacerlo sera utilizar .observe() , una funcin del cursor que
dispara callbacks cuando los documentos seleccionados cambian. De esta forma, podramos hacer
cambios en el DOM (el HTML de nuestra pgina web) a travs de esos callbacks. El cdigo resultante
sera algo como esto:
Posts.find().observe({
added: function(post) {
// when 'added' callback fires, add HTML element
$('ul').append('<li id="' + post._id + '">' + post.title + '</li>');
},
changed: function(post) {
// when 'changed' callback fires, modify HTML element's text
$('ul li#' + post._id).text(post.title);
},
removed: function(post) {
// when 'removed' callback fires, remove HTML element
$('ul li#' + post._id).remove();
}
});
Es probable que te des cuentas de que este tipo de cdigo va a tender a hacerse muy complejo muy
rpidamente. Imagnate lo que sera tratar con cambios en cada uno de los atributos del post, y tener
que cambiar HTML complejo dentro de <li> . Por no hablar de casos ms extremos cuando
empecemos a depender de mltiples fuentes de informacin que pueden cambiar en tiempo real.
Un enfoque declarativo
Meteor nos proporciona una forma fcil de hacer todo esto: la reactividad, que, en esencia es un
enfoque declarativo. De esta forma, podemos definir la relacin entre los objetos una sola vez y
podemos estar seguros de que se mantendrn siempre sincronizados, en vez de tener que especificar
el comportamiento de cada uno de los posibles cambios.
Este es un concepto realmente poderoso, ya que, en un sistema de tiempo real puede haber muchas
entradas y todas pueden cambiar de forma impredecible. Con declarativo, queremos decir que,
cuando se renderiza HTML basado en una o varias fuentes de datos reactivos, Meteor se hace cargo de
la tarea de sincronizar las fuentes y, de forma trasparente, asumir el trabajo sucio de mantener la
interfaz de usuario actualizada.
Y todo esto, slo para acabar diciendo que, en lugar de tener que pensar en callbacks tipo observe ,
Meteor nos permite escribir:
<template name="postsList">
<ul>
{{#each posts}}
<li>{{title}}</li>
{{/each}}
</ul>
</template>
Template.postsList.helpers({
posts: function() {
return Posts.find();
}
});
Cuando cambian los datos reactivos, es Meteor el que, entre bastidores, est creando callbacks
observe()
Todas las fuentes de datos reactivas hacen un seguimiento de todas las computaciones que las usan
para poder avisarles de que su valor ha cambiado y deben volver a computarse. Para hacer esto, se
llama a la funcin invalidate() .
Generalmente, las computaciones se establecen para volver a evaluar su contenido, y esto es lo que
ocurre con las computaciones que Meteor crea para las plantillas (aunque, adems, se aade un poco
ms de magia para redibujar la pgina de manera ms eficiente). Aunque, si es necesario, se puede
tener ms control sobre lo que hace una de estas computaciones, en la prctica, no va a ser necesario
porque Meteor nos va a dar justo el comportamiento que vamos a necesitar.
Meteor.startup(function() {
Tracker.autorun(function() {
console.log('There are ' + Posts.find().count() + ' posts');
});
});
Ten en cuenta que tenemos que envolver el bloque Tracker dentro de un bloque
Meteor.startup()
para asegurarnos que solo se ejecute una vez cuando Meteor ha terminado de
El resultado es que, de forma fcil y natural, podemos escribir cdigo que usa datos reactivos,
sabiendo que, por detrs, el sistema de dependencias se encargar de todo en todo momento.
Creando posts
Hemos visto lo fcil que es crear posts llamando a Posts.insert a travs de la consola pero, no
podemos esperar que nuestros usuarios hagan lo mismo.
Necesitamos construir algn tipo de interfaz de usuario para que los usuarios creen nuevas entradas
en la aplicacin.
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
lib/router.js
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>
client/templates/includes/header.html
Configurar una ruta implica que si un usuario navega a /submit , Meteor mostrar la plantilla
postSubmit
<template name="postSubmit">
<form class="main form page">
<div class="form-group">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" id="url" type="text" value="" placeholder="Your URL" c
lass="form-control"/>
</div>
</div>
<div class="form-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" id="title" type="text" value="" placeholder="Name yo
ur post" class="form-control"/>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
</template>
client/templates/posts/post_submit.html
Aqu hay un montn de markup, pero es solo porque usamos el CSS de Twitter Bootstrap. Aunque slo
son esenciales los elementos del formulario, el marcado adicional ayudar a que nuestra aplicacin
se vea un poco mejor. Ahora debera tener un aspecto similar a este:
Es un simple formulario. No tenemos que preocuparnos de programar una accin para l, porque
interceptaremos su evento submit y actualizaremos los datos va JavaScript. (No tiene sentido
proporcionar un fallback no-JS si tenemos en cuenta que Meteor no funciona con JavaScript
desactivado).
Creando posts
Vamos a enlazar un controlador de eventos al evento submit del formulario. Es mejor usar el evento
submit
(en lugar de un click en un botn), ya que cubrir todas las posibles formas de envo (como
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
post._id = Posts.insert(post);
Router.go('postPage', post);
}
});
client/templates/posts/post_submit.js
Commit 7-1
Nueva pgina de envo y enlace a ella desde la cabecera.
Ver en GitHub
Lanzar instancia
Esta funcin utiliza jQuery para analizar los valores de los distintos campos del formulario y rellenar
un objeto post con los resultados. Tenemos que asegurarnos de usar preventDefault para que el
navegador no intente enviar el formulario si volvemos atrs o adelante despus.
Al final, podemos dirigirnos a la pgina de nuestro nuevo post. La funcin insert() devuelve el
identificador _id del objeto que se ha insertado en la base de datos, que podemos pasar a la funcin
go()
El resultado es que el usuario pulsa en submit , se crea un nuevo post, y vamos inmediatamente a la
pgina de discusin de ese nuevo post.
Tal como est ahora, cualquiera que visite la web puede crear posts. Para evitarlo, debemos hacer que
los usuarios inicien sesin. Podramos ocultar el nuevo formulario, pero an as, se podra seguir
haciendo desde la consola.
Afortunadamente, Meteor gestiona la seguridad de las colecciones de la forma adecuada, lo que
ocurre es que, por defecto, esta caracterstica viene desactivada. Esto es as para permitirnos empezar
con facilidad a construir la aplicacin, dejando las cosas aburridas para ms tarde.
Es el momento de eliminar el paquete insecure :
Terminal
Despus de hacerlo, nos damos cuenta de que el formulario de posts ya no funciona. Esto es as,
porque sin el paquete insecure , no se permiten inserciones en la coleccin de posts desde el lado del
cliente.
Necesitamos escribir reglas explcitas para decirle a Meteor qu usuarios pueden insertar posts o
hacer que las inserciones se hagan en el lado del servidor.
lib/collections/posts.js
Commit 7-2
Eliminado el paquete `insecure` y permitido aadir posts
Ver en GitHub
Lanzar instancia
Llamamos a Posts.allow , que le dice a Meteor que se trata de un conjunto de circunstancias en las
que a los clientes se les permite hacer cosas en la coleccin de Posts . En este caso, estamos
diciendo: a los clientes se les permite insertar posts siempre y cuando tengan un userId .
El userId que realiza la modificacin se pasa a las funciones allow y deny (o devuelve null si no
hay ningn usuario conectado). Como las cuentas de usuario forman parte del ncleo de Meteor,
podemos confiar en que el userId siempre ser el correcto.
Nos las hemos arreglado para asegurarnos de que un usuario tiene que estar registrado para crear un
mensaje. Salimos de la sesin e intentamos crear un post para ver lo que sale por la consola del
navegador:
Sin embargo, todava tenemos que tratar con unas cuantas cosas:
Los usuarios que no han iniciado sesin an pueden ver el formulario.
El post no est vinculado al usuario de ninguna forma.
Se pueden crear mltiples posts que apunten a la misma URL.
Vamos a corregir estos problemas.
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
var requireLogin = function() {
if (! Meteor.user()) {
this.render('accessDenied');
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
lib/router.js
<template name="accessDenied">
<div class="access-denied page jumbotron">
<h2>Access Denied</h2>
<p>You can't get here! Please log in.</p>
</div>
</template>
client/templates/includes/access_denied.html
Commit 7-3
Acceso denegado al envo de posts a usuarios no registrados.
Ver en GitHub
Lanzar instancia
Lo bueno de las acciones del router es que son reactivas. Esto significa que no necesitamos pensar en
funciones de retorno cuando el usuario se autentica: cuando el estado de autenticacin del usuario
cambia, la plantilla del Router cambia instantneamente de accessDenied a postSubmit sin tener
que escribir explcitamente cdigo para manejarlo (y adems, esto funciona incluso en las otras
pestaas del navegador).
Iniciemos sesin, y vayamos a la pgina para crear un nuevo post. Ahora actualizar la pgina en el
navegador. Veremos que, por un instante, se ve la plantilla accessDenied antes de que aparezca el
formulario. Esto es porque Meteor empieza a mostrar las plantillas tan pronto como sea posible, antes
de haber hablado con el servidor y comprobado si el usuario existe.
Para evitar este problema (que es uno de los ms comunes que nos podemos encontrar cuando
tratamos de lidiar con la latencia entre el cliente y el servidor), solo mostraremos una pantalla de
espera durante un instante en el que esperamos para ver si el usuario tiene acceso o no.
Despus de todo en este momento no sabemos si el usuario tiene acceso y no podemos mostrar
ninguna de las plantillas, ya sea la de accessDenied o la de postSubmit hasta que lo sepamos.
As que vamos a modificar nuestra accin para aadir la plantilla de espera mientras
Meteor.loggingIn()
//...
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'});
lib/router.js
Commit 7-4
Mostrar la pantalla de carga mientras esperamos al login.
Ver en GitHub
Lanzar instancia
Ocultando el enlace
La forma fcil de evitar que los usuarios lleguen al formulario es esconder el enlace. Podemos hacerlo
fcilmente desde header.html :
//...
<ul class="nav navbar-nav">
{{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{
/if}}
</ul>
//...
client/templates/includes/header.html
Commit 7-5
No mostrar el enlace a la pgina de envo si el usuario n
Ver en GitHub
Lanzar instancia
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return alert(error.reason);
Router.go('postPage', {_id: result._id});
});
}
});
client/templates/posts/post_submit.js
Comprobaciones de seguridad
Aprovecharemos esta oportunidad para aadir algo de seguridad a nuestros mtodos usando el
paquete audit-argument-checks .
Este paquete nos permite realizar comprobaciones sobre un objeto JavaScript usando patrones
predefinidos. En nuestro caso, lo usaremos para comprobar que el usuario que est invocando el
mtodo est correctamente autenticado (asegurndonos que Meteor.userId() es de tipo String ),
y que el objeto postAttributes pasado como argumento al mtodo contiene las cadenas title y
url
lib/collections/posts.js
Fjate que el mtodo _.extend() forma parte de la librera Underscore, que simplemente nos
permite extender un objeto con propiedades de otro.
Commit 7-6
Usando un mtodo para enviar un post.
Ver en GitHub
Lanzar instancia
Adis Allow/Deny
Los mtodos Meteor son ejecutados en el servidor, por lo que Meteor supone que son de
confianza. Por tanto, los mtodos Meteor obvian las llamadas a allow y deny .
Si quieres ejecutar algn cdigo antes de cada operacin de insert , update , o remove
incluso en el lado servidor, te sugerimos echar un vistazo al paquete collection-hooks.
Evitando duplicidades
Vamos a hacer una comprobacin ms antes de dar por bueno nuestro mtodo. Si ya tenemos un
post con la misma URL, no vamos a permitir que se aada una segunda vez, por el contrario,
redirijamos al usuario al post ya existente.
Meteor.methods({
postInsert: function(postAttributes) {
check(this.userId, String);
check(postAttributes, {
title: String,
url: String
});
var postWithSameLink = Posts.findOne({url: postAttributes.url});
if (postWithSameLink) {
return {
postExists: true,
_id: postWithSameLink._id
}
}
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
var postId = Posts.insert(post);
return {
_id: postId
};
}
});
lib/collections/posts.js
Buscamos en nuestra base de datos las URLs duplicadas. Si se encuentra alguna, devolvemos
( return ) el _id del post junto con una marca postExists: true para informar al cliente sobre
esta situacin especial.
Y como estamos lanzando una llamada return , el mtodo se detiene en este punto sin llegar a
ejecutar la sentencia insert , evitndonos elegantemente cualquier duplicidad.
Slo falta usar postExists en nuestro ayudante de eventos en el lado del cliente para mostrarnos un
mensaje de aviso:
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return alert(error.reason);
client/templates/posts/post_submit.js
Commit 7-7
Forzando la unicidad de las URLs.
Ver en GitHub
Lanzar instancia
descendentes:
Template.postsList.helpers({
posts: function() {
return Posts.find({}, {sort: {submitted: -1}});
}
});
client/templates/posts/posts_list.js
Commit 7-8
Posts ordenados por fecha de envo.
Ver en GitHub
Lanzar instancia
Ha costado, pero Finalmente tenemos una interfaz en la que los usuarios introducen posts de forma
segura en nuestra aplicacin!
Sin embargo, cualquier aplicacin que permita a los usuarios crear contenido tambin debe permitir
editarla o borrarla. Eso es de lo que hablaremos en el siguiente captulo.
Compensacin de la latencia
SIDEBAR
7.5
Un mtodo es una forma de ejecutar una serie de comandos en el servidor de una manera
estructurada. En nuestro ejemplo, hemos utilizado un mtodo porque queramos asegurarnos que los
nuevos posts se etiqueten con el nombre e identificador de su autor y con la hora actual del servidor.
Sin embargo, tendramos un problema si Meteor ejecutara los mtodos de una forma ms bsica.
Considera la siguiente secuencia de eventos (nota: las marcas de tiempo son valores aleatorios
Compensacin de la latencia
Para evitar este problema, Meteor introduce un concepto llamado Compensacin de la Latencia.
Cuando definimos nuestro mtodo Post , lo colocamos dentro de un archivo en el directorio
collections/
. Esto implica que estar disponible tanto para el servidor como para el cliente - y se
collections/posts.js
Si nos detuviramos aqu, la demostracin no sera muy concluyente. En este punto, solo parece que
el envo del formulario est siendo pausado por cinco segundos antes de redirigirte a la lista principal
de posts, y nada ms.
Para entender porqu, volvamos a ver el ayudante de envo:
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return alert(error.reason);
client/templates/posts/post_submit.js
Pero para el propsito de este ejemplo, queremos ver el resultado de nuestras acciones
inmediatamente. Por lo que cambiaremos la llamada a Router para redirigir a la ruta postsList (no
podemos redirigir al post porque no sabemos su identificador fuera del mtodo). Lo sacaremos fuera
de la llamada, y veremos que pasa:
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return alert(error.reason);
client/templates/posts/post_submit.js
Commit 7-5-1
Demostrar el orden en el que aparecen los posts usando un
Ver en GitHub
Lanzar instancia
Si ahora creamos un nuevo post, vemos claramente la compensacin de la latencia. En primer lugar,
se inserta el post con el ttulo (cliente) (El primer post de la lista, apuntando hacia GitHub):
Cinco segundos ms tarde, se reemplaza con el documento real insertado por el servidor:
Nuestro post una vez que el cliente recibe la actualizacin del servidor
cosas:
1. Comprueba si podemos hacer el cambio llamando a los callbacks allow y deny (sin embargo,
esto no tiene porque ser as en la simulacin).
2. Efecta, de verdad, el cambio en el almacn de datos subyacente.
insert
del servidor, porque esperamos que sea la versin de post que hay en el servidor la que lo
haga.
En consecuencia, cuando el mtodo post del servidor llama a insert no tiene necesidad de
preocuparse por la simulacin, y la insercin se realiza sin problemas.
Como anteriormente, no olvides deshacer los cambios antes de avanzar al siguiente captulo.
Editando posts
Ahora que ya podemos crear posts, el siguiente paso es poder editarlos y borrarlos. Como el cdigo de
la IU ha quedado bastante simple, este parece un buen momento para hablar de cmo se gestionan
los permisos de usuario con Meteor.
Primero vamos a configurar nuestro router. Aadiremos una ruta para acceder a la pgina de edicin y
estableceremos su contexto de datos:
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
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'});
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'});
lib/router.js
<template name="postEdit">
<form class="main form page">
<div class="form-group">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" id="url" type="text" value="{{url}}" placeholder="Your
URL" class="form-control"/>
</div>
</div>
<div class="form-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" id="title" type="text" value="{{title}}" placeholder
="Name your post" class="form-control"/>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary submit"/>
<hr/>
<a class="btn btn-danger delete" href="#">Delete post</a>
</form>
</template>
client/templates/posts/post_edit.html
Template.postEdit.events({
'submit form': function(e) {
e.preventDefault();
var currentPostId = this._id;
var postProperties = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
}
Posts.update(currentPostId, {$set: postProperties}, function(error) {
if (error) {
// display the error to the user
alert(error.reason);
} else {
Router.go('postPage', {_id: currentPostId});
}
});
},
'click .delete': function(e) {
e.preventDefault();
if (confirm("Delete this post?")) {
var currentPostId = this._id;
Posts.remove(currentPostId);
Router.go('postsList');
}
}
});
client/templates/posts/post_edit.js
Aadiendo enlaces
Deberamos aadir enlaces a la pgina de edicin de nuestros posts para que los usuarios puedan
llegar a ella:
<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
submitted by {{author}}
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>
client/templates/posts/post_item.html
Por supuesto, no queremos que se muestre el enlace para editar un post que no haya sido creado por
ese usuario. Aqu es donde entra el ayudante ownPost :
Template.postItem.helpers({
ownPost: function() {
return this.userId === Meteor.userId();
},
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
}
});
client/templates/posts/post_item.js
Formulario de edicin.
Commit 8-1
Aadido el formulario de edicin.
Ver en GitHub
Lanzar instancia
Ya tenemos nuestro formulario de envo de edicin, pero en realidad, todava no se puede editar
nada. Qu es lo que est pasando?
dentro de lib . Esto nos asegura que nuestra lgica de permisos se cargar lo
lib/permissions.js
En el captulo Creando posts nos libramos de tener que usar el mtodo allow() porque estbamos
insertando nuevos posts a travs de un mtodo de servidor.
Pero ahora que estamos editando y borrando posts desde el cliente, vamos a necesitar volver a
collections/posts.js
//...
lib/collections/posts.js
Commit 8-2
Aadidos permisos bsicos para comprobar el dueo del post.
Ver en GitHub
Lanzar instancia
lib/collections/posts.js
Commit 8-3
Permitir cambios slo en ciertos campos.
Ver en GitHub
Lanzar instancia
Estamos cogiendo el array fieldNames que contiene la lista de los campos que quieren modificar, y
usamos el mtodo without() de Underscore para devolver un sub-array que contiene los campos
que no son url o title .
Si todo va bien, el array debe estar vaco y su longitud debe ser 0. Pero si alguien est tratando de
enredar, la longitud del array ser mayor que 0, y devolveremos true (denegando as la
actualizacin).
Te habrs dado cuenta de que, en el cdigo de edicin del post, no comprobamos si hay enlaces
duplicados. Esto significa que un usuario podra enviar un enlace y despus editarlo y cambiar su URL
para saltarse la comprobacin. La solucin a este problema podra ser utilizar un mtodo Meteor para
tratar este formulario de edicin, pero dejaremos esto como un ejercicio para el lector.
y deny , por lo general es ms fcil de hacer las cosas directamente desde el cliente.
Sin embargo, tan pronto como empecemos a hacer cosas que deberan estar fuera del
control del usuario (por ejemplo, el timestamp de un nuevo post o asignarlo al usuario
correcto), ser mejor que usemos mtodos.
Las llamadas a mtodos tambin son apropiadas en algunos otros casos:
Cuando necesitamos conocer o devolver valores a travs de un callback en lugar de
esperar a que Meteor propague la sincronizacin.
Para consultas pesadas a la base de datos.
Para resumir o agregar datos (por ejemplo, contadores, promedios, sumas).
Para conocer ms a fondo este tema echa un vistazo a nuestro blog.
Permitir y Denegar
SIDEBAR
8.5
El sistema de seguridad de Meteor nos permite controlar los cambios en la base de datos sin tener
que definir los mtodos necesarios para hacerlo.
Nosotros hemos tenido que definir un mtodo post especfico porque necesitamos hacer tareas
adicionales, tales como decorar el post con propiedades adicionales y tomar medidas si la URL del
post ya existe.
Por otro lado, tampoco hemos tenido que crear mtodos para actualizar y eliminar posts. Solo
necesitbamos comprobar si el usuario tena permiso para hacer la accin, y ha sido muy fcil usando
los callbacks allow y deny .
Usar estos callbacks nos ha permitido ser ms declarativos con las modificaciones en la base de
datos, definiendo qu tipo de cambios se pueden hacer. El hecho de que estn integrados en el
sistema de cuentas es, adems, una ventaja aadida.
Mltiples callbacks
Podemos definir todos los callbacks allow que queramos. Solo es necesario que, al menos uno de
ellos devuelva true , para el cambio actual. De esta forma, cuando se llama a Posts.insert desde
un navegador (no importa si es desde el cdigo cliente de nuestra aplicacin o desde la consola), el
servidor llamar a todos los insert -permitidos que pueda hasta que encuentre uno que devuelva
true. Si no encuentra ninguno, no permite la insercin, y se devuelve al cliente un error 403 .
Del mismo modo, podemos definir uno o varios callbacks deny . Si cualquiera de ellos devuelve
true
, el cambio se cancela y se devuelve un 403 . La lgica de todo esto implica que, para que un
insert
tenga xito, se ejecutarn uno o varios callbacks allow as como todos los deny .
En otras palabras, Meteor recorre la lista de callbacks empezando por los deny , y luego ejecuta todos
los allow hasta que uno devuelve true .
Un ejemplo prctico de este patrn podra ser, por ejemplo, tener dos callbacks allow() , uno
comprueba si un post pertenece al usuario actual, y otro si el usuario actual es un administrador. Si es
un administrador, se asegura que el usuario ser capaz de actualizar cualquier post, ya que al menos
uno de los callbacks devolver true.
Compensacin de la latencia
Recuerda que los mtodos que provocan cambios en la base de datos (como .update() ) compensan
la latencia, igual que cualquier otro mtodo. As, por ejemplo, si desde la consola del navegador,
intentas eliminar un post que no te pertenece, vers que, por un momento, el post desaparece,
porque la coleccin local pierde el documento, pero luego vuelve a aparecer cuando el servidor nos
dice que no, que el documento no ha sido eliminado.
Por supuesto, este comportamiento no es un problema cuando se activa desde la consola (despus
de todo, si los usuarios juegan con los datos desde la consola, no es problema nuestro lo que ocurra
en sus navegadores). Sin embargo, necesitas asegurarte de que esto no sucede desde la interfaz de
usuario. Por ejemplo, necesitas tomarte la molestia de asegurar que no muestras a los usuarios
botones para eliminar documentos que no estn autorizados a borrar.
Afortunadamente, no suele requerir demasiado cdigo extra poner el cdigo que define los permisos,
compartido entre el cliente y el servidor (por ejemplo, podras escribir una funcin
canDeletePost(user, post)
Errores
Resulta poco elegante usar un alert() para advertir al usuario de que algo ha pasado. Hay que
hacerlo mejor.
Vamos a construir un mecanismo de presentacin de errores ms verstil y que va a hacer mejor el
trabajo de informar al usuario sobre lo que est pasando sin tener que romper su flujo de trabajo.
Vamos a implementar un simple sistema que muestre los nuevos errores en la parte de arriba a la
derecha de la ventana, de forma similar a la popular aplicacin Growl de MacOS.
client/helpers/errors.js
Ahora que hemos creado la coleccin, podemos agregar una funcin throwError , que usaremos
para aadir errores. Al tratarse de una coleccin local, no tenemos que preocuparnos por definir
allow
actual.
o deny u otros mecanismos de seguridad, ya que esta coleccin es local para el usuario
throwError = function(message) {
Errors.insert({message: message});
};
client/helpers/errors.js
La ventaja de utilizar una coleccin local para almacenar los errores es que, como todas las
colecciones, es reactiva lo que significa que podemos mostrar errores de la misma forma que
mostramos cualquier otro dato de una coleccin.
Mostrando Errores
Mostraremos los errores en la parte de arriba de nuestro layout:
<template name="layout">
<div class="container">
{{> header}}
{{> errors}}
<div id="main">
{{> yield}}
</div>
</div>
</template>
client/templates/application/layout.html
<template name="errors">
<div class="errors">
{{#each errors}}
{{> error}}
{{/each}}
</div>
</template>
<template name="error">
<div class="alert alert-danger" role="alert">
<button type="button" class="close" data-dismiss="alert">×</button>
{{message}}
</div>
</template>
client/templates/includes/errors.html
Template.errors.helpers({
errors: function() {
return Errors.find();
}
});
client/templates/includes/errors.js
Ya puedes probar nuestros mensajes de error manualmente. Abre una consola en el navegador y
escribe:
throwError("I'm an error!");
Commit 9-1
Mostrando errores.
Ver en GitHub
Lanzar instancia
Creando errores
Ya sabemos cmo mostrar los errores, pero todava tenemos que crearlos antes de poder verlos. Ya
hemos implementado un buen escenario para mostrarlos: el aviso de post duplicado. Sencillamente
remplazaremos las llamadas a alert en el ayudante de eventos de postSubmit con la nueva
funcin throwError que acabamos de crear:
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return throwError(error.reason);
client/templates/posts/post_submit.js
Template.postEdit.events({
'submit form': function(e) {
e.preventDefault();
var currentPostId = this._id;
var postProperties = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
}
Posts.update(currentPostId, {$set: postProperties}, function(error) {
if (error) {
// display the error to the user
throwError(error.reason);
} else {
Router.go('postPage', {_id: currentPostId});
}
});
},
//...
});
client/templates/posts/post_edit.js
Commit 9-2
Usando el mecanismo de presentar errores.
Ver en GitHub
Lanzar instancia
Vamos a probar: intenta crear un post e introduce la URL http://meteor.com . Como esta URL ya
existe en un post en nuestros datos de ejemplo, deberamos ver:
Disparando un error
@keyframes fadeOut {
0% {opacity: 0;}
10% {opacity: 1;}
90% {opacity: 1;}
100% {opacity: 0;}
}
//...
.alert {
animation: fadeOut 2700ms ease-in 0s 1 forwards;
//...
}
client/stylesheets/style.css
Estamos definiendo una animacin CSS fadeOut que especifica cuatro keyframes para la propiedad
opacidad (al 0%, 10%, 90% y 100% del total de la duracin de la animacin), y aplicando esta
animacin a la clase alert .
La animacin se ejecuta durante 2700 milisegundos en total, usa la ecuacin de tiempo ease-in , se
inicia con un retardo de 0 segundos, se ejecuta una sola vez, y finalmente permanece en el ltimo
keyframe una vez que se ha terminado de ejecutar.
Esto funciona, pero si lanzamos varios errores (enviando el mismo enlace tres veces por ejemplo)
notars que se apilarn uno encima de otro:
Stack overflow.
Esto pasa porque mientras los elementos .alert estn desapareciendo visualmente, todava estn
presentes en el DOM. Necesitamos corregir esto.
Esta es la clase de situaciones en las que Meteor reluce. Puesto que la coleccin Errors es reactiva,
Todo lo que necesitamos para deshacernos de estos antiguos errores es eliminarlos de la coleccin!
Usaremos Meteor.setTimeout para especificar una funcin que se ejecute despus de que se expire
el tiempo de espera (en este caso, 3000 milisegundos).
Template.errors.helpers({
errors: function() {
return Errors.find();
}
});
Template.error.onRendered(function() {
var error = this.data;
Meteor.setTimeout(function () {
Errors.remove(error._id);
}, 3000);
});
client/templates/includes/errors.js
Commit 9-3
Limpiar errores despus de 3 segundos.
Ver en GitHub
Lanzar instancia
La llamada a onRendered se lanza una vez que nuestra plantilla ha sido renderizada en el navegador.
Dentro de esta llamada, this hace referencia a la instancia actual de la plantilla, y this.data da
acceso a los datos del objeto que est siendo renderizado (en nuestro caso, un error).
Para comenzar, vamos a preparar nuestra plantilla postSubmit para aceptar este nuevo tipo de
ayudantes:
<template name="postSubmit">
<form class="main form page">
<div class="form-group {{errorClass 'url'}}">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" id="url" type="text" value="" placeholder="Your URL" c
lass="form-control"/>
<span class="help-block">{{errorMessage 'url'}}</span>
</div>
</div>
<div class="form-group {{errorClass 'title'}}">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" id="title" type="text" value="" placeholder="Name yo
ur post" class="form-control"/>
<span class="help-block">{{errorMessage 'title'}}</span>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
</template>
client/templates/posts/post_submit.html
Fjate que estamos pasando parmetros ( url y title respectivamente) a cada ayudante. Esto nos
permite reutilizar el mismo ayudante, modificando su comportamiento segn el parmetro.
Ahora la parte divertida: hacer que estos ayudantes hagan realmente algo.
Usaremos la Session para almacenar un objeto postSubmitErrors que contendr el potencial
mensaje de error. Segn el usuario interacta con el formulario, este objeto ir cambiando, por lo que
ir cambiando reactivamente el estilo y contenido del formulario.
Primero, iniciamos el objeto donde se crea la plantilla postSubmit . Esto nos asegura que el usuario
no ve un mensaje de error antiguo que se haya quedado de una visita anterior a esta pgina.
Despus definimos dos ayudantes de plantilla. Ambos buscarn la propiedad field del objeto
Session.get('postSubmitErrors')
Template.postSubmit.onCreated(function() {
Session.set('postSubmitErrors', {});
});
Template.postSubmit.helpers({
errorMessage: function(field) {
return Session.get('postSubmitErrors')[field];
},
errorClass: function (field) {
return !!Session.get('postSubmitErrors')[field] ? 'has-error' : '';
}
});
//...
client/templates/posts/post_submit.js
Puedes comprobar que nuestros ayudantes estn funcionando correctamente abriendo una consola
en el navegador y escribiendo la siguiente lnea de cdigo:
//...
validatePost = function (post) {
var errors = {};
if (!post.title)
errors.title = "Please fill in a headline";
if (!post.url)
errors.url =
return errors;
}
//...
lib/collections/posts.js
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
var errors = validatePost(post);
if (errors.title || errors.url)
return Session.set('postSubmitErrors', errors);
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return throwError(error.reason);
client/templates/posts/post_submit.js
Fjate que estamos usando return para abortar la ejecucin del ayudante si hay errores, no porque
queramos devolver ningn valor concreto.
Capturando errores.
Meteor.methods({
postInsert: function(postAttributes) {
check(this.userId, String);
check(postAttributes, {
title: String,
url: String
});
var errors = validatePost(postAttributes);
if (errors.title || errors.url)
throw new Meteor.Error('invalid-post', "You must set a title and URL for you
r post");
var postWithSameLink = Posts.findOne({url: postAttributes.url});
if (postWithSameLink) {
return {
postExists: true,
_id: postWithSameLink._id
}
}
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
var postId = Posts.insert(post);
return {
_id: postId
};
}
});
lib/collections/posts.js
De nuevo, los usuarios no deberan nunca ver este mensaje You must set a title and URL for your
post. Slo se mostrar si alguien quiere saltarse los controles que hemos dispuesto
concienzudamente, y usa la consola del navegador.
Para probarlo, abre una consola del navegador y prueba a enviar un post sin URL:
Si hemos hecho bien nuestro trabajo, obtendrs un montn de cdigo extrao junto con un mensaje
You must set a title and URL for your post.
Commit 9-4
Validar el contenido de la post al enviar.
Ver en GitHub
Lanzar instancia
Validacin al editar
Pare redondear las cosas, aplicaremos la misma validacin a nuestro formulario de edicin de posts.
El cdigo es muy similar. Primero, la plantilla:
<template name="postEdit">
<form class="main form page">
<div class="form-group {{errorClass 'url'}}">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" id="url" type="text" value="{{url}}" placeholder="Your
URL" class="form-control"/>
<span class="help-block">{{errorMessage 'url'}}</span>
</div>
</div>
<div class="form-group {{errorClass 'title'}}">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" id="title" type="text" value="{{title}}" placeholder
="Name your post" class="form-control"/>
<span class="help-block">{{errorMessage 'title'}}</span>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary submit"/>
<hr/>
<a class="btn btn-danger delete" href="#">Delete post</a>
</form>
</template>
client/templates/posts/post_edit.html
Template.postEdit.onCreated(function() {
Session.set('postEditErrors', {});
});
Template.postEdit.helpers({
errorMessage: function(field) {
return Session.get('postEditErrors')[field];
},
errorClass: function (field) {
return !!Session.get('postEditErrors')[field] ? 'has-error' : '';
}
});
Template.postEdit.events({
'submit form': function(e) {
e.preventDefault();
var currentPostId = this._id;
var postProperties = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
}
var errors = validatePost(postProperties);
if (errors.title || errors.url)
return Session.set('postEditErrors', errors);
Posts.update(currentPostId, {$set: postProperties}, function(error) {
if (error) {
// display the error to the user
throwError(error.reason);
} else {
Router.go('postPage', {_id: currentPostId});
}
});
},
'click .delete': function(e) {
e.preventDefault();
if (confirm("Delete this post?")) {
var currentPostId = this._id;
Posts.remove(currentPostId);
Router.go('postsList');
}
}
});
client/templates/posts/post_edit.js
Tal y como hicimos en el formulario de envo de posts, queremos validar nuestros posts en el servidor.
Excepto que recordars que no estamos usando un mtodo para editar los posts, si no una llamada
update
Esto significa que tenemos que aadir una nueva regla deny :
//...
Posts.deny({
update: function(userId, post, fieldNames, modifier) {
var errors = validatePost(modifier.$set);
return errors.title || errors.url;
}
});
//...
lib/collections/posts.js
Fjate que el argumento post se refiere al post existente. En este caso, queremos validar la
actualizacin, que es por lo que estamos llamando a validatePost con el contenido del modificador
de la propiedad $set (como en Posts.update({$set: {title: ..., url: ...}}) ).
Esto funciona porque modifier.$set contiene las mismas dos propiedades title y url que el
objeto post . Por supuesto, esto quiere decir que cualquier actualizacin parcial que afecte solo a
title
Te habrs dado cuenta de que esta es nuestra segunda llamada deny . Cuando aadimos mltiples
llamadas deny , la operacin fallar si uno de ellos devuelve true . En este caso, esto significa que el
update
solo ser satisfactorio si estamos modificando el title y la url , y ninguna de ellas est
vaca.
Commit 9-5
Validar el contenido del post al editar.
Ver en GitHub
Lanzar instancia
SIDEBAR
9.5
Durante nuestro trabajo en los errores, hemos construido un modelo reutilizable, por qu no ponerlo
dentro de un paquete y compartirlo con el resto de la comunidad Meteor?
Para empezar, tenemos que asegurarnos de que tenemos una cuenta de desarrollador Meteor.
Puedes hacerte una en meteor.com, pero es muy probable que ya lo hayas hecho cuando te
registraste para el libro! En cualquier caso, deberas saber cul es tu nombre de usuario, porque lo
utilizaremos bastante durante este captulo.
En este captulo usaremos tmeasday como usuario puedes cambiarlo por el tuyo.
En primer lugar, necesitamos crear una estructura de carpetas para nuestro paquete. Podemos usar el
comando meteor create --package tmeasday:errors para conseguirla. Fjate que Meteor ha
creado una carpeta llamada packages/tmeasday:errors/ , con algunos ficheros dentro.
Comenzaremos por editar package.js , el archivo que informa a Meteor de cmo debe utilizar el
paquete, y los smbolos y funciones que exporta.
Package.describe({
name: "tmeasday:errors",
summary: "A pattern to display application errors to the user",
version: "1.0.0"
});
Package.onUse(function (api, where) {
api.versionsFrom('0.9.0');
api.use(['minimongo', 'mongo-livedata', 'templating'], 'client');
api.addFiles(['errors.js', 'errors_list.html', 'errors_list.js'], 'client');
if (api.export)
api.export('Errors');
});
packages/tmeasday:errors/package.js
Cuando desarrollamos un paquete para su uso en el mundo-real, es una buena prctica rellenar la
seccin Package.describe con la URL del repositorio Git (como por ejemplo,
https://github.com/tmeasday/meteor-errors.git
al cdigo fuente, y (si usas GitHub) ver el README del paquete en Atmosphere.
Vamos a aadir al paquete los tres archivos que se pasan en la llamada a add_files . Podemos usar
los mismos que tenemos para Microscope, haciendo solo, unos pequeos cambios para los espacios
de nombres y para dejar la API un poco ms limpia:
Errors = {
// Local (client-only) collection
collection: new Mongo.Collection(null),
throw: function(message) {
Errors.collection.insert({message: message, seen: false})
}
};
packages/tmeasday:errors/errors.js
<template name="meteorErrors">
<div class="errors">
{{#each errors}}
{{> meteorError}}
{{/each}}
</div>
</template>
<template name="meteorError">
<div class="alert alert-danger" role="alert">
<button type="button" class="close" data-dismiss="alert">×</button>
{{message}}
</div>
</template>
packages/tmeasday:errors/errors_list.html
Template.meteorErrors.helpers({
errors: function() {
return Errors.collection.find();
}
});
Template.meteorError.rendered = function() {
var error = this.data;
Meteor.setTimeout(function () {
Errors.collection.remove(error._id);
}, 3000);
};
packages/tmeasday:errors/errors_list.js
paquete:
rm client/helpers/errors.js
rm client/templates/includes/errors.html
rm client/templates/includes/errors.js
Otra cosa que debemos hacer son algunos pequeos cambios en el cdigo de la aplicacin para que
use la API correcta:
{{> header}}
{{> meteorErrors}}
client/templates/application/layout.html
client/templates/posts/post_submit.js
client/templates/posts/post_edit.js
Commit 9-5-1
Creado y enlazado un paquete bsico.
Ver en GitHub
Lanzar instancia
Una vez hechos estos cambios, deberamos ver el mismo comportamiento que tenamos con el
cdigo sin empaquetar.
Escribiendo Tests
El primer paso en el desarrollo de un paquete es probarlo contra una aplicacin, pero el siguiente es
escribir un conjunto de tests que evalen adecuadamente el comportamiento del paquete. Meteor
incluye Tinytest, que permite ejecutar este tipo de pruebas de forma fcil y, de esta forma, tener la
conciencia tranquila cuando compartimos el paquete con los dems.
Vamos a crear un archivo que usa Tinytest para ejecutar tests contra el cdigo de los errores.
packages/tmeasday:errors/errors_tests.js
Con estos tests comprobamos que las funciones bsicas de Meteor.Errors funcionan
correctamente, as como que el cdigo mostrado en la plantilla sigue funcionando bien.
No vamos a cubrir los aspectos especficos sobre cmo escribir tests de paquetes (porque la API
todava no est acabada y podra cambiar mucho), pero viendo el cdigo, puedes hacerte una idea
cmo funciona.
Para decirle a Meteor que ejecute los tests, aadimos este cdigo a package.js
Package.onTest(function(api) {
api.use('tmeasday:errors', 'client');
api.use(['tinytest', 'test-helpers'], 'client');
api.addFiles('errors_tests.js', 'client');
});
packages/tmeasday:errors/package.js
Commit 9-5-2
Tests aadidos al paquete.
Ver en GitHub
Terminal
Lanzar instancia
Publicando el paquete
Ahora, queremos liberar el paquete y ponerlo a disposicin de todo el mundo. Para ello tendremos
que subirlo al servidor de paquetes de Meteor y, hacerlo miembro del repositorio Atmosphere.
Afortunadamente, es muy fcil. Entramos en el directorio del paquete, y ejecutamos meteor publish
--create
cd packages/tmeasday:errors
meteor publish --create
Terminal
Ahora que hemos publicado el paquete, podemos eliminarlo del proyecto y luego aadirlo de nuevo
directamente:
rm -r packages/errors
meteor add tmeasday:errors
Commit 9-5-4
Paquete eliminado del rbol de desarrollo.
Ver en GitHub
Lanzar instancia
Ahora debemos ver a Meteor descargar nuestro paquete por primera vez. Bien hecho!
Como de costumbre, asegrate de deshacer los cambios antes de continuar (o mantenerlos,
tenindolos en cuenta en el resto del libro).
Comentarios
El objetivo de un sitio de noticias es crear una comunidad de usuarios, y ser difcil hacerlo sin que
puedan a hablar unos con otros. En este captulo, vamos a agregar los comentarios.
Empezaremos creando una nueva coleccin para almacenar los comentarios.
lib/collections/comments.js
// Fixture data
if (Posts.find().count() === 0) {
var now = new Date().getTime();
// create two users
var tomId = Meteor.users.insert({
profile: { name: 'Tom Coleman' }
});
var tom = Meteor.users.findOne(tomId);
var sachaId = Meteor.users.insert({
profile: { name: 'Sacha Greif' }
});
var sacha = Meteor.users.findOne(sachaId);
var telescopeId = Posts.insert({
title: 'Introducing Telescope',
userId: sacha._id,
author: sacha.profile.name,
url: 'http://sachagreif.com/introducing-telescope/',
submitted: new Date(now - 7 * 3600 * 1000)
});
Comments.insert({
postId: telescopeId,
userId: tom._id,
author: tom.profile.name,
submitted: new Date(now - 5 * 3600 * 1000),
body: 'Interesting project Sacha, can I get involved?'
10
});
Comments.insert({
postId: telescopeId,
userId: sacha._id,
author: sacha.profile.name,
submitted: new Date(now - 3 * 3600 * 1000),
body: 'You sure can Tom!'
});
Posts.insert({
title: 'Meteor',
userId: tom._id,
author: tom.profile.name,
url: 'http://meteor.com',
submitted: new Date(now - 10 * 3600 * 1000)
});
Posts.insert({
title: 'The Meteor Book',
userId: tom._id,
author: tom.profile.name,
url: 'http://themeteorbook.com',
submitted: new Date(now - 12 * 3600 * 1000)
});
}
server/fixtures.js
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function() {
return Comments.find();
});
server/publications.js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
}
});
lib/router.js
Commit 10-1
Aadidos comentarios a la coleccin, pub/sub y datos de p
Ver en GitHub
Lanzar instancia
Ten en cuenta que para que se carguen los nuevos datos de prueba, es necesario ejecutar meteor
reset
En primer lugar, hemos creado un par de usuarios (inventados), insertndolos en la base de datos y
usando los ids para seleccionarlos despus en la base de datos. Luego aadimos un comentario de
cada usuario al primer post, enlazando el comentario al post (con postId ), y el usuario (con
userId
Mostrando comentarios
Est bien tener comentarios en la base de datos, pero habr que mostrarlos en la pgina de discusin.
Este proceso ya nos debe ser familiar:
<template name="postPage">
<div class="post-page page">
{{> postItem}}
<ul class="comments">
{{#each comments}}
{{> commentItem}}
{{/each}}
</ul>
</div>
</template>
client/templates/posts/post_page.html
Template.postPage.helpers({
comments: function() {
return Comments.find({postId: this._id});
}
});
client/templates/posts/post_page.js
Ponemos el bloque {{#each comments}} dentro de la plantilla del post, por lo que this es un post
para el ayudante comments . Para encontrar los comentarios adecuados, buscamos los que estn
vinculados a ese post a travs de postId .
Con todo lo que hemos aprendido acerca de ayudantes y plantillas, sabemos que mostrar un
comentario es bastante sencillo. Vamos a crear un nuevo directorio comments dentro de templates
para almacenar toda la informacin acerca de los comentarios, y una nueva plantilla commentItem
dentro:
<template name="commentItem">
<li>
<h4>
<span class="author">{{author}}</span>
<span class="date">on {{submittedText}}</span>
</h4>
<p>{{body}}</p>
</li>
</template>
client/templates/comments/comment_item.html
Vamos a crear rpidamente un ayudante de plantilla para dar a nuestra fecha de envo submitted un
formato ms amigable:
Template.commentItem.helpers({
submittedText: function() {
return this.submitted.toString();
}
});
client/templates/comments/comment_item.js
<template name="postItem">
<div class="post">
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
submitted by {{author}},
<a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>
client/templates/posts/post_item.html
Template.postItem.helpers({
ownPost: function() {
return this.userId === Meteor.userId();
},
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
},
commentsCount: function() {
return Comments.find({postId: this._id}).count();
}
});
client/templates/posts/post_item.js
Commit 10-2
Mostrar los comentarios en `postPage`.
Ver en GitHub
Lanzar instancia
Ahora deberamos ser capaces de mostrar nuestros comentarios de prueba y ver algo como esto:
Displaying comments
Enviando comentarios
Vamos a aadir una forma de que los usuarios puedan hacer nuevos comentarios. El proceso ser
bastante similar a como ya hemos hecho para permitir a los usuarios crear nuevos posts.
Empezaremos aadiendo un rea de envo en la parte inferior de cada post:
<template name="postPage">
<div class="post-page page">
{{> postItem}}
<ul class="comments">
{{#each comments}}
{{> commentItem}}
{{/each}}
</ul>
{{#if currentUser}}
{{> commentSubmit}}
{{else}}
<p>Please log in to leave a comment.</p>
{{/if}}
</div>
</template>
client/templates/posts/post_page.html
<template name="commentSubmit">
<form name="comment" class="comment-form form">
<div class="form-group {{errorClass 'body'}}">
<div class="controls">
<label for="body">Comment on this post</label>
<textarea name="body" id="body" class="form-control" rows="3"></textar
ea>
<span class="help-block">{{errorMessage 'body'}}</span>
</div>
</div>
<button type="submit" class="btn btn-primary">Add Comment</button>
</form>
</template>
client/templates/comments/comment_submit.html
Template.commentSubmit.onCreated(function() {
Session.set('commentSubmitErrors', {});
});
Template.commentSubmit.helpers({
errorMessage: function(field) {
return Session.get('commentSubmitErrors')[field];
},
errorClass: function (field) {
return !!Session.get('commentSubmitErrors')[field] ? 'has-error' : '';
}
});
Template.commentSubmit.events({
'submit form': function(e, template) {
e.preventDefault();
var $body = $(e.target).find('[name=body]');
var comment = {
body: $body.val(),
postId: template.data._id
};
var errors = {};
if (! comment.body) {
errors.body = "Please write some content";
return Session.set('commentSubmitErrors', errors);
}
Meteor.call('commentInsert', comment, function(error, commentId) {
if (error){
throwError(error.reason);
} else {
$body.val('');
}
});
}
});
client/templates/comments/comment_submit.js
Al igual que anteriormente establecimos un mtodo post en el servidor, vamos a hacer lo mismo
para crear comentarios, comprobar que todo est bien, y finalmente insertar el nuevo comentario
dentro de su coleccin.
lib/collections/comments.js
Commit 10-3
Creado el formulario de envo de comentarios.
Ver en GitHub
Lanzar instancia
Comprobamos que el usuario est conectado, que el comentario tiene cuerpo, y que est vinculado a
un post.
es cuando un usuario accede a la pgina de un post individual, y solo hay que cargar el
parmetro puede cambiar en cualquier momento. As que tendremos que cambiar nuestro cdigo de
suscripcin desde el nivel de router al nivel de ruta.
Esto tiene otra consecuencia: en vez de cargar nuestros datos cuando se inicializa la aplicacin, ahora
los cargaremos cada vez que llegamos a una ruta concreta. Esto significa que ahora tendremos
tiempos de carga mientras navegamos por la aplicacin. Esto es un inconveniente inevitable a no ser
que queramos cargar siempre todos los datos.
Primero, dejaremos de pre-cargar todos los comentarios en el bloque configure , eliminando la
lnea Meteor.subscribe('comments') (dicho de otro manera, volvemos a lo que tenamos
anteriormente):
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return Meteor.subscribe('posts');
}
});
lib/router.js
//...
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return Meteor.subscribe('comments', this.params._id);
},
data: function() { return Posts.findOne(this.params._id); }
});
//...
lib/router.js
Estamos pasando this.params._id como argumento a la suscripcin. As que, utilicemos esa nueva
informacin para asegurarnos que limitamos el conjunto de datos a los comentarios que pertenecen
al post actual:
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function(postId) {
check(postId, String);
return Comments.find({postId: postId});
});
server/publications.js
Commit 10-4
Creado un mecanismo simple de publicacin/suscripcin par
Ver en GitHub
Lanzar instancia
Solo hay un problema: cuando volvemos a la pgina principal, todos nuestros mensajes tienen 0
comentarios:
Contando comentarios
La razn de que esto ocurra est bien clara: solo cargaremos comentarios en la ruta postPage , as
que cuando llamamos a Comments.find({postId: this._id}) en nuestro ayudante
commentsCount
// Fixture data
if (Posts.find().count() === 0) {
var now = new Date().getTime();
// create two users
var tomId = Meteor.users.insert({
profile: { name: 'Tom Coleman' }
});
var tom = Meteor.users.findOne(tomId);
var sachaId = Meteor.users.insert({
profile: { name: 'Sacha Greif' }
});
var sacha = Meteor.users.findOne(sachaId);
var telescopeId = Posts.insert({
title: 'Introducing Telescope',
userId: sacha._id,
author: sacha.profile.name,
url: 'http://sachagreif.com/introducing-telescope/',
submitted: new Date(now - 7 * 3600 * 1000),
commentsCount: 2
});
Comments.insert({
postId: telescopeId,
userId: tom._id,
author: tom.profile.name,
submitted: new Date(now - 5 * 3600 * 1000),
body: 'Interesting project Sacha, can I get involved?'
});
Comments.insert({
postId: telescopeId,
userId: sacha._id,
author: sacha.profile.name,
submitted: new Date(now - 3 * 3600 * 1000),
body: 'You sure can Tom!'
});
Posts.insert({
title: 'Meteor',
userId: tom._id,
author: tom.profile.name,
url: 'http://meteor.com',
submitted: new Date(now - 10 * 3600 * 1000),
commentsCount: 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
});
}
server/fixtures.js
Como de costumbre cuando actualizamos el fichero de fixtures, debers ejecutar meteor reset para
inicializar la base de datos y asegurarnos que se ejecutan de nuevo los fixtures.
Luego, nos aseguramos de que todos los nuevos posts empiezan con 0 comentarios:
//...
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date(),
commentsCount: 0
});
var postId = Posts.insert(post);
//...
lib/collections/posts.js
//...
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
//...
lib/collections/comments.js
Commit 10-5
Denormalizando el nmero de comentarios.
Ver en GitHub
Lanzar instancia
Ahora que los usuarios pueden hablar entre s, sera una lstima que se perdieran los nuevos
comentarios de otros usuarios. En el siguiente captulo veremos cmo implementar notificaciones!
Denormalizacin
SIDEBAR
10.5
Denormalizar los datos significa no almacenar esos datos de una manera normal. En otras palabras,
significa tener mltiples copias de la misma porcin de datos.
En el captulo anterior, denormalizamos la cantidad de comentarios dentro del objeto post para evitar
tener que cargar todos los comentarios todo el tiempo. Teniendo en cuenta el modelado de datos,
esto es redundante, ya que en su lugar podramos simplemente contar el nmero correcto de
comentarios en cualquier momento para averiguar su valor (dejando de lado las consideraciones de
rendimiento).
Denormalizar a menudo significa un trabajo extra para el desarrollador. En nuestro ejemplo, cada vez
que agregamos o eliminamos un comentario adems tenemos que acordarnos de actualizar el post
en cuestin para asegurarnos de que el campo commentsCount siga siendo correcto. Esto es
exactamente la razn por la cual las bases de datos relacionales como MySQL desaprueban esta
tcnica.
De todas maneras, la tcnica normal tambin tiene sus desventajas: sin una propiedad
commentsCount
, necesitaramos enviar todos los comentarios todo el tiempo tan slo para poder
contarlos, que es lo que estbamos haciendo en un principio. Denormalizar permite evitar esto
ltimo.
Por supuesto, dichas consideraciones son especficas para cada aplicacin: si ests escribiendo
cdigo donde la integridad de datos es fundamental, entonces evitar inconsistencias en los datos es
de lejos ms importante y de mayor prioridad que cualquier mejora de rendimiento.
documento ms grande.
2. allow y deny operan a nivel de documento, por consiguiente, facilita la tarea de asegurarse
que cualquier modificacin de un comentario individual es correcta. Esto sera mucho ms
complejo si operara a nivel de post.
3. DDP opera a nivel de los atributos top-level de un documento. Esto significa que si comments
fuese una propiedad de un post , cada vez que un comentario fuese creado en un post, el
servidor debera enviar toda la lista de comentarios actualizada para ese post a cada uno de los
clientes conectados.
4. Publicaciones y suscripciones son mucho ms fciles de controlar a nivel de documentos. Por
ejemplo, si quisiramos paginar comentarios en un post sera muy difcil a menos que los
comentarios estuviesen en su propia coleccin.
Mongo sugiere incrustar documentos para reducir la cantidad de consultas necesarias para buscar los
documentos. De todos modos, esto es un problema mnimo cuando se tiene en cuenta la arquitectura
de Meteor: la mayor parte del tiempo estamos consultando comentarios en el cliente, donde el acceso
a la base de datos prcticamente no tiene coste.
Notificaciones
11
Ahora que los usuarios pueden comentar los posts de otros usuarios, sera bueno hacerles saber que
alguien ha comenzado una conversacin.
Para ello, notificaremos al autor, de que ha habido un comentario en su post, y le proporcionaremos
un enlace para poder comentar.
En este tipo de funcionalidad es en la que Meteor brilla. Como por defecto, Meteor trabaja en tiempo
real, vamos a poder mostrar notificaciones instantneamente. No necesitamos esperar a que el
usuario actualice la pgina, podemos mostrar nuevas notificaciones sin tener que escribir ningn
cdigo especial.
Creando notificaciones
Crearemos una notificacin cuando alguien comente uno de nuestros posts. En el futuro, las
notificaciones podran extenderse para cubrir muchos otros escenarios, pero, por ahora ser
suficiente con esto para mantener a los usuarios informados sobre lo que est pasando.
Vamos a crear la coleccin Notifications y la funcin createCommentNotification que insertar
una notificacin para cada comentario que se haga en uno de nuestros posts.
Puesto que estamos actualizando las notificaciones desde el lado del cliente, necesitamos
asegurarnos que nuestra llamada allow es a prueba de balas. Por lo que deberemos comprobar que:
El usuario que hace la llamada update es el dueo de la notificacin modificada.
El usuario solo est intentando modificar un solo campo.
El campo a modificar es la propiedad read de nuestra notificacin.
lib/collections/notifications.js
Al igual que con los posts o los comentarios, esta coleccin estar compartida por clientes y servidor.
Como tendremos que actualizar las notificaciones cuando un usuario las haya visto, permitimos hacer
update
Tambin creamos una funcin que mira qu post est comentando el usuario, averigua qu usuario
debe ser notificado e inserta una nueva notificacin.
Ya tenemos un mtodo en el servidor para crear comentarios, por lo que podemos ampliarlo para que
llame a nuestra nueva funcin. Para guardar el _id del nuevo comentario en una variable,
cambiamos return Comments.insert(comment); , por comment._id =
Comments.insert(comment)
//...
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
lib/collections/comments.js
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function(postId) {
check(postId, String);
return Comments.find({postId: postId});
});
Meteor.publish('notifications', function() {
return Notifications.find();
});
server/publications.js
Y suscribirnos en el cliente:
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('posts'), Meteor.subscribe('notifications')]
}
});
lib/router.js
Commit 11-1
Aadida la coleccin de comentarios.
Ver en GitHub
Lanzar instancia
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
{{#if currentUser}}
<li>
<a href="{{pathFor 'postSubmit'}}">Submit Post</a>
</li>
<li class="dropdown">
{{> notifications}}
</li>
{{/if}}
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>
client/templates/includes/header.html
):
<template name="notifications">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Notifications
{{#if notificationCount}}
<span class="badge badge-inverse">{{notificationCount}}</span>
{{/if}}
<b class="caret"></b>
</a>
<ul class="notification dropdown-menu">
{{#if notificationCount}}
{{#each notifications}}
{{> notificationItem}}
{{/each}}
{{else}}
<li><span>No Notifications</span></li>
{{/if}}
</ul>
</template>
<template name="notificationItem">
<li>
<a href="{{notificationPostPath}}">
<strong>{{commenterName}}</strong> commented on your post
</a>
</li>
</template>
client/templates/notifications/notifications.html
Podemos ver que para cada notificacin, tendremos un enlace al post que ha sido comentado junto
con el usuario que lo ha hecho.
A continuacin, hay que asegurarse de que se selecciona la lista de notificaciones correcta desde
nuestro ayudante, y actualizar las notificaciones como ledas cuando el usuario hace clic en el
enlace al que apuntan.
Template.notifications.helpers({
notifications: function() {
return Notifications.find({userId: Meteor.userId(), read: false});
},
notificationCount: function(){
return Notifications.find({userId: Meteor.userId(), read: false}).count();
}
});
Template.notificationItem.helpers({
notificationPostPath: function() {
return Router.routes.postPage.path({_id: this.postId});
}
});
Template.notificationItem.events({
'click a': function() {
Notifications.update(this._id, {$set: {read: true}});
}
});
client/templates/notifications/notifications.js
Commit 11-2
Mostrar las notificaciones en la cabecera.
Ver en GitHub
Lanzar instancia
Como podemos ver, las notificaciones no son muy diferentes de los errores, y su estructura es muy
similar. Solo hay una diferencia clave: hemos creado una coleccin sincronizada cliente-servidor. Esto
significa que nuestras notificaciones son persistentes y, siempre y cuando se utilice la misma cuenta
de usuario, persistir en distintos navegadores y dispositivos.
Abre un segundo navegador, crea una nueva cuenta de usuario, y aade un comentario en un post del
usuario anterior. Deberas ver algo as:
Notifications.find().count();
1
El nuevo usuario (el que ha comentado) no debera tener notificaciones. Las que vemos son las de los
dems usuarios.
Meteor.publish('notifications', function() {
return Notifications.find({userId: this.userId, read: false});
});
server/publications.js
Commit 11-3
Sincronizar solo las notificaciones relevantes al usuario.
Ver en GitHub
Lanzar instancia
Si ahora se busca en las consolas de los dos navegadores, deberamos ver dos colecciones distintas
de notificaciones:
Notifications.find().count();
1
Notifications.find().count();
0
De hecho, la lista de notificaciones cambiar si accedes y sales de la aplicacin. Esto se debe a que las
publicaciones se republican automticamente cada vez que cambia el estado del usuario.
Nuestra aplicacin es cada vez ms funcional, y a medida que cada vez ms usuarios entran y
empiezan a publicar enlaces, corremos el riesgo de acabar con una pgina de inicio demasiado larga.
Vamos a abordar este problema en el prximo captulo: la paginacin.
Reactividad avanzada
11.5
SIDEBAR
No es comn tener que escribir cdigo de seguimiento de dependencias por ti mismo, pero para
comprender el concepto, es verdaderamente til seguir el camino de cmo funciona el flujo de
dependencias.
Imagina que quisiramos saber a cuntos amigos del usuario actual de Facebook le ha gustado cada
post en Microscope. Supongamos que ya hemos trabajado en los detalles de cmo autenticar el
usuario con Facebook, hacer las llamadas necesarias a la API, y procesar los datos relevantes. Ahora
tenemos una funcin asncrona en el lado del cliente que devuelve el nmero de me gusta:
getFacebookLikeCount(user, url, callback)
Lo importante a recordar sobre una funcin de esta naturaleza es que no es reactiva ni funciona en
tiempo real. Har una peticin HTTP a Facebook, enviando algunos datos, y obtendremos el resultado
en la aplicacin a travs de una llamada asncrona. Pero la funcin no se va a volver a ejecutar por s
sola cuando haya un cambio en Facebook, ni nuestra UI va a cambiar cuando los datos lo hagan.
Para solucionar esto, podemos comenzar utilizando setInterval para llamar a la funcin cada
ciertos segundos:
currentLikeCount = 0;
Meteor.setInterval(function() {
var postId;
if (Meteor.user() && postId = Session.get('currentPostId')) {
getFacebookLikeCount(Meteor.user(), Posts.find(postId).url,
function(err, count) {
if (!err)
currentLikeCount = count;
});
}
}, 5 * 1000);
Cada vez que usemos esa variable currentLikeCount , obtendremos el nmero correcto con un
margen de error de cinco segundos. Ahora podemos usar esa variable en un ayudante:
Template.postItem.likeCount = function() {
return currentLikeCount;
}
Sin embargo, nada le dice todava a nuestra plantilla que se redibuje cuando cambie
currentLikeCount
. Si bien la variable ahora est en pseudo tiempo real (se cambia a s misma), no
es reactiva y por lo tanto todava no puede comunicarse correctamente con el resto del ecosistema de
Meteor.
var _currentLikeCount = 0;
var _currentLikeCountListeners = new Tracker.Dependency();
currentLikeCount = function() {
_currentLikeCountListeners.depend();
return _currentLikeCount;
}
Meteor.setInterval(function() {
var postId;
if (Meteor.user() && postId = Session.get('currentPostId')) {
getFacebookLikeCount(Meteor.user(), Posts.find(postId),
function(err, count) {
if (!err && count !== _currentLikeCount) {
_currentLikeCount = count;
_currentLikeCountListeners.changed();
}
});
}
}, 5 * 1000);
En Angular, la reactividad es medida por el objeto scope . Un scope, o alcance, puede ser pensado
como un simple objeto de JavaScript con algunos mtodos especiales.
Cuando se desea depender reactivamente de un valor dentro del scope, se llama a scope.$watch ,
declarando la expresin en la que uno est interesado (por ejemplo, qu partes del scope te
importan) y una funcin que se ejecutar cada vez que esa expresin cambie. As, se puede declarar
explcitamente qu hacer cada vez que ese valor sea modificado.
Volviendo a nuestro ejemplo con Facebook, escribiramos:
$rootScope.$watch('currentLikeCount', function(likeCount) {
console.log('Current like count is ' + likeCount);
});
Por supuesto, es tan raro tener que configurar computaciones en Meteor, como tener que invocar a
$watch
Meteor.setInterval(function() {
getFacebookLikeCount(Meteor.user(), Posts.find(postId),
function(err, count) {
if (!err) {
$rootScope.currentLikeCount = count;
$rootScope.$apply();
}
});
}, 5 * 1000);
Decididamente, Meteor se ocupa de la parte ms pesada por nosotros y nos deja beneficiarnos de la
reactividad sin demasiado trabajo. Pero tal vez, aprender estos patrones ser de ayuda si alguna vez
necesitas ir ms all.
Paginacin
12
Nuestra aplicacin va tomando forma y podemos esperar un gran xito cuando todo el mundo la
conozca.
As que quizs debamos pensar un poco sobre cmo afectar al rendimiento el gran nmero de
nuevos posts que vamos a recibir.
Hemos visto antes cmo una coleccin 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 todava estamos publicando todos nuestros posts de una sola vez a todos los
usuarios conectados. Si se publicaran miles de enlaces, esto sera un problema. Para solucionarlo
tenemos que paginar nuestros posts.
// 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
});
}
}
server/fixtures.js
Despus de ejecutar meteor reset e iniciar la aplicacin de nuevo, deberamos ver algo como esto:
Commit 12-1
Aadidos suficientes posts para hacer necesaria la pagina
Ver en GitHub
Lanzar instancia
Paginacin infinita
Vamos a implementar una paginacin de estilo infinito. Lo que queremos decir con esto es que
primero mostramos, por ejemplo, 10 posts, con un enlace de cargar ms en la parte inferior. Al hacer
clic en este enlace se cargarn 10 ms, y as hasta el infinito y ms all. Esto significa que podemos
controlar todo nuestro sistema de paginacin con un solo parmetro que representa el nmero de
posts que mostraremos en pantalla.
Vamos a necesitar entonces una forma de pasar este parmetro al servidor para que sepa la cantidad
suscripcin notifications :
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('notifications')]
}
});
lib/router.js
//...
Router.route('/:postsLimit?', {
name: 'postsList',
});
//...
lib/router.js
Es importante sealar 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 ms especficas como /posts/:_id deben ir primero, y nuestra ruta
postsList
debera ir en la parte inferior del grupo de rutas para que todo coincida correctamente.
Es el momento de abordar el difcil problema de suscribirse y encontrar los datos correctos. Tenemos
que lidiar con el caso en el que el parmetro postsLimit no est presente, por lo que vamos a
asignarle un valor predeterminado. Usaremos 5, que nos dar suficiente espacio para jugar con la
paginacin.
//...
Router.route('/:postsLimit?', {
name: 'postsList',
waitOn: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
}
});
//...
lib/router.js
Ya te habrs dado cuenta de que estamos pasando un objeto JavaScript ({sort: {submitted: -1}, limit:
postsLimit}) junto con el nombre de nuestra publicacin posts . Este objeto servir como parmetro
options
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});
});
server/publications.js
Paso de parmetros
Nuestro cdigo 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 enven cualquier
opcin a travs de la consola del navegador.
En nuestro caso, esto es relativamente inofensivo, ya que todo lo que un usuario podra
hacer es reordenar los mensajes de manera diferente, o cambiar el lmite (que es lo que
queremos hacer). De todas formas una aplicacin del mundo real debera probablemente
limitar el lmite!
Afortunadamente, usando check() sabemos que los usuarios no podrn inyectar opciones
adicionales (como la opcin fields , que en algunos casos podra exponer datos privados
en los documentos).
De todas formas, un patrn ms seguro para asegurarnos el control de nuestros datos podra
ser pasar los parmetros de forma individual en lugar de todo el objeto:
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 patrn anterior y hacer que la funcin 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 implcitamente de los datos en this dentro de la
plantilla, estar disponible tambin como posts . Aparte de este pequeo elemento, el cdigo debe
sernos familiar:
//...
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})
};
}
});
//...
lib/router.js
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 :
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('notifications')]
}
});
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
lib/router.js
Commit 12-2
Aumentada la ruta `postsList` para que tenga un lmite.
Ver en GitHub
Lanzar instancia
Vamos a probar nuestro nuevo sistema de paginacin. Ahora podemos mostrar un nmero arbitrario
de posts en la pgina principal simplemente cambiando el parmetro en la URL. Por ejemplo, intenta
acceder a http://localhost:3000/3 . Deberas ver algo como esto:
10 a 20 porque solo hay diez posts en total en el conjunto de datos del lado del cliente.
Una solucin sera publicar esos 10 mensajes en el servidor y, a continuacin, hacer un
Posts.find()
Te habrs dado cuenta de que repetimos dos veces la lnea var limit =
parseInt(this.params.postsLimit) || 5;
preocupes, no es el fin del mundo, pero como siempre es mejor seguir el principio DRY (Dont Repeat
Yourself), vamos a ver cmo 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, caractersticas de enrutamiento que
puede heredar cualquier ruta. Ahora solo lo utilizaremos para una sola ruta, pero en el prximo
captulo veremos que esta caracterstica es muy til.
//...
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'
});
//...
lib/router.js
Vamos a verlo paso a paso. En primer lugar, creamos nuestro controlador extendiendo
RouteController
Commit 12-3
postLists refactorizado en un controlador de rutas
Ver en GitHub
Lanzar instancia
de aritmtica!
Al igual que antes, vamos a aadir nuestra lgica de paginacin en la ruta. Recuerdas que
nombramos explcitamente el contexto de datos en lugar de usar un cursor annimo? Bueno, pues no
hay ninguna regla que diga que la funcin data solo puede pasar cursores, de modo que usaremos
la misma tcnica para generar la URL del botn Load more.
//...
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
};
}
});
//...
lib/router.js
Echemos un vistazo en profundidad a este pequeo 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 parmetro postsLimit .
Cuando alimentamos {postsLimit: this.postsLimit() + this.increment} a
this.route.path()
, salvo que reemplazamos el this implcito por nuestro propio contexto de datos a
medida.
Estamos cogiendo ese path y aadindolo al contexto de datos de nuestra plantilla, pero slo si hay
ms posts que mostrar. La forma de hacerlo es un poco complicada.
Sabemos que this.limit() devuelve el nmero actual de posts que nos gustara mostrar, que
puede ser el valor de la URL actual, o el valor por defecto (5) si la URL no contiene ningn parmetro.
Por otro lado, this.posts se refiere al cursor actual, de modo que this.posts.count() es el
nmero 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 botn Load more. Pero si pedimos n y tenemos menos de n , significa que hemos llegado al
lmite y deberamos dejar de mostrarlo.
Con todo esto, nuestro sistema falla en un caso: cuando el nmero 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 botn Load more, sin darse cuenta de que ya no quedan ms 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 aadir el botn Load more en la parte inferior de nuestra lista de
posts, asegurndonos de mostrarlo solo si tenemos ms posts que cargar:
<template name="postsList">
<div class="posts">
{{#each posts}}
{{> postItem}}
{{/each}}
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{/if}}
</div>
</template>
client/templates/posts/posts_list.html
Commit 12-4
Aadido nextPath() al controlador para desplazarnos por l
Ver en GitHub
Lanzar instancia
//...
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
};
}
});
//...
lib/router.js
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:
<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>
client/templates/posts/posts_list.html
Commit 12-5
Aadir un spinner para hacer la pagicin mas atractiva.
Ver en GitHub
Lanzar instancia
Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});
Meteor.publish('singlePost', function(id) {
check(id, String)
return Posts.find(id);
});
//...
server/publications.js
Ahora, vamos a suscribirnos a los posts correctos en el lado del cliente. Ya estamos suscritos a la
publicacin comments en la funcin waitOn de la ruta postPage , por lo que simplemente podemos
aadir ah la suscripcin a singlePost . Sin olvidarnos de aadir la suscripcin a la ruta postEdit ,
que tambin necesita los mismos datos:
//...
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); }
});
//...
lib/router.js
Commit 12-6
Usando una sola suscripcin a los posts para asegurarnos
Ver en GitHub
Lanzar instancia
Votos
13
Ahora que nuestro sitio es cada vez ms popular, empieza a ser complicado buscar los mejores posts.
Lo que necesitamos es algn tipo de sistema de clasificacin para ordenarlos.
Podramos construir un sistema de clasificacin complejo con karma, basado en tiempo, y muchas
otras cosas (la mayora de las cuales se implementan en Telescope, el hermano mayor de
Microscope). Nosotros vamos a mantener las cosas sencillas y ordenaremos los posts por el nmero
de votos que reciban.
Vamos a empezar proporcionado a los usuarios una manera de votar los posts.
El modelo de datos
Vamos a guardar una lista de upvoters en cada post para que sepamos dnde mostrar el botn
upvote a los usuarios, as como para evitar que la gente vote varias veces el mismo post.
Tambin vamos a denormalizar el nmero total de upvoters de un post para que sea ms fcil
recuperar esa cifra. As que vamos a aadir dos atributos a nuestros posts, upvoters y votes .
Vamos a empezar aadindolos a nuestros datos de prueba:
// Fixture data
if (Posts.find().count() === 0) {
var now = new Date().getTime();
// create two users
var tomId = Meteor.users.insert({
profile: { name: 'Tom Coleman' }
});
var tom = Meteor.users.findOne(tomId);
var sachaId = Meteor.users.insert({
profile: { name: 'Sacha Greif' }
});
var sacha = Meteor.users.findOne(sachaId);
var telescopeId = Posts.insert({
title: 'Introducing Telescope',
userId: sacha._id,
author: sacha.profile.name,
url: 'http://sachagreif.com/introducing-telescope/',
submitted: new Date(now - 7 * 3600 * 1000),
commentsCount: 2,
upvoters: [],
votes: 0
});
Comments.insert({
postId: telescopeId,
userId: tom._id,
author: tom.profile.name,
submitted: new Date(now - 5 * 3600 * 1000),
body: 'Interesting project Sacha, can I get involved?'
});
Comments.insert({
postId: telescopeId,
userId: sacha._id,
author: sacha.profile.name,
submitted: new Date(now - 3 * 3600 * 1000),
body: 'You sure can Tom!'
});
Posts.insert({
title: 'Meteor',
userId: tom._id,
author: tom.profile.name,
url: 'http://meteor.com',
submitted: new Date(now - 10 * 3600 * 1000),
commentsCount: 0,
upvoters: [],
votes: 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,
upvoters: [],
votes: 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 + 1),
commentsCount: 0,
upvoters: [],
votes: 0
});
}
}
server/fixtures.js
Como de costumbre, ejecutamos meteor reset y creamos una nueva cuenta de usuario. Ahora nos
aseguraremos que inicializamos las dos nuevas propiedades cuando se crean los posts:
//...
var postWithSameLink = Posts.findOne({url: postAttributes.url});
if (postWithSameLink) {
return {
postExists: true,
_id: postWithSameLink._id
}
}
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date(),
commentsCount: 0,
upvoters: [],
votes: 0
});
var postId = Posts.insert(post);
return {
_id: postId
};
//...
collections/posts.js
Plantillas de voto
Lo primero es aadir un botn upvote a nuestros posts y mostrar el contador de votos en los
metadatos del post:
<template name="postItem">
<div class="post">
<a href="#" class="upvote btn btn-default"> </a>
<div class="post-content">
<h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
{{votes}} Votes,
submitted by {{author}},
<a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
</div>
</template>
client/templates/posts/post_item.html
El botn upvote
//...
Template.postItem.events({
'click .upvote': function(e) {
e.preventDefault();
Meteor.call('upvote', this._id);
}
});
client/templates/posts/post_item.js
//...
Meteor.methods({
post: function(postAttributes) {
//...
},
upvote: function(postId) {
check(this.userId, String);
check(postId, String);
var post = Posts.findOne(postId);
if (!post)
throw new Meteor.Error('invalid', 'Post not found');
if (_.include(post.upvoters, this.userId))
throw new Meteor.Error('invalid', 'Already upvoted this post');
Posts.update(post._id, {
$addToSet: {upvoters: this.userId},
$inc: {votes: 1}
});
}
});
//...
lib/collections/posts.js
Commit 13-1
Algoritmo bsico de voto.
Ver en GitHub
Lanzar instancia
El mtodo es bastante sencillo. Hacemos algunas comprobaciones para garantizar que el usuario ha
iniciado sesin y que el post realmente existe. Despus de corroborar que el usuario no ha votado ya
este post, incrementamos el total de votos y aadimos al usuario a la lista de upvoters.
Este ltimo paso es muy interesante. Hemos utilizado un par de operadores de Mongo que son muy
tiles: $addToSet agrega un elemento a una lista siempre y cuando este no exista ya en ella, y $inc
simplemente incrementa un entero.
<template name="postItem">
<div class="post">
<a href="#" class="upvote btn btn-default {{upvotedClass}}">
<div class="post-content">
//...
</div>
</template>
client/templates/posts/post_item.html
</a>
Template.postItem.helpers({
ownPost: function() {
//...
},
domain: function() {
//...
},
upvotedClass: function() {
var userId = Meteor.userId();
if (userId && !_.include(this.upvoters, userId)) {
return 'btn-primary upvotable';
} else {
return 'disabled';
}
}
});
Template.postItem.events({
'click .upvotable': function(e) {
e.preventDefault();
Meteor.call('upvote', this._id);
}
});
client/templates/posts/post_item.js
Creamos el ayudante upvotedClass para cambiar la clase .upvote por .upvotable , as que no
podemos olvidar hacerlo tambin en el controlador de eventos.
client/views/posts/post_item.js
Commit 13-2
Deshabilitado el botn upvote si el usuario no ha accedid
Ver en GitHub
Lanzar instancia
Ahora nos damos cuenta que los posts con un solo voto estn etiquetados como 1 votes, por lo que
vamos a pararnos a pluralizar las etiquetas correctamente. La pluralizacin puede ser un proceso
complicado, pero por ahora vamos a hacerlo de una manera bastante simple. Crearemos un nuevo
ayudante Spacebars que podemos utilizar desde cualquier lugar:
client/helpers/spacebars.js
Los ayudantes que hemos creado con anterioridad siempre han estado relacionados con la plantilla a
la que se aplican. Pero usando Template.registerHelper , hemos creado un ayudante global que se
puede utilizar dentro de cualquier plantilla:
<template name="postItem">
//...
<p>
{{pluralize votes "Vote"}},
submitted by {{author}},
<a href="{{pathFor 'postPage'}}">{{pluralize commentsCount "comment"}}</a>
{{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
//...
</template>
client/templates/posts/post_item.html
Pluralizando
Commit 13-3
Aadida una funcin para pluralizar textos.
Ver en GitHub
Lanzar instancia
la base de datos dos veces. Pero lo ms importante es que se introduce una condicin de carrera.
Estamos siguiendo el siguiente algoritmo:
1. Coger el post desde la base de datos.
2. Comprobar si el usuario lo ha votado.
3. Si no, aadir un voto.
Qu pasa si el mismo usuario vota el mismo post entre los pasos 1 y 3? Nuestro cdigo abre la puerta
al usuario a votar dos veces el mismo post. Afortunadamente, Mongo nos permite ser ms inteligentes
y combinar las consultas 1 y 3 en una sola:
//...
Meteor.methods({
post: function(postAttributes) {
//...
},
upvote: function(postId) {
check(this.userId, String);
check(postId, String);
var affected = Posts.update({
_id: postId,
upvoters: {$ne: this.userId}
}, {
$addToSet: {upvoters: this.userId},
$inc: {votes: 1}
});
if (! affected)
throw new Meteor.Error('invalid', "You weren't able to upvote that post");
}
});
//...
collections/posts.js
Commit 13-4
Mejorado el algoritmo de voto.
Ver en GitHub
Lanzar instancia
Lo que estamos diciendo es encuentra todos los posts con este id que todava no hayan sido
votados por este usuario, y actualzalos. Si el usuario an no ha votado el post con esta id , lo
encontrar, pero si ya lo ha hecho, la consulta no coincidir con ningn documento, y por lo tanto no
ocurrir nada.
Compensacin de la latencia
Digamos que tratas de engaarnos y pones uno de tus posts el primero de la lista ajustando
su nmero de votos desde la consola del navegador:
general.
Para empezar, queremos tener dos suscripciones, una para cada tipo de ordenacin. El truco aqu es
suscribirse a la misma publicacin, solo que con diferentes argumentos!.
Tambin crearemos dos nuevas rutas denominadas newPosts y bestPosts , accesibles desde las
direcciones /new y /best respectivamente (junto con /new/5 y /best/5 para la paginacin, por
supuesto).
Para ello, vamos a extender nuestro PostsListController en dos controladores distintos:
NewPostsListController
opciones de ruta, tanto para las rutas home y newPosts , quedndonos un solo
NewPostsListController
//...
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: this.sort, 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();
return {
posts: this.posts(),
ready: this.postsSub.ready,
nextPath: hasMore ? this.nextPath() : null
};
}
});
NewPostsController = PostsListController.extend({
sort: {submitted: -1, _id: -1},
nextPath: function() {
return Router.routes.newPosts.path({postsLimit: this.postsLimit() + this.incre
ment})
}
});
BestPostsController = PostsListController.extend({
sort: {votes: -1, submitted: -1, _id: -1},
nextPath: function() {
return Router.routes.bestPosts.path({postsLimit: this.postsLimit() + this.incr
ement})
}
});
Router.route('/', {
name: 'home',
controller: NewPostsController
});
Router.route('/new/:postsLimit?', {name: 'newPosts'});
Router.route('/best/:postsLimit?', {name: 'bestPosts'});
lib/router.js
Ten en cuenta que ahora que tenemos ms de una ruta, sacamos la lgica para nextPath de
PostsListController
Router.route('/:postsLimit?', {
name: 'postsList'
})
lib/router.js
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'home'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
<li>
<a href="{{pathFor 'newPosts'}}">New</a>
</li>
<li>
<a href="{{pathFor 'bestPosts'}}">Best</a>
</li>
{{#if currentUser}}
<li>
<a href="{{pathFor 'postSubmit'}}">Submit Post</a>
</li>
<li class="dropdown">
{{> notifications}}
</li>
{{/if}}
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>
client/templates/includes/header.html
client/templates/posts/posts_edit.js
Posts ordenados
Commit 13-5
Aadidas rutas para las listas de posts y pginas para mo
Ver en GitHub
Lanzar instancia
Mejorando la cabecera
Ahora que tenemos dos listas, puede resultar difcil saber cul de ellas estamos viendo. As que vamos
a revisar nuestra cabecera para que sea ms evidente. Vamos a crear el gestor header.js y un
ayudante auxiliar que use la ruta actual y una o ms rutas con nombre para activar una clase en
nuestros elementos de navegacin:
La razn por la que queremos permitir varias rutas es que tanto la ruta home como la ruta newPosts
(que se corresponden con las nuevas URLs / y /new ) devuelven la misma plantilla, lo que significa
que nuestro activeRouteClass debe ser lo suficientemente inteligente como para activar la etiqueta
<li>
en ambos casos.
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'home'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
<li class="{{activeRouteClass 'home' 'newPosts'}}">
<a href="{{pathFor 'newPosts'}}">New</a>
</li>
<li class="{{activeRouteClass 'bestPosts'}}">
<a href="{{pathFor 'bestPosts'}}">Best</a>
</li>
{{#if currentUser}}
<li class="{{activeRouteClass 'postSubmit'}}">
<a href="{{pathFor 'postSubmit'}}">Submit Post</a>
</li>
<li class="dropdown">
{{> notifications}}
</li>
{{/if}}
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</nav>
</template>
client/templates/includes/header.html
Template.header.helpers({
activeRouteClass: function(/* route names */) {
var args = Array.prototype.slice.call(arguments, 0);
args.pop();
var active = _.any(args, function(name) {
return Router.current() && Router.current().route.getName() === name
});
return active && 'active';
}
});
client/templates/includes/header.js
Para cada elemento, el ayudante activeRouteClass toma una lista de nombres de ruta, y luego
utiliza el ayudante any() de Underscore para ver si las rutas pasan la prueba (es decir, que su
correspondiente URL sea igual a la actual). Si cualquiera de las rutas se corresponde con la actual,
any()
devolver true .
Por ltimo, estamos aprovechando el patrn boolean && myString de JavaScript con el que false
&& myString
Commit 13-6
Aadidas clases activas a la cabecera.
Ver en GitHub
Lanzar instancia
Ahora que los usuarios pueden votar en tiempo real, podemos ver cmo saltan los posts hacia arriba
o abajo segn cambia su clasificacin. Pero no sera ms agradable si hubiera una manera de
suavizar estos cambios con algunas animaciones?
Publicaciones avanzadas
SIDEBAR
13.5
A estas alturas ya deberas conocer bastante bien cmo interactan las suscripciones y las
publicaciones. As que vamos a deshacernos de las ruedas de entrenamiento y examinar algunos
escenarios ms avanzados.
Meteor.publish('allPosts', function() {
return Posts.find({}, {fields: {title: true, author: true}});
});
Meteor.publish('postDetail', function(postId) {
return Posts.find(postId);
});
Ahora cuando el cliente se suscriba a esas dos publicaciones, su coleccin 'posts' se rellena desde
dos fuentes: una lista de ttulos y nombres de autor de la primera suscripcin, y los detalles completos
de un nico post de la segunda.
Tal vez te hayas dado cuenta de que el post publicado por postDetail se publica tambin desde
allPosts
(aunque solo con un subconjunto de sus propiedades). Sin embargo, Meteor se hace cargo
Meteor.publish('newPosts', function(limit) {
return Posts.find({}, {sort: {submitted: -1}, limit: limit});
});
Meteor.publish('bestPosts', function(limit) {
return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
});
server/publications.js
En Microscope, nos suscribimos a la publicacin de posts varias veces, pero Iron Router activa y
desactiva cada suscripcin por nosotros. Aun as, no hay ninguna razn por la cual no podamos
suscribirnos muchas veces simultneamente.
Por ejemplo, digamos que queremos cargar los posts ms recientes y los mejores en la memoria al
mismo tiempo:
Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});
Luego, nos suscribimos a esta publicacin mltiples veces. De hecho, esto es ms o menos
exactamente lo que estamos haciendo en Microscope:
Entonces, qu est pasando exactamente? Cada navegador abre dos suscripciones diferentes, cada
uno se conecta a la misma publicacin en el servidor.
Cada suscripcin ofrece diferentes argumentos para esa publicacin, pero fundamentalmente, cada
vez que un conjunto de documentos (diferente) se saca de la coleccin posts , se enva por el canal a
la coleccin del lado del cliente.
Puedes incluso suscribirte dos veces a la misma publicacin con los mismos argumentos! Es difcil
pensar en escenarios donde esto sea de utilidad, pero esta flexibilidad puede que sea til algn da!
Sin embargo, supongamos que queremos mostrar todos los comentarios en los posts en la pgina
principal (teniendo en cuenta que estos posts van a cambiar a medida que paginamos a travs de
ellos). Este caso de uso presenta una buena razn para insertar comentarios en los posts, y de hecho
es lo que nos empuja a desnormalizar la coleccin de comentarios.
Por supuesto que siempre se pueden insertar los comentarios en los posts, deshacindonos de la
coleccin de comments por completo. Pero, como hemos visto anteriormente en el captulo
Desnormalizacin, al hacerlo estaramos perdiendo algunos beneficios adicionales de trabajar con
colecciones separadas.
Pero resulta que hay un truco que hace posible embeber nuestros comentarios, preservando
colecciones separadas.
Supongamos que, junto con la lista de la pgina principal de posts, queremos suscribirnos a una lista
de los mejores 2 comentarios para cada uno de ellos.
Lograr esto con una publicacin de comentarios independiente sera difcil, sobre todo si la relacin
de posts se limita de alguna manera (por ejemplo, los 10 ms recientes). Tendramos que crear una
publicacin que se pareciera a algo como esto:
Meteor.publish('topComments', function(topPostIds) {
return Comments.find({postId: topPostIds});
});
Esto sera un problema desde el punto de vista de rendimiento, ya que habra que eliminar y volver a
establecer la publicacin cada vez que cambiara la lista de topPostIds .
Existe una manera de evitar esto. Acabamos de utilizar el hecho de que no solo podemos tener ms de
una publicacin por coleccin, sino que tambin podemos tener ms de una coleccin por publicacin:
Meteor.publish('topPosts', function(limit) {
var sub = this, commentHandles = [], postHandle = null;
Tengamos en cuenta que no estamos devolviendo nada en esta publicacin, le enviamos mensajes
manualmente a la sub nosotros mismos (a travs de .added() y sus amigos). As que no
necesitamos que _publishCursor lo haga mediante la devolucin de un cursor.
Ahora, cada vez que publiquemos un post tambin publicaremos automticamente los dos primeros
usar.
Una razn por la que no querramos hacer esto es la Herencia de Tabla Simple
Supongamos que quisiramos referenciar varios tipos de objetos desde nuestros posts, cada uno
alojado en campos comunes pero ligeramente diferentes en contenido. Por ejemplo, podramos estar
creando un motor de blogging al estilo de Tumblr en el que cada post posee el habitual ID, un
timestamp, y el ttulo. Pero tambin puede tener imgenes, videos, links o simplemente texto.
Podramos guardar todos estos objetos en una coleccin llamada 'resources' (recursos), usando
un atributo type que indique qu tipo de objeto son ( video , image , link , etc.).
Y aunque tendramos una sola coleccin resources en el servidor, podramos transformar esa nica
coleccin en mltiples colecciones en el cliente, como Videos , Images , etc., con el siguiente trozo
de magia:
Meteor.publish('videos', function() {
var sub = this;
var videosCursor = Resources.find({type: 'video'});
Mongo.Collection._publishCursor(videosCursor, sub, 'videos');
// _publishCursor doesn't call this for us in case we do this more than once.
sub.ready();
});
Le estamos diciendo a _publishCursor que publique nuestros videos (como hacer un return) como
lo hara el cursor, pero en lugar de publicar la coleccin resources en el cliente, publicamos de
resources
a videos .
Otra idea similar es usar publish para una coleccin en el lado del cliente donde no hay ninguna
coleccin en el lado servidor!. Por ejemplo, podras obtener datos de un servicio de terceros, y
publicarlos como si fuera una coleccin en el cliente.
Gracias a la flexibilidad de la API de publicacin, las posibilidades son ilimitadas.
Animaciones
14
Aunque contamos un sistema de votacin en tiempo real, no tenemos una gran experiencia de
usuario viendo la forma en la que los posts se mueven en la pgina principal. Usaremos animaciones
para suavizar este problema.
Introduciendo a los
_uihooks
Los _uihooks son una caracterstica de Blaze relativamente nueva y poco documentada. Como su
propio nombre indica, nos da acceso a acciones que podemos ejecutar cuando se insertan, eliminan o
animan elementos.
La lista completa de acciones es esta:
insertElement
moveElement
removeElement
Una vez definidas, estas acciones reemplazarn el comportamiento que Meteor tiene por defecto. En
otras palabras, en vez de insertar, mover o eliminar elementos, Meteor usar el comportamiento que
hayamos definido y ser cosa nuestra que ese comportamiento sea correcto!
Meteor y el DOM
Antes de poder empezar con la parte divertida (hacer que se muevan las cosas), tenemos que
entender cmo interacta Meteor con el DOM (Document Object Model - la coleccin de elementos
HTML que componen el contenido de una pgina).
Lo ms importante que hay que tener en cuenta es que los elementos del DOM realmente no se
pueden mover. Slo se pueden aadir y eliminar (esto es una limitacin del propio DOM, no de
Meteor). As que para crear la ilusin de que los elementos A y B se intercambian, Meteor tendr que
El corredor ruso
Pero, primero, una historia.
En 1980, en pleno apogeo de la guerra fra, los Juegos Olmpicos se celebraban en Mosc, y los
soviticos estaban decididos a ganar la carrera de 100 metros a cualquier precio. As que un grupo de
brillantes cientficos soviticos equiparon a uno de sus atletas con un teletransportador, y en cuanto
son el disparo de salida, el corredor fue trasladado de inmediato a la lnea de meta.
Afortunadamente, los jueces de la carrera se dieron cuenta de la infraccin inmediatamente, y el
atleta no tuvo ms remedio que teletransportarse de nuevo a su casilla de salida, antes de permitirle
participar de nuevo corriendo como los dems.
Mis fuentes histricas no son muy fiables, por lo que debes tomar esa historia como cogida con
pinzas. Pero trataremos de mantener en mente la analoga del corredor sovitico con
teletransportador a medida que avancemos en este captulo.
De nuevo, en los pasos 3 y 4 no estamos animando A y B hasta sus posiciones sino que las
teletransportamos all al instante. Dado que el cambio es instantneo, parecer que B no se ha
borrado, pero ya tenemos posicionados correctamente los elementos para que puedan ser animados
hasta su nueva posicin.
Afortunadamente, Meteor se ocupa de los pasos 1 y 2 y re-implementarlos ser una tarea fcil. En los
pasos 5 y 6, lo nico que hacemos es mover los elementos al lugar adecuado. As que, slo tenemos
que preocuparnos de los pasos 3 y 4, enviar los elementos al punto de arranque de la animacin.
Posicionamiento CSS
Para animar los posts que se estn reordenando por la pgina, vamos a tener que meternos en
territorio CSS. Sera recomendable una rpida revisin del posicionamiento con CSS.
Los elementos de una pgina utilizan posicionamiento esttico por defecto. Los elementos
posicionados de forma esttica estn fijos y sus coordenadas no se pueden cambiar o animar.
Por otra parte, el posicionamiento relativo, implica que el elemento est fijado a la pgina, pero se
puede mover con relacin a su posicin original.
El posicionamiento absoluto va un paso ms all y permite dar coordenadas x/y a un elemento en
relacin al documento o al primer elemento padre posicionado de forma absoluta o relativa.
Nosotros vamos a usar posicionamiento relativo en nuestras animaciones. Ya disponemos del CSS
necesario en client/stylesheets/style.css , pero si necesitas aadirlo, este es el cdigo para la
hoja de estilo:
.post{
position:relative;
}
.post.animate{
transition:all 300ms 0ms ease-in;
}
client/stylesheets/style.css
Ten en cuenta que slo animamos los posts con la clase CSS .animate . De esta forma, podemos
aadir y quitar esa clase para controlar cundo deben producirse o no las animaciones.
Esto facilita muchos los pasos 5 y 6: todo lo que necesitamos hacer es configurar la parte top a 0px
(su valor predeterminado) y nuestros posts se deslizarn de nuevo a su posicin normal.
Esto significa que nuestro nico problema es averiguar desde dnde animar los posts (pasos 3 y 4)
con respecto a su nueva posicin. En otras palabras, en qu posicin hay que ponerlo. Pero, esto no es
tan difcil: el desplazamiento correcto (oset) es la posicin anterior restada a la nueva.
<template name="postsList">
<div class="posts page">
<div class="wrapper">
{{#each posts}}
{{> postItem}}
{{/each}}
</div>
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{else}}
{{#unless ready}}
{{> spinner}}
{{/unless}}
{{/if}}
</div>
</template>
client/templates/posts/posts_list.html
Vamos a por los _uihooks . Dentro del callback onRendered de la plantilla, seleccionamos el div
.wrapper
Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
moveElement: function (node, next) {
// do nothing for now
}
}
});
client/templates/posts/posts_list.js
Hemos comprobado que los _uihooks funcionan. Ahora vamos a hacer que animen los posts!
node
next
es el elemento que hay justo despus de la nueva posicin a la que estamos moviendo
node
Sabiendo esto, podemos definir el proceso de animacin (si necesitas refrescar la memoria, no dudes
en volver al ejemplo del Corredor Ruso). Cuando detectamos un nuevo cambio en la posicin de un
elemento, tendremos que hacer lo siguiente:
1. Insertar node antes de next (en otras palabras, establecer el comportamieento por defecto,
como si no hubiramos definido la accin moveElement ).
2. Mover node a su posicin original.
3. Moveremos todos los elementos que hay entre node y next para hacer sitio a node .
4. Animaremos todos los elementos hasta su posicin original.
Para hacer todo esto usaremos la magia de jQuery, de lejos, la mejor librera de manipulacin del
DOM que existe. jQuery est fuera del alcance de este libro, pero vamos a ver rpidamente los
mtodos que vamos a usar:
Con $() convertimos cualquier elemento del DOM en un objeto jQuery.
offset()
Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
moveElement: function (node, next) {
var $node = $(node), $next = $(next);
var oldTop = $node.offset().top;
var height = $node.outerHeight(true);
// force a redraw
$node.offset();
// reset everything to 0, animated
$node.addClass('animate').css('top', 0);
$inBetween.addClass('animate').css('top', 0);
}
}
});
client/templates/posts/posts_list.js
Algunas notas:
Calculamos la altura de $node para saber cunto debemos mover los elementos $inBetween .
Y usamos outerHeight(true) para incluir margen y padding en el clculo.
No sabemos si next va antes o despus de node as que comprobamos las dos
configuraciones cuando definimos $inBetween .
Para cambiar los elementos de teletransportados a animados, simplemente aadimos o
quitamos la clase animate (la animacin definida en el cdigo CSS de la aplicacin).
Dado que usamos posicionamiento relativo, siempre podemos poner a 0 la propiedad top del
elemento para devolverlo a la posicin dnde se supone que tiene que ir.
Forzando el redibujado
Te estars preguntando para qu es la lnea $node.offset() . Para qu obtenemos la
posicin de $node si no vamos a hacer nada con ella?
Mralo as: si le dices a un robot muy inteligente que se mueva al norte 5 kilmetros, y luego
al sur otros 5, probablemente sabr deducir que va a terminar en el mismo sitio, y que puede
ahorrar energa y hacer bien el trabajo sin moverse.
As que si quieres que el robot ande 10 kilmetros, le diremos que mida sus coordenadas a
los 5 kilmetros, antes de que de la vuelta.
El navegador funciona de una manera similar: si le damos las instrucciones css('top',
oldTop - newTop)
Vamos a probar de nuevo. Volvamos a la vista Best y votemos unos posts: Deberas verlos
movindose suavemente hacia arriba y hacia abajo como en un ballet!
Animated reordering
Animated reordering
Commit 14-1
Added post reordering animation.
Ver en GitHub
Aparecer y desaparecer
Lanzar instancia
Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
insertElement: function (node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
moveElement: function (node, next) {
//...
}
}
});
client/templates/posts/posts_list.js
Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
insertElement: function (node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
moveElement: function (node, next) {
//...
},
removeElement: function(node) {
$(node).fadeOut(function() {
$(this).remove();
});
}
}
});
client/templates/posts/posts_list.js
De nuevo, para ver el efecto, prueba a eliminar algn post desde la consola
( Posts.remove('algunPostId') ).
Commit 14-2
Fade items in when they are drawn.
Ver en GitHub
Lanzar instancia
Hemos creado animaciones para elementos dentro de una pgina. Pero, qu pasa si queremos
animar las transiciones entre pginas?
Las transiciones entre pginas son trabajo del Iron Router. Haces click en un enlace y se reemplaza el
contenido del ayudante {{> yield}} en layout.html
Ocurre que, como cuando reemplazamos el comportamiento de Blaze para la lista de posts,
podemos hacer lo mismo para el elemento {{> yield}} y aadirle un efecto de transicin entre
rutas!
Si queremos animar la entrada y salida entre dos pginas, debemos asegurarnos de que se muestran
una por encima de la otra. Lo hacemos usando la propiedad position:absolute en el contenedor
.page
Piensa que no queremos que las pginas estn posicionadas de forma absoluta, porque de esta
forma, se solaparan con la cabecera de la app. As que establecemos la propiedad
position:relative
.page
//...
#main{
position: relative;
}
.page{
position: absolute;
top: 0px;
width: 100%;
}
//...
client/stylesheets/style.css
Es el momento de aadir el cdigo para las transiciones entre pginas. Nos debe resultar familiar,
puesto que es exactamente el mismo que para las inserciones y eliminaciones de posts:
Template.layout.onRendered(function() {
this.find('#main')._uihooks = {
insertElement: function(node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
removeElement: function(node) {
$(node).fadeOut(function() {
$(this).remove();
});
}
}
});
client/templates/application/layout.js
Commit 14-3
Transition between pages by fading.
Ver en GitHub
Lanzar instancia
Hemos visto unos pocos patrones para animar elementos en nuestra aplicacin Meteor. Aunque no es
una lista exhaustiva, con suerte, nos aportar una base sobre la que construir transiciones ms
elaboradas.
Ir ms lejos
14.5
Esperamos que con la lectura de los captulos anteriores tengas una buena visin general de todo lo
que involucra la construccin de una aplicacin Meteor. Entonces, dnde podemos ir ahora?.
Captulos extra
En primer lugar, puedes comprar las ediciones Full o Premium para desbloquear el acceso a los
captulos adicionales. Estos captulos te guiarn a travs de escenarios del mundo real, tales como la
construccin de una API para tu aplicacin, la integracin con servicios de terceros o la migracin de
datos.
Manual de Meteor
Adems de contar con la documentacin oficial, el Manual de Meteor profundiza en temas
especficos como Tracker o Blaze.
Evented Mind
Si quieres sumergirte en los entresijos de Meteor, te recomendamos echarle un vistazo a Evented
Mind de Chris Mather, una plataforma de aprendizaje con ms de 50 vdeos sobre Meteor (y nuevos
vdeos que se agregan cada semana).
MeteorHacks
Una de las mejores formas de mantenerse al da con Meteor es suscribirse al boletn semanal de
Arunoda Susiripala MeteorHacks. En el blog tambin puedes encontrar un montn de consejos
avanzados sobre Meteor.
Atmosphere
Atmosphere es el repositorio de paquetes no oficiales de Meteor, es otro gran lugar para aprender
ms: puedes descubrir nuevos paquetes y echar un vistazo a su cdigo para ver qu patrones utiliza la
gente.
(Aviso legal: Atmosphere es mantenida, en parte por Tom Coleman, uno de los autores de este libro).
Meteorpedia
Meteorpedia es un wiki sobre Meteor. Y, por supuesto, est hecho con Meteor!
BulletProof Meteor
Otra iniciativa de Arunoda de MeteorHacks, BulletProof Meteor te guiar a travs de lecciones con
preguntas tipo test, y enfocadas al rendimiento de Meteor.
El Podcast Meteor
Josh y Ry de la empresa dierential graban el Podcast Meteor todas las semanas, otra forma de
mantenerse al da con lo que pasa en la comunidad Meteor.
Otros recursos
Stephan Hochhaus ha compilado una lista bastante exhaustiva de recursos Meteor.
El blog de Manuel Schoebel y el de Gentlenode son una buena fuente de informacin sobre Meteor.
Pedir ayuda
Si encuentras algn obstculo, el mejor lugar para preguntar es Stack Overflow. Asegrate de
etiquetar la pregunta con la etiqueta meteor .
La comunidad
Por ltimo, la mejor forma de estar al da con Meteor es mantenerse activo en la comunidad. Nosotros
recomendamos inscribirse en la lista de correo de Meteor, seguir los grupos de Google Meteor Core y
Meteor Talk y crear una cuenta en el foro de Meteor Crater.io.
Vocabulario
14.5
Cliente
Cuando hablamos del Cliente, nos referimos al cdigo que se ejecuta en el navegador de los usuarios,
ya sea uno tradicional, como Firefox o Safari, o algo tan complejo como un UIWebView en una
aplicacin nativa para el iPhone.
Coleccin
Una coleccin es el almacn de datos que se sincroniza automticamente entre el cliente y el
servidor. Las colecciones tienen un nombre (como posts ), y por lo general existen tanto en el cliente
como en el servidor. Si bien se comportan de forma distinta, tienen una API comn basada en la API
de Mongo.
Computacin
Una computacin es un bloque de cdigo que se ejecuta cada vez que cambia una de las fuentes de
datos reactivos de las que depende. Si tienes una fuente reactiva (por ejemplo, una variable de
sesin) y quieres responder reactivamente a ella, tendrs que crear una computacin.
Cursor
Un cursor es el resultado de ejecutar una consulta en una coleccin Mongo. En el lado del cliente, un
cursor no es tan slo un conjunto de resultados, sino que es un objeto reactivo desde el que se puede
observar (con observe() ) los cambios (aadir, eliminar o actualizar) en la coleccin
correspondiente.
DDP
El DDP es el Protocolo de Datos Distribuidos que utiliza Meteor para sincronizar colecciones y efectuar
llamadas a mtodos. DDP pretende ser un protocolo genrico, que toma el relevo a HTTP para
Suscripcin
Una suscripcin es una conexin a una publicacin desde un cliente especfico. La suscripcin es el
cdigo que ejecuta el navegador y que utiliza para comunicarse con una publicacin del servidor y
que, adems, mantiene los datos sincronizados.
Plantilla
Una plantilla es una forma de generar cdigo HTML desde JavaScript. Por defecto, Meteor slo
soporta el sistema Spacebars, pero hay planes para incluir ms.
Contexto de datos de una plantilla
Cuando se muestra un plantilla, lo que se representa es un objeto JavaScript que proporciona datos
especficos para esta representacin en particular. Por lo general, este tipo de objetos son, de tipo
POJO (plain-old-JavaScript-objects), a menudo son documentos de una coleccin, aunque pueden
ser ms complejos e incluir funciones.
Changelog
April 15, 2015
99
1.9
1.8
December 5, 2014
1.7.2
1.7.1
Various fixes.
Fix code highlighting in Voting chapter.
Change router to route in Pagination chapter.
Removed mentions of Router.map() in Comments and Pagination chapters.
Linking to Boostrap site in Adding Users chapter.
Added BulletProof Meteor to Going Further chapter.
1.7
1.6.1
Updated introduction.
Added Get A Load Of This section in Routing chapter.
1.6
Various edits.
Animations
This chapter is out of date. Update coming sometimes after 1.0.
Note: the following extra chapters are only included in the Full and Premium editions:
RSS Feeds & APIs
Updated package syntax.
Minor tweaks.
Using External APIs
Minor edits.
Implementing Intercom
Added favorite_color custom attribute.
Various minor edits.
Migrations
Minor edits.
October 3, 2014
1.5.1
1.5
1.3.4
1.3.3
May 5, 2014
1.3.2
April 8, 2014
1.3.1
1.3
7 Creating Posts:
HTML changes for stricter parser.
Update our onBeforeAction hook to use pause() rather than this.stop()
13 Voting: Small change to the activeRouteClass helper.
1.2
The first update of 2014 is a big one! First of all, youll notice a beautiful, photo-based layout that
makes each chapter stand out more and introduces a bit of variety in the book.
And on the content side, weve updated parts of the book and even written two whole new chapters:
New Chapters
[NEW!] 3.5 Using GitHub: New sidebar on how to use GitHub.
December 1, 2013
1.1
Major Updates
5 Routing: Rewrote chapter from scratch to use Iron Router.
5.5 The Session: Added a section about Autorun.
10 Comments: Updated chapter to use IR.
12 Pagination: Rewrote chapter from scratch, now managing pagination with IR.
13 Voting: Updated the chapter to use IR, simplifed the template structure.
Minor Updates
Minor updates include API changes between the old Router and Iron Router, file paths updates, and
small rewordings.
6 Adding Users
7 Creating Posts
7.5 Latency Compensation
8 Editing Posts
9 Errors
11 Notifications
12 Animations
If youd like to confirm what exactly has changed, weve created a full di of our Markdown source
files [PDF].
October 4, 2013
1.02
September 4, 2013
1.01
May 5, 2013
First version.
1.0