Documente Academic
Documente Profesional
Documente Cultură
R
Building Real-Time JavaScript Web Apps
Versin 1.9 (updated 27 de junio de 2015)
www.discovermeteor.com
Introduccin
1
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
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?
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.
Contacto
Empezando
2
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:
curlhttps://install.meteor.com|sh
Este comando crea un proyecto bsico listo para usar. Cuando termina,
deberamos ver un directorio llamadomicroscope/, que contiene lo siguiente:
.meteor
microscope.css
microscope.html
microscope.js
cdmicroscope
meteor
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:
meteoraddtwbs:bootstrap
meteoraddunderscore
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 las muchas ventajas de los
paquetes Meteor.
Una nota sobre los paquetes
Al hablar acerca de los paquetes en el contexto de Meteor, vale la pena ser
especfico. Meteor puede usar cinco tipos bsicos de paquetes:
Debemos mencionar que algunos de los directorios que hemos creado son
especiales y Meteor tiene reglas para ellos:
Y tambin es til saber como Meteor decide en que orden cargan los
ficheros:
Los archivos con nombre main.* se cargan despus que todos los
dems.
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?
padding:10px;
marginbottom:10px;
webkitboxshadow:01px1pxrgba(0,0,0,0.15);
mozboxshadow:01px1pxrgba(0,0,0,0.15);
boxshadow:01px1pxrgba(0,0,0,0.15);}
body{
background:#eee;
color:#666666;}
#main{
position:relative;
}
.page{
position:absolute;
top:0px;
width:100%;
}
.navbar{
marginbottom:10px;}
/*line32,../sass/style.scss*/
.navbar.navbarinner{
borderradius:0px0px3px3px;}
#spinner{
height:300px;}
.post{
/*Formodernbrowsers*/
/*ForIE6/7(triggerhasLayout)*/
*zoom:1;
position:relative;
opacity:1;}
.post:before,.post:after{
content:"";
display:table;}
.post:after{
clear:both;}
.post.invisible{
opacity:0;}
.post.instant{
webkittransition:none;
moztransition:none;
otransition:none;
transition:none;}
.post.animate{
webkittransition:all300ms0ms;
moztransition:all300ms0mseasein;
otransition:all300ms0mseasein;
transition:all300ms0mseasein;}
.post.upvote{
display:block;
margin:7px12px00;
float:left;}
.post.postcontent{
float:left;}
.post.postcontenth3{
margin:0;
lineheight:1.4;
fontsize:18px;}
.post.postcontenth3a{
display:inlineblock;
marginright:5px;}
.post.postcontenth3span{
fontweight:normal;
fontsize:14px;
display:inlineblock;
color:#aaaaaa;}
.post.postcontentp{
margin:0;}
.post.discuss{
display:block;
float:right;
margintop:7px;}
.comments{
liststyletype:none;
margin:0;}
.commentslih4{
fontsize:16px;
margin:0;}
.commentslih4.date{
fontsize:12px;
fontweight:normal;}
.commentslih4a{
fontsize:12px;}
.commentslip:lastchild{
marginbottom:0;}
.dropdownmenuspan{
display:block;
padding:3px20px;
clear:both;
lineheight:20px;
color:#bbb;
whitespace:nowrap;}
.loadmore{
display:block;
borderradius:3px;
background:rgba(0,0,0,0.05);
textalign:center;
height:60px;
lineheight:60px;
marginbottom:10px;}
.loadmore:hover{
textdecoration:none;
background:rgba(0,0,0,0.1);}
.posts.spinnercontainer{
position:relative;
height:100px;
}
.jumbotron{
textalign:center;
}
.jumbotronh2{
fontsize:60px;
fontweight:100;
}
@webkitkeyframesfadeOut{
0%{opacity:0;}
10%{opacity:1;}
90%{opacity:1;}
100%{opacity:0;}
}
@keyframesfadeOut{
0%{opacity:0;}
10%{opacity:1;}
90%{opacity:1;}
100%{opacity:0;}
}
.errors{
position:fixed;
zindex:10000;
padding:10px;
top:0px;
left:0px;
right:0px;
bottom:0px;
pointerevents:none;
}
.alert{
animation:fadeOut2700mseasein0s1forwards;
webkitanimation:fadeOut2700mseasein0s1forwards;
mozanimation:fadeOut2700mseasein0s1forwards;
width:250px;
float:right;
clear:both;
marginbottom:5px;
pointerevents:auto;
}
client/stylesheets/style.css
Commit 2-3
Estructura de ficheros reorganizada.
Ver en GitHubLanzar instancia
Despliegue
SIDEBAR2.5
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,
Inicializando Meteor Up
Para empezar, necesitamos instalar Meteor Up va npm:
npminstallgmup
cd~/microscopedeploy
mupinit
Configuracin de Meteor Up
Cuando inicializamos un nuevo proyecto, Meteor Up crea dos
archivos: mup.json y settings.json.
mup.json contendr los ajustes relacionados con el despliegue, mientras
que settings.json contendr todos los ajustes relacionados con la aplicacin
}],
//installMongoDBintheserver
"setupMongo":true,
//locationofapp(localdirectory)
"app":"/path/to/the/app",
//configureenvironmental
"env":{
"ROOT_URL":"http://supersite.com"
}
}
mup.json
Configuracin de MongoDB
El siguiente paso es configurar una base de datos MongoDB. Recomendamos
usar Compose o cualquier otro proveedor en la nube porque ofrecen un
apoyo profesional y mejores herramientas de gestin.
Si has decidido usar Compose, configura setupMongo como false y aade la
variable de entorno MONGO_URL en un bloque env en mup.json. Si por el contrario
decides usar MongoDB en el propio servidor, configura setupMongo comotrue y
Meteor Up se har cargo de todo.
El path de la aplicacin Meteor
Debido a que la configuracin de Meteor Up reside en un directorio diferente.
Mediante la propiedad app 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!
mupsetup
Mostrando logs
Los registros son muy importantes y Meteor Up proporciona una forma fcil
de manejarlos, emulando el comando tailf. Solo tienes que escribir:
muplogsf
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
3
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>
<divclass="container">
<headerclass="navbarnavbardefault"role="navigation">
<divclass="navbarheader">
<aclass="navbarbrand"href="/">Microscope</a>
</div>
</header>
<divid="main">
{{>postsList}}
</div>
</div>
</body>
client/main.html
client/templates/posts/posts_list.html
Y post_item.html:
<templatename="postItem">
<divclass="post">
<divclass="postcontent">
<h3><ahref="{{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}}, y para cada iteracin, incluimos la
plantilla postItem.
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.
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 dentro
de/client/templates/posts para construir nuestro primer ayudante:
varpostsData=[
{
title:'IntroducingTelescope',
url:'http://sachagreif.com/introducingtelescope/'
},
{
title:'Meteor',
url:'http://meteor.com'
},
{
title:'TheMeteorBook',
url:'http://themeteorbook.com'
}
];
Template.postsList.helpers({
posts:postsData
});
client/templates/posts/posts_list.js
client/templates/posts/posts_list.html
Al definir el ayudante posts, conseguimos que est disponible para usarlo en
El ayudante
domain
Template.postItem.helpers({
domain:function(){
vara=document.createElement('a');
a.href=this.url;
returna.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.
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.
SIDEBAR3.5
GitHub es un repositorio para proyectos de cdigo abierto basado en el
sistema de control de versiones Git, y su funcin es hacer que sea fcil
compartir cdigo y colaborar en proyectos. Pero tambin es una gran
herramienta de aprendizaje. En este captulo, vamos a ver cmo usar GitHub
para seguir Descubriendo Meteor.
Esta barra lateral asume que no ests familiarizado con Git ni con Github. Si
ya manejas las dos cosas, no dudes en pasar al siguiente captulo!
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:
Modificando cdigo.
Esta vez, solo se resaltan en verde las lneas modificadas.
Y, por supuesto, a veces no ests aadiendo o modificando cdigo,
tambin borras cosas:
Codigo eliminado.
Ya hemos visto el primer uso de GitHub: ver de un vistazo lo que ha
cambiado.
Ahora,nuestrorepositorioseha"aislado"delcommitHEAD.Podemosrevisarlo,
hacercambios,experimentarconellosyhacercommitsydescartarlossinque
estoafecteaotrasramascuandohagamosotroscheckouts.
Siquieresmantenerloscambiosquehashecho,puedescrearunanuevarama
usandolaopcinbconelcomandocheckout.Porejemplo:
gitcheckoutbnew_branch_name
HEADisnowata004b56...Addedbasicpostslisttemplateandstaticdata.
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 chapter31 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:
Colecciones
4
En el Captulo uno hablamos sobre la caracterstica central de Meteor: la
sincronizacin automtica de datos entre cliente y servidor.
En este captulo miraremos ms de cerca todo esto y observaremos cmo
funciona la tecnologa que lo hace posible, lasColecciones Meteor.
Una coleccin es una estructura de datos especial que se encarga de
almacenar los datos de forma permanente, en una base de datos MongoDB
en el servidor, y de la sincronizacin de datos en tiempo real con el
navegador de cada usuario conectado.
Queremos que nuestros posts sean permanentes y los podamos compartir
con otros usuarios, as que vamos a empezar creando una coleccin
llamada Posts para poder almacenarlos.
Las colecciones son el eje central de cualquier aplicacin, as que para
asegurarnos de que se definen primero, las pondremos en el directorio lib. Si
todava no lo has hecho, crea un directorio llamado collections/ dentro
de lib, crea un archivo llamado posts.js y aade lo siguiente:
Posts=newMongo.Collection('posts');
lib/collections/posts.js
Commit 4-1
Coleccin Posts
Ver en GitHubLanzar instancia
Almacenando datos
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 hace la coleccin en cada entorno es
bastante diferente.
En el servidor, la coleccin tiene la tarea de hablar con la base de datos
MongoDB y leer y escribir cualquier cambio. En este sentido, se puede
comparar con una librera de base de datos estndar.
En el cliente sin embargo, la coleccin es una copia de un subconjunto de la
coleccin cannica. La coleccin del lado del cliente se mantiene
actualizada, de forma constante y (normalmente) trasparente con ese
subconjunto de datos en tiempo real.
Consola vs. Consola vs. Consola
La Terminal
Prompt: $.
Tambin se conoce como: Shell, Bash
Consola del navegador
Prompt: .
Tambin se conoce como: JavaScript Console, DevTools Console
La consola de Meteor
La consola de Meteor
Prompt: >.
La consola de Mongo
La consola de Mongo
Prompt: >.
>db.posts.insert({title:"Anewpost"});
>db.posts.find();
{"_id":ObjectId(".."),"title":"Anewpost"};
Consola de mongo
Mongo en Meteor.com
Comunicacin cliente-servidor
Consola de Mongo
Posts.findOne();
{title:"Anewpost",_id:LocalCollection._ObjectID};
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
url:'http://sachagreif.com/introducingtelescope/'
});
Posts.insert({
title:'Meteor',
url:'http://meteor.com'
});
Posts.insert({
title:'TheMeteorBook',
url:'http://themeteorbook.com'
});
}
server/fixtures.js
Commit 4-2
Datos para la coleccin de posts.
Ver en GitHubLanzar instancia
Datos dinmicos
Si abrimos una consola de navegador, veremos los tres mensajes cargados
desde MiniMongo:
Posts.find().fetch();
client/templates/posts/posts_list.js
Commit 4-3
Conexin entre la coleccin Posts y la plantilla `postList`.
Ver en GitHubLanzar instancia
Meteor.publish('posts',function(){
returnPosts.find();
});
server/publications.js
client/main.js
Commit 4-4
`autopublish` eliminado y configurada una publicacin bs
Ver en GitHubLanzar instancia
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
SIDEBAR4.5
magia). As que vamos a desenvolver las capas de dicha magia para tratar
de entender lo que est pasando.
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:
});
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.
});
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
returnPosts.find({author:'bobsmith',category:'JavaScript'});
}
});
Autopublicacin
Si creas un proyecto Meteor desde cero (es decir, usando meteorcreate), 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 automticamente a una coleccin
llamada 'posts' en el cliente.
As que si usas autopublish, no necesitas pensar en publicaciones. Los datos
son omnipresentes, y todo es ms sencillo. Por supuesto, aparecen
problemas obvios al tener una copia completa de la base de datos en la
cach de cada usuario.
Por esta razn, autopublish solo es apropiado cuando estamos empezando,
cundo todava no se ha pensado en las publicaciones.
Toma todos los documentos que coinciden con el cursor y los enva al
cliente en una coleccin del mismo nombre. (Para hacer esto,
utiliza .added()).
date:false
}});
});
Recapitulando
Hemos visto cmo pasar de publicar todas las propiedades de todos los
documentos de todas las colecciones (conautopublish) 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
5
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 formahttp://myapp.com/posts/xyz (donde xyz es un
identificador _id de MongoDB) que sea nica para cada post.
Terminal
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 (que colocaremos
dentro del directorio client/templates/application).
Iron Router se ocupar de insertar nuestro layout en
nuestro main.html adelgazado, que ahora quedar as:
<head>
<title>Microscope</title>
</head>
client/main.html
</div>
</template>
client/templates/application/layout.html
Router.route('/',{name:'postsList'});
lib/router.js
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.
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(){returnMeteor.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.
client/templates/includes/loading.html
Ten en cuenta que {{>spinner}} est contenido en el paquete spin. A pesar de
Solo hay un problema: no podemos continuar definiendo rutas una por una
para cada post, ya que podra haber cientos de ellos. As que tendremos que
crear una ruta dinmica y hacer que se vea esta nos muestre cualquier post
que queremos.
Para empezar, vamos a crear una nueva plantilla post_page.html que
simplemente muestra la misma plantilla para un post que hemos utilizado
anteriormente en la lista de posts.
<templatename="postPage">
<divclass="postpagepage">
{{>postItem}}
</div>
</template>
client/templates/posts/post_page.html
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(){returnMeteor.subscribe('posts');}
});
Router.route('/',{name:'postsList'});
Router.route('/posts/:_id',{
name:'postPage',
data:function(){returnPosts.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 : dentro de nuestro path).
Ms acerca de los contextos de datos
</div>
<ahref="{{pathFor'postPage'}}"class="discussbtnbtn
default">Discuss</a>
</div>
</template>
client/templates/posts/post_item.html
Commit 5-3
Ruta para un nico post.
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:
<templatename="notFound">
<divclass="notfoundpagejumbotron">
<h2>404</h2>
<p>Sorry,wecouldn'tfindapageatthisaddress.</p>
</div>
</template>
client/templates/application/not_found.html
Router.configure({
layoutTemplate:'layout',
loadingTemplate:'loading',
notFoundTemplate:'notFound',
waitOn:function(){returnMeteor.subscribe('posts');}
});
//...
lib/router.js
Para probar la nueva pgina de error, puedes intentar introducir una URL
aleatoria comohttp://localhost:3000/nothinghere.
Pero un momento, qu pasa si alguien introduce una URL de la
forma http://localhost:3000/posts/xyz, dondexyz no es un identificador _id de
post vlido? Esto es una ruta vlida, pero no apunta a ningn dato.
Afortunadamente, Iron Router es lo suficientemente inteligente para saber
esto si definimos un hook especialdataNotFound al final de router.js:
//...
Router.onBeforeAction('dataNotFound',{only:'postPage'});
lib/router.js
Commit 5-4
Aadida la plantilla de no encontrado.
Ver en GitHubLanzar 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
SIDEBAR5.5
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:
Session.set('pageTitle','Adifferenttitle');
client/templates/application/layout.html
Template.layout.helpers({
pageTitle:function(){returnSession.get('pageTitle');}
});
client/templates/application/layout.js
Session.set('pageTitle','Abrandnewtitle');
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'));
}
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.
Browser console
Aadiendo usuarios
6
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: {{>
loginButtonsalign="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/). Estamos utilizando
cdigo y clases como se indica en Bootstrap para que todo se vea mejor:
<templatename="layout">
<divclass="container">
{{>header}}
<divid="main">
{{>yield}}
</div>
</div>
</template>
client/templates/application/layout.html
<templatename="header">
<navclass="navbarnavbardefault"role="navigation">
<divclass="navbarheader">
<buttontype="button"class="navbartogglecollapsed"data
toggle="collapse"datatarget="#navigation">
<spanclass="sronly">Togglenavigation</span>
<spanclass="iconbar"></span>
<spanclass="iconbar"></span>
<spanclass="iconbar"></span>
</button>
<aclass="navbarbrand"href="{{pathFor'postsList'}}">Microscope</a>
</div>
<divclass="collapsenavbarcollapse"id="navigation">
<ulclass="navnavbarnavnavbarright">
{{>loginButtons}}
</ul>
</div>
</nav>
</template>
client/templates/includes/header.html
client/helpers/config.js
Commit 6-1
Aadidas cuentas de usuario y una plantilla en la cabecera
Ver en GitHubLanzar instancia
Meteor.users.find().count();
1
Consola de Mongo
Consola de Mongo
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 ladocumentacin de Meteor para ver
cmo se hace.
Reactividad
SIDEBAR6.5
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:
<templatename="postsList">
<ul>
{{#eachposts}}
<li>{{title}}</li>
{{/each}}
</ul>
</template>
Cuando cambian los datos reactivos, es Meteor el que, entre bastidores, est
creando callbacks observe(), y redibujando las secciones pertinentes del
HTML.
>Posts.insert({title:'NewPost'});
Thereare4posts.
Creando posts
7
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.route('/',{name:'postsList'});
Router.route('/posts/:_id',{
name:'postPage',
data:function(){returnPosts.findOne(this.params._id);}
});
Router.route('/submit',{name:'postSubmit'});
Router.onBeforeAction('dataNotFound',{only:'postPage'});
lib/router.js
<spanclass="iconbar"></span>
</button>
<aclass="navbarbrand"href="{{pathFor'postsList'}}">Microscope</a>
</div>
<divclass="collapsenavbarcollapse"id="navigation">
<ulclass="navnavbarnav">
<li><ahref="{{pathFor'postSubmit'}}">SubmitPost</a></li>
</ul>
<ulclass="navnavbarnavnavbarright">
{{>loginButtons}}
</ul>
</div>
</nav>
</template>
client/templates/includes/header.html
<labelclass="controllabel"for="url">URL</label>
<divclass="controls">
<inputname="url"id="url"type="text"value=""placeholder="Your
URL"class="formcontrol"/>
</div>
</div>
<divclass="formgroup">
<labelclass="controllabel"for="title">Title</label>
<divclass="controls">
<inputname="title"id="title"type="text"value=""
placeholder="Nameyourpost"class="formcontrol"/>
</div>
</div>
<inputtype="submit"value="Submit"class="btnbtnprimary"/>
</form>
</template>
client/templates/posts/post_submit.html
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 por ejemplo pulsar intro).
Template.postSubmit.events({
'submitform':function(e){
e.preventDefault();
varpost={
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 GitHubLanzar 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() del router para que
nos lleve a la pgina correcta.
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.
Terminal
Posts.allow({
insert:function(userId,doc){
//onlyallowpostingifyouareloggedin
return!!userId;
}
});
lib/collections/posts.js
Commit 7-2
Eliminado el paquete `insecure` y permitido aadir posts
Ver en GitHubLanzar instancia
Router.route('/',{name:'postsList'});
Router.route('/posts/:_id',{
name:'postPage',
data:function(){returnPosts.findOne(this.params._id);}
});
Router.route('/submit',{name:'postSubmit'});
varrequireLogin=function(){
if(!Meteor.user()){
this.render('accessDenied');
}else{
this.next();
}
}
Router.onBeforeAction('dataNotFound',{only:'postPage'});
Router.onBeforeAction(requireLogin,{only:'postSubmit'});
lib/router.js
client/templates/includes/access_denied.html
Commit 7-3
Acceso denegado al envo de posts a usuarios no registrados.
Ver en GitHubLanzar instancia
varrequireLogin=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 GitHubLanzar instancia
Ocultando el enlace
La forma fcil de evitar que los usuarios lleguen al formulario es esconder el
enlace. Podemos hacerlo fcilmente desdeheader.html:
//...
<ulclass="navnavbarnav">
{{#ifcurrentUser}}<li><ahref="{{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 GitHubLanzar instancia
Aadir detalles sobre el autor del post (ID, nombre de usuario, etc.)
Los clientes no conocern todas las URL publicadas. Solo conocen los
posts que pueden ver en ese momento (veremos porqu), as que no
podemos asegurar desde el lado del cliente que las URLs sean nicas.
varpost={
url:$(e.target).find('[name=url]').val(),
title:$(e.target).find('[name=title]').val()
};
Meteor.call('postInsert',post,function(error,result){
//displaytheerrortotheuserandabort
if(error)
returnalert(error.reason);
Router.go('postPage',{_id:result._id});
});
}
});
client/templates/posts/post_submit.js
La funcin Meteor.call llama a un mtodo nombrado por su primer
Comprobaciones de seguridad
Aprovecharemos esta oportunidad para aadir algo de seguridad a nuestros
mtodos usando el paquete auditargumentchecks.
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, para no terminar insertando cualquier dato extrao en
nuestra base de datos.
Vamos a definir el mtodo postInsert en nuestro fichero collections/posts.js.
Eliminaremos el bloque allow()del fichero posts.js porque usando mtodos,
Meteor no lo evala.
Extenderemos (extend) el objeto postAttributes con tres propiedades ms: el
identificador del usuario _id y elusername, adems de la fecha y
hora submitted, antes de insertarlos en nuestra base de datos y devolver
el _id al cliente (en otras palabras, a la funcin original que llam a este
mtodo) como un objeto JavaScript.
Posts=newMongo.Collection('posts');
Meteor.methods({
postInsert:function(postAttributes){
check(Meteor.userId(),String);
check(postAttributes,{
title:String,
url:String
});
varuser=Meteor.user();
varpost=_.extend(postAttributes,{
userId:user._id,
author:user.username,
submitted:newDate()
});
varpostId=Posts.insert(post);
return{
_id:postId
};
}
});
lib/collections/posts.js
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
});
varpostWithSameLink=Posts.findOne({url:postAttributes.url});
if(postWithSameLink){
return{
postExists:true,
_id:postWithSameLink._id
}
}
varuser=Meteor.user();
varpost=_.extend(postAttributes,{
userId:user._id,
author:user.username,
submitted:newDate()
});
varpostId=Posts.insert(post);
return{
_id:postId
};
}
});
lib/collections/posts.js
varpost={
url:$(e.target).find('[name=url]').val(),
title:$(e.target).find('[name=title]').val()
};
Meteor.call('postInsert',post,function(error,result){
//displaytheerrortotheuserandabort
if(error)
returnalert(error.reason);
//showthisresultbutrouteanyway
if(result.postExists)
alert('Thislinkhasalreadybeenposted');
Router.go('postPage',{_id:result._id});
});
}
});
client/templates/posts/post_submit.js
Commit 7-7
Forzando la unicidad de las URLs.
Ver en GitHubLanzar instancia
Ahora que tenemos una fecha de envo en todos nuestros posts, tiene
sentido asegurarnos que se estn ordenando usando este atributo. Para ello
usaremos el operador sort de Mongo que espera un objeto que consta de las
claves de ordenacin, y un signo que indica si son ascendentes o
descendentes:
Template.postsList.helpers({
posts:function(){
returnPosts.find({},{sort:{submitted:1}});
}
});
client/templates/posts/posts_list.js
Commit 7-8
Posts ordenados por fecha de envo.
Ver en GitHubLanzar instancia
Compensacin de la latencia
SIDEBAR7.5
Compensacin de la latencia
Posts=newMongo.Collection('posts');
Meteor.methods({
postInsert:function(postAttributes){
check(this.userId,String);
check(postAttributes,{
title:String,
url:String
});
if(Meteor.isServer){
postAttributes.title+="(server)";
//waitfor5seconds
Meteor._sleepForMs(5000);
}else{
postAttributes.title+="(client)";
}
varpostWithSameLink=Posts.findOne({url:postAttributes.url});
if(postWithSameLink){
return{
postExists:true,
_id:postWithSameLink._id
}
}
varuser=Meteor.user();
varpost=_.extend(postAttributes,{
userId:user._id,
author:user.username,
submitted:newDate()
});
varpostId=Posts.insert(post);
return{
_id:postId
};
}
});
collections/posts.js
varpost={
url:$(e.target).find('[name=url]').val(),
title:$(e.target).find('[name=title]').val()
};
Meteor.call('postInsert',post,function(error,result){
//displaytheerrortotheuserandabort
if(error)
returnalert(error.reason);
//showthisresultbutrouteanyway
if(result.postExists)
alert('Thislinkhasalreadybeenposted');
Router.go('postPage',{_id:result._id});
});
}
});
client/templates/posts/post_submit.js
varpost={
url:$(e.target).find('[name=url]').val(),
title:$(e.target).find('[name=title]').val()
};
Meteor.call('postInsert',post,function(error,result){
//displaytheerrortotheuserandabort
if(error)
returnalert(error.reason);
//showthisresultbutrouteanyway
if(result.postExists)
alert('Thislinkhasalreadybeenposted');
});
Router.go('postsList');
});
client/templates/posts/post_submit.js
Commit 7-5-1
Demostrar el orden en el que aparecen los posts usando un
Ver en GitHubLanzar instancia
Nuestro post una vez que el cliente recibe la actualizacin del servidor
Editando posts
8
Router.route('/',{name:'postsList'});
Router.route('/posts/:_id',{
name:'postPage',
data:function(){returnPosts.findOne(this.params._id);}
});
Router.route('/posts/:_id/edit',{
name:'postEdit',
data:function(){returnPosts.findOne(this.params._id);}
});
Router.route('/submit',{name:'postSubmit'});
varrequireLogin=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
<inputtype="submit"value="Submit"class="btnbtnprimarysubmit"/>
<hr/>
<aclass="btnbtndangerdelete"href="#">Deletepost</a>
</form>
</template>
client/templates/posts/post_edit.html
Y aqu tenemos el fichero post_edit.js que la acompaa:
Template.postEdit.events({
'submitform':function(e){
e.preventDefault();
varcurrentPostId=this._id;
varpostProperties={
url:$(e.target).find('[name=url]').val(),
title:$(e.target).find('[name=title]').val()
}
Posts.update(currentPostId,{$set:postProperties},function(error){
if(error){
//displaytheerrortotheuser
alert(error.reason);
}else{
Router.go('postPage',{_id:currentPostId});
}
});
},
'click.delete':function(e){
e.preventDefault();
if(confirm("Deletethispost?")){
varcurrentPostId=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:
<templatename="postItem">
<divclass="post">
<divclass="postcontent">
<h3><ahref="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
submittedby{{author}}
{{#ifownPost}}<ahref="{{pathFor'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<ahref="{{pathFor'postPage'}}"class="discussbtnbtn
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(){
returnthis.userId===Meteor.userId();
},
domain:function(){
vara=document.createElement('a');
a.href=this.url;
returna.hostname;
}
});
client/templates/posts/post_item.js
Formulario de edicin.
Commit 8-1
Aadido el formulario de edicin.
Ver en GitHubLanzar instancia
lib/permissions.js
Posts.allow({
update:function(userId,post){returnownsDocument(userId,post);},
remove:function(userId,post){returnownsDocument(userId,post);},
});
//...
lib/collections/posts.js
Commit 8-2
Aadidos permisos bsicos para comprobar el dueo del post.
Ver en GitHubLanzar instancia
Posts.allow({
update:ownsDocument,
remove:ownsDocument
});
Posts.deny({
update:function(userId,post,fieldNames){
//mayonlyeditthefollowingtwofields:
return(_.without(fieldNames,'url','title').length>0);
}
});
lib/collections/posts.js
Commit 8-3
Permitir cambios slo en ciertos campos.
Ver en GitHubLanzar instancia
Estamos cogiendo el array fieldNames que contiene la lista de los campos que
quieren modificar, y usamos el mtodowithout() 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.
Mtodos en el servidor vs. mtodos en el cliente
Para crear un post, estamos utilizando un mtodo postInsert en el servidor,
mientras que para editarlos y eliminarlos, llamamos
a update y remove directamente desde el cliente, limitando el acceso a travs
deallow y deny.
Cundo es ms adecuado usar uno u otro?
Cuando las cosas son relativamente sencillas se pueden expresar las reglas a
travs de allow 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:
Permitir y Denegar
SIDEBAR8.5
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.
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) y ponerla en
el directorio/lib).
Errores
9
Resulta poco elegante usar un alert() para advertir al usuario de que algo ha
pasado. Hay que hacerlo mejor.
client/helpers/errors.js
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:
<templatename="layout">
<divclass="container">
{{>header}}
{{>errors}}
<divid="main">
{{>yield}}
</div>
</div>
</template>
client/templates/application/layout.html
{{/each}}
</div>
</template>
<templatename="error">
<divclass="alertalertdanger"role="alert">
<buttontype="button"class="close"datadismiss="alert">×</button>
{{message}}
</div>
</template>
client/templates/includes/errors.html
}
});
client/templates/includes/errors.js
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({
'submitform':function(e){
e.preventDefault();
varpost={
url:$(e.target).find('[name=url]').val(),
title:$(e.target).find('[name=title]').val()
};
Meteor.call('postInsert',post,function(error,result){
//displaytheerrortotheuserandabort
if(error)
returnthrowError(error.reason);
//showthisresultbutrouteanyway
if(result.postExists)
throwError('Thislinkhasalreadybeenposted');
Router.go('postPage',{_id:result._id});
});
}
});
client/templates/posts/post_submit.js
e.preventDefault();
varcurrentPostId=this._id;
varpostProperties={
url:$(e.target).find('[name=url]').val(),
title:$(e.target).find('[name=title]').val()
}
Posts.update(currentPostId,{$set:postProperties},function(error){
if(error){
//displaytheerrortotheuser
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 GitHubLanzar instancia
Disparando un error
//...
.alert{
animation:fadeOut2700mseasein0s1forwards;
//...
}
client/stylesheets/style.css
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(){
returnErrors.find();
}
});
Template.error.onRendered(function(){
varerror=this.data;
Meteor.setTimeout(function(){
Errors.remove(error._id);
},3000);
});
client/templates/includes/errors.js
Commit 9-3
<divclass="formgroup{{errorClass'title'}}">
<labelclass="controllabel"for="title">Title</label>
<divclass="controls">
<inputname="title"id="title"type="text"value=""
placeholder="Nameyourpost"class="formcontrol"/>
<spanclass="helpblock">{{errorMessage'title'}}</span>
</div>
</div>
<inputtype="submit"value="Submit"class="btnbtnprimary"/>
</form>
</template>
client/templates/posts/post_submit.html
Template.postSubmit.helpers({
errorMessage:function(field){
returnSession.get('postSubmitErrors')[field];
},
errorClass:function(field){
return!!Session.get('postSubmitErrors')[field]?'haserror':'';
}
});
//...
client/templates/posts/post_submit.js
validatePost=function(post){
varerrors={};
if(!post.title)
errors.title="Pleasefillinaheadline";
if(!post.url)
errors.url="PleasefillinaURL";
returnerrors;
}
//...
lib/collections/posts.js
varpost={
url:$(e.target).find('[name=url]').val(),
title:$(e.target).find('[name=title]').val()
};
varerrors=validatePost(post);
if(errors.title||errors.url)
returnSession.set('postSubmitErrors',errors);
Meteor.call('postInsert',post,function(error,result){
//displaytheerrortotheuserandabort
if(error)
returnthrowError(error.reason);
//showthisresultbutrouteanyway
if(result.postExists)
throwError('Thislinkhasalreadybeenposted');
Router.go('postPage',{_id:result._id});
});
}
});
client/templates/posts/post_submit.js
Fjate que estamos usando return para abortar la ejecucin del ayudante si
Capturando errores.
varerrors=validatePost(postAttributes);
if(errors.title||errors.url)
thrownewMeteor.Error('invalidpost',"YoumustsetatitleandURLfor
yourpost");
varpostWithSameLink=Posts.findOne({url:postAttributes.url});
if(postWithSameLink){
return{
postExists:true,
_id:postWithSameLink._id
}
}
varuser=Meteor.user();
varpost=_.extend(postAttributes,{
userId:user._id,
author:user.username,
submitted:newDate()
});
varpostId=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:
Meteor.call('postInsert',{url:'',title:'NoURLhere!'});
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:
<templatename="postEdit">
<formclass="mainformpage">
<divclass="formgroup{{errorClass'url'}}">
<labelclass="controllabel"for="url">URL</label>
<divclass="controls">
<inputname="url"id="url"type="text"value="{{url}}"
placeholder="YourURL"class="formcontrol"/>
<spanclass="helpblock">{{errorMessage'url'}}</span>
</div>
</div>
<divclass="formgroup{{errorClass'title'}}">
<labelclass="controllabel"for="title">Title</label>
<divclass="controls">
<inputname="title"id="title"type="text"value="{{title}}"
placeholder="Nameyourpost"class="formcontrol"/>
<spanclass="helpblock">{{errorMessage'title'}}</span>
</div>
</div>
<inputtype="submit"value="Submit"class="btnbtnprimarysubmit"/>
<hr/>
<aclass="btnbtndangerdelete"href="#">Deletepost</a>
</form>
</template>
client/templates/posts/post_edit.html
Session.set('postEditErrors',{});
});
Template.postEdit.helpers({
errorMessage:function(field){
returnSession.get('postEditErrors')[field];
},
errorClass:function(field){
return!!Session.get('postEditErrors')[field]?'haserror':'';
}
});
Template.postEdit.events({
'submitform':function(e){
e.preventDefault();
varcurrentPostId=this._id;
varpostProperties={
url:$(e.target).find('[name=url]').val(),
title:$(e.target).find('[name=title]').val()
}
varerrors=validatePost(postProperties);
if(errors.title||errors.url)
returnSession.set('postEditErrors',errors);
Posts.update(currentPostId,{$set:postProperties},function(error){
if(error){
//displaytheerrortotheuser
throwError(error.reason);
}else{
Router.go('postPage',{_id:currentPostId});
}
});
},
'click.delete':function(e){
e.preventDefault();
if(confirm("Deletethispost?")){
varcurrentPostId=this._id;
Posts.remove(currentPostId);
Router.go('postsList');
}
}
});
client/templates/posts/post_edit.js
Posts.deny({
update:function(userId,post,fieldNames,modifier){
varerrors=validatePost(modifier.$set);
returnerrors.title||errors.url;
}
});
//...
lib/collections/posts.js
Package.onUse(function(api,where){
api.versionsFrom('0.9.0');
api.use(['minimongo','mongolivedata','templating'],'client');
api.addFiles(['errors.js','errors_list.html','errors_list.js'],'client');
if(api.export)
api.export('Errors');
});
packages/tmeasday:errors/package.js
throw:function(message){
Errors.collection.insert({message:message,seen:false})
}
};
packages/tmeasday:errors/errors.js
<templatename="meteorErrors">
<divclass="errors">
{{#eacherrors}}
{{>meteorError}}
{{/each}}
</div>
</template>
<templatename="meteorError">
<divclass="alertalertdanger"role="alert">
<buttontype="button"class="close"datadismiss="alert">×</button>
{{message}}
</div>
</template>
packages/tmeasday:errors/errors_list.html
Template.meteorErrors.helpers({
errors:function(){
returnErrors.collection.find();
}
});
Template.meteorError.rendered=function(){
varerror=this.data;
Meteor.setTimeout(function(){
Errors.collection.remove(error._id);
},3000);
};
packages/tmeasday:errors/errors_list.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
Meteor.call('postInsert',post,function(error,id){
if(error){
//displaytheerrortotheuser
Errors.throw(error.reason);
client/templates/posts/post_submit.js
Posts.update(currentPostId,{$set:postProperties},function(error){
if(error){
//displaytheerrortotheuser
Errors.throw(error.reason);
//showthisresultbutrouteanyway
if(result.postExists)
Errors.throw('Thislinkhasalreadybeenposted');
client/templates/posts/post_edit.js
Commit 9-5-1
Creado y enlazado un paquete bsico.
Ver en GitHubLanzar instancia
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.
Tinytest.add("Errorscollection",function(test){
test.equal(Errors.collection.find({}).count(),0);
Errors.throw('Anewerror!');
test.equal(Errors.collection.find({}).count(),1);
Errors.collection.remove({});
});
Tinytest.addAsync("Errorstemplate",function(test,done){
Errors.throw('Anewerror!');
test.equal(Errors.collection.find({}).count(),1);
//renderthetemplate
UI.insert(UI.render(Template.meteorErrors),document.body);
Meteor.setTimeout(function(){
test.equal(Errors.collection.find({}).count(),0);
done();
},3500);
});
packages/tmeasday:errors/errors_tests.js
api.addFiles('errors_tests.js','client');
});
packages/tmeasday:errors/package.js
Commit 9-5-2
Tests aadidos al paquete.
Ver en GitHubLanzar instancia
Terminal
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 meteorpublishcreate:
cdpackages/tmeasday:errors
meteorpublishcreate
Terminal
Commit 9-5-4
Paquete eliminado del rbol de desarrollo.
Ver en GitHubLanzar 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
10
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.
Comments=newMongo.Collection('comments');
lib/collections/comments.js
//Fixturedata
if(Posts.find().count()===0){
varnow=newDate().getTime();
//createtwousers
vartomId=Meteor.users.insert({
profile:{name:'TomColeman'}
});
vartom=Meteor.users.findOne(tomId);
varsachaId=Meteor.users.insert({
profile:{name:'SachaGreif'}
});
varsacha=Meteor.users.findOne(sachaId);
vartelescopeId=Posts.insert({
title:'IntroducingTelescope',
userId:sacha._id,
author:sacha.profile.name,
url:'http://sachagreif.com/introducingtelescope/',
submitted:newDate(now7*3600*1000)
});
Comments.insert({
postId:telescopeId,
userId:tom._id,
author:tom.profile.name,
submitted:newDate(now5*3600*1000),
body:'InterestingprojectSacha,canIgetinvolved?'
});
Comments.insert({
postId:telescopeId,
userId:sacha._id,
author:sacha.profile.name,
submitted:newDate(now3*3600*1000),
body:'YousurecanTom!'
});
Posts.insert({
title:'Meteor',
userId:tom._id,
author:tom.profile.name,
url:'http://meteor.com',
submitted:newDate(now10*3600*1000)
});
Posts.insert({
title:'TheMeteorBook',
userId:tom._id,
author:tom.profile.name,
url:'http://themeteorbook.com',
submitted:newDate(now12*3600*1000)
});
}
server/fixtures.js
returnPosts.find();
});
Meteor.publish('comments',function(){
returnComments.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 GitHubLanzar instancia
Ten en cuenta que para que se carguen los nuevos datos de prueba, es
necesario ejecutar meteorreset. Despus de la restauracin, no olvides crear
una nueva cuenta y volver a entrar!
En primer lugar, hemos creado un par de usuarios (inventados),
insertndolos en la base de datos y usando los ids para seleccionarlos
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:
<templatename="postPage">
<divclass="postpagepage">
{{>postItem}}
<ulclass="comments">
{{#eachcomments}}
{{>commentItem}}
{{/each}}
</ul>
</div>
</template>
client/templates/posts/post_page.html
Template.postPage.helpers({
comments:function(){
returnComments.find({postId:this._id});
}
});
client/templates/posts/post_page.js
Ponemos el bloque {{#eachcomments}} dentro de la plantilla del post, por lo
que this es un post para el ayudantecomments. Para encontrar los comentarios
adecuados, buscamos los que estn vinculados a ese post a travs depostId.
client/templates/comments/comment_item.html
submittedText:function(){
returnthis.submitted.toString();
}
});
client/templates/comments/comment_item.js
client/templates/posts/post_item.html
Y aadimos el ayudante commentsCount a post_item.js:
Template.postItem.helpers({
ownPost:function(){
returnthis.userId===Meteor.userId();
},
domain:function(){
vara=document.createElement('a');
a.href=this.url;
returna.hostname;
},
commentsCount:function(){
returnComments.find({postId:this._id}).count();
}
});
client/templates/posts/post_item.js
Commit 10-2
Mostrar los comentarios en `postPage`.
Ver en GitHubLanzar instancia
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:
<templatename="postPage">
<divclass="postpagepage">
{{>postItem}}
<ulclass="comments">
{{#eachcomments}}
{{>commentItem}}
{{/each}}
</ul>
{{#ifcurrentUser}}
{{>commentSubmit}}
{{else}}
<p>Pleaselogintoleaveacomment.</p>
{{/if}}
</div>
</template>
client/templates/posts/post_page.html
client/templates/comments/comment_submit.html
});
Template.commentSubmit.helpers({
errorMessage:function(field){
returnSession.get('commentSubmitErrors')[field];
},
errorClass:function(field){
return!!Session.get('commentSubmitErrors')[field]?'haserror':'';
}
});
Template.commentSubmit.events({
'submitform':function(e,template){
e.preventDefault();
var$body=$(e.target).find('[name=body]');
varcomment={
body:$body.val(),
postId:template.data._id
};
varerrors={};
if(!comment.body){
errors.body="Pleasewritesomecontent";
returnSession.set('commentSubmitErrors',errors);
}
Meteor.call('commentInsert',comment,function(error,commentId){
if(error){
throwError(error.reason);
}else{
$body.val('');
}
});
}
});
client/templates/comments/comment_submit.js
Comments=newMongo.Collection('comments');
Meteor.methods({
commentInsert:function(commentAttributes){
check(this.userId,String);
check(commentAttributes,{
postId:String,
body:String
});
varuser=Meteor.user();
varpost=Posts.findOne(commentAttributes.postId);
if(!post)
thrownewMeteor.Error('invalidcomment','Youmustcommentonapost');
comment=_.extend(commentAttributes,{
userId:user._id,
author:user.username,
submitted:newDate()
});
returnComments.insert(comment);
}
});
lib/collections/comments.js
Commit 10-3
Creado el formulario de envo de comentarios.
Ver en GitHubLanzar instancia
notFoundTemplate:'notFound',
waitOn:function(){
returnMeteor.subscribe('posts');
}
});
lib/router.js
Router.route('/posts/:_id',{
name:'postPage',
waitOn:function(){
returnMeteor.subscribe('comments',this.params._id);
},
data:function(){returnPosts.findOne(this.params._id);}
});
//...
lib/router.js
Meteor.publish('comments',function(postId){
check(postId,String);
returnComments.find({postId:postId});
});
server/publications.js
Commit 10-4
Creado un mecanismo simple de publicacin/suscripcin par
Ver en GitHubLanzar instancia
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 del
gestorclient/views/posts/post_item.js, Meteor no encuentra los datos
necesarios en el lado del cliente para devolver un resultado.
La mejor manera de resolver esto es denormalizar el nmero de comentarios
dentro del post (si no sabes lo que significa denormalizar, no te preocupes, lo
veremos en el prximo captulo). Aunque, como veremos, hay que aadir
un poco de complejidad a nuestro cdigo, a cambio, mejoramos la velocidad
al no tener que publicar todos los comentarios de la base de datos solo para
contarlos.
Lo conseguiremos aadiendo una propiedad commentsCount a la estructura de
datos del post (y restableceremos Meteor con meteorreset - no olvides volver
a crear una cuenta de usuario):
//Fixturedata
if(Posts.find().count()===0){
varnow=newDate().getTime();
//createtwousers
vartomId=Meteor.users.insert({
profile:{name:'TomColeman'}
});
vartom=Meteor.users.findOne(tomId);
varsachaId=Meteor.users.insert({
profile:{name:'SachaGreif'}
});
varsacha=Meteor.users.findOne(sachaId);
vartelescopeId=Posts.insert({
title:'IntroducingTelescope',
userId:sacha._id,
author:sacha.profile.name,
url:'http://sachagreif.com/introducingtelescope/',
submitted:newDate(now7*3600*1000),
commentsCount:2
});
Comments.insert({
postId:telescopeId,
userId:tom._id,
author:tom.profile.name,
submitted:newDate(now5*3600*1000),
body:'InterestingprojectSacha,canIgetinvolved?'
});
Comments.insert({
postId:telescopeId,
userId:sacha._id,
author:sacha.profile.name,
submitted:newDate(now3*3600*1000),
body:'YousurecanTom!'
});
Posts.insert({
title:'Meteor',
userId:tom._id,
author:tom.profile.name,
url:'http://meteor.com',
submitted:newDate(now10*3600*1000),
commentsCount:0
});
Posts.insert({
title:'TheMeteorBook',
userId:tom._id,
author:tom.profile.name,
url:'http://themeteorbook.com',
submitted:newDate(now12*3600*1000),
commentsCount:0
});
}
server/fixtures.js
varpost=_.extend(postAttributes,{
userId:user._id,
author:user.username,
submitted:newDate(),
commentsCount:0
});
varpostId=Posts.insert(post);
//...
lib/collections/posts.js
comment=_.extend(commentAttributes,{
userId:user._id,
author:user.username,
submitted:newDate()
});
//updatethepostwiththenumberofcomments
Posts.update(comment.postId,{$inc:{commentsCount:1}});
returnComments.insert(comment);
//...
lib/collections/comments.js
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
SIDEBAR10.5
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.
Una publicacin especial
Sera posible crear una publicacin especial que solo enve la cantidad de
comentarios que nos interesan (por ejemplo, la cantidad de comentarios en
los posts que actualmente estamos viendo, haciendo ms consultas al
servidor).
Pero vale la pena considerar si la complejidad de dicha publicacin superara
o no las dificultades creadas al denormalizar
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.
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:
Notifications=newMongo.Collection('notifications');
Notifications.allow({
update:function(userId,doc,fieldNames){
returnownsDocument(userId,doc)&&
fieldNames.length===1&&fieldNames[0]==='read';
}
});
createCommentNotification=function(comment){
varpost=Posts.findOne(comment.postId);
if(comment.userId!==post.userId){
Notifications.insert({
userId:post.userId,
postId:post._id,
commentId:comment._id,
commenterName:comment.author,
read:false
});
}
};
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 siempre que se trate de los datos del propio usuario.
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) y
llamamos a la funcincreateCommentNotification:
Comments=newMongo.Collection('comments');
Meteor.methods({
commentInsert:function(commentAttributes){
//...
comment=_.extend(commentAttributes,{
userId:user._id,
author:user.username,
submitted:newDate()
});
//updatethepostwiththenumberofcomments
Posts.update(comment.postId,{$inc:{commentsCount:1}});
//createthecomment,savetheid
comment._id=Comments.insert(comment);
//nowcreateanotification,informingtheuserthatthere'sbeena
comment
createCommentNotification(comment);
returncomment._id;
}
});
lib/collections/comments.js
Meteor.publish('comments',function(postId){
check(postId,String);
returnComments.find({postId:postId});
});
Meteor.publish('notifications',function(){
returnNotifications.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 GitHubLanzar instancia
<aclass="navbarbrand"href="{{pathFor'postsList'}}">Microscope</a>
</div>
<divclass="collapsenavbarcollapse"id="navigation">
<ulclass="navnavbarnav">
{{#ifcurrentUser}}
<li>
<ahref="{{pathFor'postSubmit'}}">SubmitPost</a>
</li>
<liclass="dropdown">
{{>notifications}}
</li>
{{/if}}
</ul>
<ulclass="navnavbarnavnavbarright">
{{>loginButtons}}
</ul>
</div>
</nav>
</template>
client/templates/includes/header.html
Y crear las plantillas notifications y notificationItem (que pondremos en el
archivo notifications.html):
<templatename="notifications">
<ahref="#"class="dropdowntoggle"datatoggle="dropdown">
Notifications
{{#ifnotificationCount}}
<spanclass="badgebadgeinverse">{{notificationCount}}</span>
{{/if}}
<bclass="caret"></b>
</a>
<ulclass="notificationdropdownmenu">
{{#ifnotificationCount}}
{{#eachnotifications}}
{{>notificationItem}}
{{/each}}
{{else}}
<li><span>NoNotifications</span></li>
{{/if}}
</ul>
</template>
<templatename="notificationItem">
<li>
<ahref="{{notificationPostPath}}">
<strong>{{commenterName}}</strong>commentedonyourpost
</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(){
returnNotifications.find({userId:Meteor.userId(),read:false});
},
notificationCount:function(){
returnNotifications.find({userId:Meteor.userId(),read:false}).count();
}
});
Template.notificationItem.helpers({
notificationPostPath:function(){
returnRouter.routes.postPage.path({_id:this.postId});
}
});
Template.notificationItem.events({
'clicka':function(){
Notifications.update(this._id,{$set:{read:true}});
}
});
client/templates/notifications/notifications.js
Commit 11-2
Mostrar las notificaciones en la cabecera.
Ver en GitHubLanzar 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.
server/publications.js
Commit 11-3
Sincronizar solo las notificaciones relevantes al usuario.
Ver en GitHubLanzar instancia
Reactividad avanzada
SIDEBAR11.5
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.
currentLikeCount=function(){
_currentLikeCountListeners.depend();
return_currentLikeCount;
}
Meteor.setInterval(function(){
varpostId;
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);
Meteor.setInterval(function(){
varpostId;
if(Meteor.user()&&postId=Session.get('currentPostId')){
getFacebookLikeCount(Meteor.user(),Posts.find(postId),
function(err,count){
if(!err){
currentLikeCount.set(count);
}
});
}
},5*1000);
});
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.
//...
Posts.insert({
title:'TheMeteorBook',
userId:tom._id,
author:tom.profile.name,
url:'http://themeteorbook.com',
submitted:newDate(now12*3600*1000),
commentsCount:0
});
for(vari=0;i<10;i++){
Posts.insert({
title:'Testpost#'+i,
author:sacha.profile.name,
userId:sacha._id,
url:'http://google.com/?q=test'+i,
submitted:newDate(nowi*3600*1000),
commentsCount:0
});
}
}
server/fixtures.js
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 ashasta 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 de mensajes que debe enviar al cliente. Se da la
circunstancia de que ya estamos suscritos a la publicacin posts en el router,
as que vamos a aprovecharlo y a dejar que el router maneje tambin la
paginacin.
La forma ms fcil de configurar esto es hacer que el parmetro lmite forme
parte de la ruta, quedando las URL de la forma http://localhost:3000/25. Una
ventaja aadida de utilizar la URL en vez de otros mtodos es que si estamos
viendo 25 posts y resulta que se recarga la pgina por error, todava
seguiremos viendo 25 posts.
Para hacer esto correctamente, tenemos que cambiar la forma en que nos
suscribimos a los posts. Al igual que hicimos en el captulo Comentarios,
tendremos que mover nuestro cdigo de suscripcin desde el nivel de router
al nivel de ruta.
Parece demasiado para hacerlo todo de una sola vez, pero se ver ms claro
escribiendo el cdigo.
En primer lugar, vamos a dejar de suscribirnos a la publicacin posts en el
bloque Router.configure(). Simplemente elimina Meteor.subscribe('posts'),
dejando solo la 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
Router.route('/:postsLimit?',{
name:'postsList',
waitOn:function(){
varlimit=parseInt(this.params.postsLimit)||5;
returnMeteor.subscribe('posts',{sort:{submitted:1},limit:limit});
}
});
//...
lib/router.js
Meteor.publish('posts',function(options){
check(options,{
sort:Object,
limit:Number
});
returnPosts.find({},options);
});
Meteor.publish('comments',function(postId){
check(postId,String);
returnComments.find({postId:postId});
});
Meteor.publish('notifications',function(){
returnNotifications.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.
Router.route('/:postsLimit?',{
name:'postsList',
waitOn:function(){
varlimit=parseInt(this.params.postsLimit)||5;
returnMeteor.subscribe('posts',{sort:{submitted:1},limit:limit});
},
data:function(){
varlimit=parseInt(this.params.postsLimit)||5;
return{
posts:Posts.find({},{sort:{submitted:1},limit:limit})
};
}
});
//...
lib/router.js
return[Meteor.subscribe('notifications')]
}
});
Router.route('/posts/:_id',{
name:'postPage',
waitOn:function(){
returnMeteor.subscribe('comments',this.params._id);
},
data:function(){returnPosts.findOne(this.params._id);}
});
Router.route('/posts/:_id/edit',{
name:'postEdit',
data:function(){returnPosts.findOne(this.params._id);}
});
Router.route('/submit',{name:'postSubmit'});
Router.route('/:postsLimit?',{
name:'postsList',
waitOn:function(){
varlimit=parseInt(this.params.postsLimit)||5;
returnMeteor.subscribe('posts',{sort:{submitted:1},limit:limit});
},
data:function(){
varlimit=parseInt(this.params.postsLimit)||5;
return{
posts:Posts.find({},{sort:{submitted:1},limit:limit})
};
}
});
varrequireLogin=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 12-2
Aumentada la ruta `postsList` para que tenga un lmite.
Ver en GitHubLanzar instancia
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(){
returnparseInt(this.params.postsLimit)||this.increment;
},
findOptions:function(){
return{sort:{submitted:1},limit:this.postsLimit()};
},
waitOn:function(){
returnMeteor.subscribe('posts',this.findOptions());
},
data:function(){
return{posts:Posts.find({},this.findOptions())};
}
});
//...
Router.route('/:postsLimit?',{
name:'postsList'
});
//...
lib/router.js
PostsListController=RouteController.extend({
template:'postsList',
increment:5,
postsLimit:function(){
returnparseInt(this.params.postsLimit)||this.increment;
},
findOptions:function(){
return{sort:{submitted:1},limit:this.postsLimit()};
},
waitOn:function(){
returnMeteor.subscribe('posts',this.findOptions());
},
posts:function(){
returnPosts.find({},this.findOptions());
},
data:function(){
varhasMore=this.posts().count()===this.postsLimit();
varnextPath=this.route.path({postsLimit:this.postsLimit()+
this.increment});
return{
posts:this.posts(),
nextPath:hasMore?nextPath:null
};
}
});
//...
lib/router.js
{{#ifnextPath}}
<aclass="loadmore"href="{{nextPath}}">Loadmore</a>
{{/if}}
</div>
</template>
client/templates/posts/posts_list.html
PostsListController=RouteController.extend({
template:'postsList',
increment:5,
postsLimit:function(){
returnparseInt(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(){
returnPosts.find({},this.findOptions());
},
data:function(){
varhasMore=this.posts().count()===this.postsLimit();
varnextPath=this.route.path({postsLimit:this.postsLimit()+
this.increment});
return{
posts:this.posts(),
ready:this.postsSub.ready,
nextPath:hasMore?nextPath:null
};
}
});
//...
lib/router.js
{{#ifnextPath}}
<aclass="loadmore"href="{{nextPath}}">Loadmore</a>
{{else}}
{{#unlessready}}
{{>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 GitHubLanzar instancia
Meteor.publish('singlePost',function(id){
check(id,String)
returnPosts.find(id);
});
//...
server/publications.js
Router.route('/posts/:_id',{
name:'postPage',
waitOn:function(){
return[
Meteor.subscribe('singlePost',this.params._id),
Meteor.subscribe('comments',this.params._id)
];
},
data:function(){returnPosts.findOne(this.params._id);}
});
Router.route('/posts/:_id/edit',{
name:'postEdit',
waitOn:function(){
returnMeteor.subscribe('singlePost',this.params._id);
},
data:function(){returnPosts.findOne(this.params._id);}
});
//...
lib/router.js
Commit 12-6
Usando una sola suscripcin a los posts para asegurarnos
Ver en GitHubLanzar 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.
//createtwousers
vartomId=Meteor.users.insert({
profile:{name:'TomColeman'}
});
vartom=Meteor.users.findOne(tomId);
varsachaId=Meteor.users.insert({
profile:{name:'SachaGreif'}
});
varsacha=Meteor.users.findOne(sachaId);
vartelescopeId=Posts.insert({
title:'IntroducingTelescope',
userId:sacha._id,
author:sacha.profile.name,
url:'http://sachagreif.com/introducingtelescope/',
submitted:newDate(now7*3600*1000),
commentsCount:2,
upvoters:[],
votes:0
});
Comments.insert({
postId:telescopeId,
userId:tom._id,
author:tom.profile.name,
submitted:newDate(now5*3600*1000),
body:'InterestingprojectSacha,canIgetinvolved?'
});
Comments.insert({
postId:telescopeId,
userId:sacha._id,
author:sacha.profile.name,
submitted:newDate(now3*3600*1000),
body:'YousurecanTom!'
});
Posts.insert({
title:'Meteor',
userId:tom._id,
author:tom.profile.name,
url:'http://meteor.com',
submitted:newDate(now10*3600*1000),
commentsCount:0,
upvoters:[],
votes:0
});
Posts.insert({
title:'TheMeteorBook',
userId:tom._id,
author:tom.profile.name,
url:'http://themeteorbook.com',
submitted:newDate(now12*3600*1000),
commentsCount:0,
upvoters:[],
votes:0
});
for(vari=0;i<10;i++){
Posts.insert({
title:'Testpost#'+i,
author:sacha.profile.name,
userId:sacha._id,
url:'http://google.com/?q=test'+i,
submitted:newDate(nowi*3600*1000+1),
commentsCount:0,
upvoters:[],
votes:0
});
}
}
server/fixtures.js
varpostWithSameLink=Posts.findOne({url:postAttributes.url});
if(postWithSameLink){
return{
postExists:true,
_id:postWithSameLink._id
}
}
varuser=Meteor.user();
varpost=_.extend(postAttributes,{
userId:user._id,
author:user.username,
submitted:newDate(),
commentsCount:0,
upvoters:[],
votes:0
});
varpostId=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:
<templatename="postItem">
<divclass="post">
<ahref="#"class="upvotebtnbtndefault"></a>
<divclass="postcontent">
<h3><ahref="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
<p>
{{votes}}Votes,
submittedby{{author}},
<ahref="{{pathFor'postPage'}}">{{commentsCount}}comments</a>
{{#ifownPost}}<ahref="{{pathFor'postEdit'}}">Edit</a>{{/if}}
</p>
</div>
<ahref="{{pathFor'postPage'}}"class="discussbtnbtn
default">Discuss</a>
</div>
</template>
client/templates/posts/post_item.html
El botn upvote
Despus, llamaremos a un mtodo en el servidor cuando el usuario haga clic
en el botn:
//...
Template.postItem.events({
'click.upvote':function(e){
e.preventDefault();
Meteor.call('upvote',this._id);
}
});
client/templates/posts/post_item.js
Finalmente, volvemos a lib/collections/posts.js para aadir el mtodo de
Meteor.methods({
post:function(postAttributes){
//...
},
upvote:function(postId){
check(this.userId,String);
check(postId,String);
varpost=Posts.findOne(postId);
if(!post)
thrownewMeteor.Error('invalid','Postnotfound');
if(_.include(post.upvoters,this.userId))
thrownewMeteor.Error('invalid','Alreadyupvotedthispost');
Posts.update(post._id,{
$addToSet:{upvoters:this.userId},
$inc:{votes:1}
});
}
});
//...
lib/collections/posts.js
Commit 13-1
Algoritmo bsico de voto.
Ver en GitHubLanzar instancia
client/templates/posts/post_item.html
Template.postItem.helpers({
ownPost:function(){
//...
},
domain:function(){
//...
},
upvotedClass:function(){
varuserId=Meteor.userId();
if(userId&&!_.include(this.upvoters,userId)){
return'btnprimaryupvotable';
}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:
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:
Template.registerHelper('pluralize',function(n,thing){
//fairlystupidpluralizer
if(n===1){
return'1'+thing;
}else{
returnn+''+thing+'s';
}
});
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:
<templatename="postItem">
//...
<p>
{{pluralizevotes"Vote"}},
submittedby{{author}},
<ahref="{{pathFor'postPage'}}">{{pluralizecommentsCount"comment"}}</a>
{{#ifownPost}}<ahref="{{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 GitHubLanzar instancia
Meteor.methods({
post:function(postAttributes){
//...
},
upvote:function(postId){
check(this.userId,String);
check(postId,String);
varaffected=Posts.update({
_id:postId,
upvoters:{$ne:this.userId}
},{
$addToSet:{upvoters:this.userId},
$inc:{votes:1}
});
if(!affected)
thrownewMeteor.Error('invalid',"Youweren'tabletoupvotethat
post");
}
});
//...
collections/posts.js
Commit 13-4
Mejorado el algoritmo de voto.
Ver en GitHubLanzar 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:
>Posts.update(postId,{$set:{votes:10000}});
Este descarado intento de jugar con el sistema sera capturado por nuestro
callback deny() (encollections/posts.js, recuerdas?) e inmediatamente
denegada.
Pero si miras detenidamente, podrs ver la compensacin de latencia en
accin. Puede ser rpido, pero el post saltar brevemente al principio de la
lista antes de volver de nuevo a su posicin.
Qu est pasando? En su coleccin de Posts locales, la actualizacin se ha
aplicado sin incidentes. Esto sucede al instante, por lo que el mensaje se va
al principio de la lista. Mientras tanto, en el servidor, se deniega la
actualizacin, de forma que un poco ms tarde (milisegundos si ests
ejecutando Meteor en tu propia mquina), el servidor devuelve un error,
obligando a la coleccin local a revertirse.
El resultado final: mientras esperamos a que el servidor responda, la interfaz
de usuario no puede dejar de confiar en la coleccin local. Tan pronto como
el servidor deniega la modificacin, las interfaces de usuario se adaptan para
reflejarlo.
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 yBestPostsListController. Esto nos permitir
reutilizar las mismas opciones de ruta, tanto para las rutas home ynewPosts,
quedndonos un solo NewPostsListController del que heredar. Y, adems, todo
esto es una buena ilustracin de lo flexible que puede ser Iron Router.
Vamos a reemplazar la propiedad de ordenacin {submitted:
1} en PostsListController por this.sort, que ser proporcionado
por NewPostsListController y BestPostsListController:
//...
PostsListController=RouteController.extend({
template:'postsList',
increment:5,
postsLimit:function(){
returnparseInt(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(){
returnPosts.find({},this.findOptions());
},
data:function(){
varhasMore=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(){
returnRouter.routes.newPosts.path({postsLimit:this.postsLimit()+
this.increment})
}
});
BestPostsController=PostsListController.extend({
sort:{votes:1,submitted:1,_id:1},
nextPath:function(){
returnRouter.routes.bestPosts.path({postsLimit:this.postsLimit()+
this.increment})
}
});
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 y la ponemos
en NewPostsController y BestPostsController, ya que el path ser diferente en
uno y otro caso.
Adems, cuando ordenamos por votos, establecemos un segundo orden por
fecha y por _id para garantizar que el orden est completamente
especificado.
Una vez listos los nuevos controladores podemos deshacernos de la ruta
anterior a postsList. Simplemente eliminamos el siguiente cdigo:
Router.route('/:postsLimit?',{
name:'postsList'
})
lib/router.js
<spanclass="iconbar"></span>
</button>
<aclass="navbarbrand"href="{{pathFor'home'}}">Microscope</a>
</div>
<divclass="collapsenavbarcollapse"id="navigation">
<ulclass="navnavbarnav">
<li>
<ahref="{{pathFor'newPosts'}}">New</a>
</li>
<li>
<ahref="{{pathFor'bestPosts'}}">Best</a>
</li>
{{#ifcurrentUser}}
<li>
<ahref="{{pathFor'postSubmit'}}">SubmitPost</a>
</li>
<liclass="dropdown">
{{>notifications}}
</li>
{{/if}}
</ul>
<ulclass="navnavbarnavnavbarright">
{{>loginButtons}}
</ul>
</div>
</nav>
</template>
client/templates/includes/header.html
if(confirm("Deletethispost?")){
varcurrentPostId=this._id;
Posts.remove(currentPostId);
Router.go('home');
}
}
client/templates/posts/posts_edit.js
Posts ordenados
Commit 13-5
Aadidas rutas para las listas de posts y pginas para mo
Ver en GitHubLanzar 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
nuestroactiveRouteClass debe ser lo suficientemente inteligente como para
activar la etiqueta <li> en ambos casos.
<templatename="header">
<navclass="navbarnavbardefault"role="navigation">
<divclass="navbarheader">
<buttontype="button"class="navbartogglecollapsed"data
toggle="collapse"datatarget="#navigation">
<spanclass="sronly">Togglenavigation</span>
<spanclass="iconbar"></span>
<spanclass="iconbar"></span>
<spanclass="iconbar"></span>
</button>
<aclass="navbarbrand"href="{{pathFor'home'}}">Microscope</a>
</div>
<divclass="collapsenavbarcollapse"id="navigation">
<ulclass="navnavbarnav">
<liclass="{{activeRouteClass'home''newPosts'}}">
<ahref="{{pathFor'newPosts'}}">New</a>
</li>
<liclass="{{activeRouteClass'bestPosts'}}">
<ahref="{{pathFor'bestPosts'}}">Best</a>
</li>
{{#ifcurrentUser}}
<liclass="{{activeRouteClass'postSubmit'}}">
<ahref="{{pathFor'postSubmit'}}">SubmitPost</a>
</li>
<liclass="dropdown">
{{>notifications}}
</li>
{{/if}}
</ul>
<ulclass="navnavbarnavnavbarright">
{{>loginButtons}}
</ul>
</div>
</nav>
</template>
client/templates/includes/header.html
Template.header.helpers({
activeRouteClass:function(/*routenames*/){
varargs=Array.prototype.slice.call(arguments,0);
args.pop();
varactive=_.any(args,function(name){
returnRouter.current()&&Router.current().route.getName()===name
});
returnactive&&'active';
}
});
client/templates/includes/header.js
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
SIDEBAR13.5
Meteor.publish('postDetail',function(postId){
returnPosts.find(postId);
});
});
Meteor.publish('bestPosts',function(limit){
returnPosts.find({},{sort:{votes:1,submitted:1},limit:limit});
});
server/publications.js
//sendoverthetoptwocommentsattachedtoasinglepost
functionpublishPostComments(postId){
varcommentsCursor=Comments.find({postId:postId},{limit:2});
commentHandles[postId]=
Mongo.Collection._publishCursor(commentsCursor,sub,'comments');
}
postHandle=Posts.find({},{limit:limit}).observeChanges({
added:function(id,post){
publishPostComments(id);
sub.added('posts',id,post);
},
changed:function(id,fields){
sub.changed('posts',id,fields);
},
removed:function(id){
//stopobservingchangesonthepost'scomments
commentHandles[id]&&commentHandles[id].stop();
//deletethepost
sub.removed('posts',id);
}
});
sub.ready();
//makesurewecleaneverythingup(note`_publishCursor`
//doesthisforuswiththecommentobservers)
sub.onStop(function(){postHandle.stop();});
});
varvideosCursor=Resources.find({type:'video'});
Mongo.Collection._publishCursor(videosCursor,sub,'videos');
//_publishCursordoesn'tcallthisforusincasewedothismorethan
once.
sub.ready();
});
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
insertElement:
moveElement:
removeElement:
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
eliminar B e insertar una nueva copia (B) antes del elemento A.
Esto hace de la animacin algo complicado ya que, no podemos
simplemente mover B a una nueva posicin, porque B habr desaparecido
tan pronto como Meteor redibuje de nuevo la pgina (que, como sabemos,
sucede instantneamente gracias a la reactividad). Pero no te preocupes,
encontraremos la manera de hacerlo.
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.
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 aldocumento 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 enclient/stylesheets/style.css, pero si
necesitas aadirlo, este es el cdigo para la hoja de estilo:
.post{
position:relative;
}
.post.animate{
transition:all300ms0mseasein;
}
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 (offset) es la posicin anterior restada a la nueva.
{{#ifnextPath}}
<aclass="loadmore"href="{{nextPath}}">Loadmore</a>
{{else}}
{{#unlessready}}
{{>spinner}}
{{/unless}}
{{/if}}
</div>
</template>
client/templates/posts/posts_list.html
}
});
client/templates/posts/posts_list.js
node
next
offset()
Template.postsList.onRendered(function(){
this.find('.wrapper')._uihooks={
moveElement:function(node,next){
var$node=$(node),$next=$(next);
varoldTop=$node.offset().top;
varheight=$node.outerHeight(true);
//findalltheelementsbetweennextandnode
var$inBetween=$next.nextUntil(node);
if($inBetween.length===0)
$inBetween=$node.nextUntil(next);
//nowputnodeinplace
$node.insertBefore(next);
//measurenewtop
varnewTop=$node.offset().top;
//movenode*back*towhereitwasbefore
$node
.removeClass('animate')
.css('top',oldTopnewTop);
//pusheveryotherelementdown(orup)toputthemback
$inBetween
.removeClass('animate')
.css('top',oldTop<newTop?height:1*height)
//forcearedraw
$node.offset();
//reseteverythingto0,animated
$node.addClass('animate').css('top',0);
$inBetween.addClass('animate').css('top',0);
}
}
});
client/templates/posts/posts_list.js
Algunas notas:
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',oldTopnewTop) ycss('top',0) a la vez, las nuevas
coordenadas reemplazarn las viejas y no pasar nada. Si queremos ver la
animacin, debemos forzar al navegador a redibujar el elemento despus de
moverlo la primera vez.
Una forma sencilla de hacerlo es pedirle al navegador el offset del elemento.
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
Commit 14-1
Added post reordering animation.
Ver en GitHubLanzar instancia
Aparecer y desaparecer
Ahora que ya tenemos resuelta la reordenacin ms complicada, animar las
inserciones y eliminaciones va ser muy sencillo.
Primero, haremos aparecer nuevos posts (esta vez, por simplicidad,
usaremos animaciones JavaScript):
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
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')).
#main{
position:relative;
}
.page{
position:absolute;
top:0px;
width:100%;
}
//...
client/stylesheets/style.css
$(this).remove();
});
}
}
});
client/templates/application/layout.js
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 SusiripalaMeteorHacks. 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 differential 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
Sesin
La sesin en Meteor es una fuente de datos reactiva que usa tu aplicacin
para hacer un seguimiento del estado del usuario.
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-JavaScriptobjects), a menudo son documentos de una coleccin, aunque pueden ser
ms complejos e incluir funciones.
Changelog
99
1.9
Added note about default Iron Router help page in Routing chapter.
1.8
December 5, 2014
1.7.2
1.7.1
Various fixes.
1.7
1.6.1
Updated introduction.
1.6
collections
views
Getting Started
Templates
Collections
Routing
The Session
Adding Users
Reactivity
Creating Posts
Latency Compensation
Errors
Comments
Various edits.
Notifications
Advanced Reactivity
Pagination
Voting
Various edits.
Advanced Publications
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
Minor tweaks.
Minor edits.
Implementing Intercom
Migrations
Minor edits.
October 3, 2014
1.5.1
1.5
Fixed typos.
1.3.4
1.3.3
May 5, 2014
1.3.2
April 8, 2014
1.3.1
1.3
becomes {{>yield}}
{{loginButtons}}
becomes {{>loginButtons}}
7 Creating Posts:
o
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
Updates
December 1, 2013
1.1
Major Updates
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
8 Editing Posts
9 Errors
11 Notifications
12 Animations
If youd like to confirm what exactly has changed, weve created a full diff
of our Markdown source files [PDF].
October 4, 2013
1.02
September 4, 2013
1.01
May 5, 2013
First version.
1.0