Discover Meteor

Building Real-Time JavaScript Web Apps

Introdução

1

Faça um pequeno experimento mental para mim. Imagine que você está abrindo uma pasta em duas diferentes janelas do seu computador.

Agora clique dentro de uma das janelas e delete um arquivo. Esse arquivo desapareceu da outra janela também?

Você não precisa realmente seguir esses passos para saber disso. Quando nós modificamos algo num sistema de arquivos local, a mudança é aplicada em todo lugar sem necessidade de refreshes ou callbacks. Apenas acontece.

Entretanto, vamos pensar sobre como o mesmo cenário funcionaria na web. Por exemplo, vamos dizer que você abriu o mesmo site do Wordpress como admin em duas janelas do browser e então criou um nova postagem numa delas. Ao contrário do desktop, não importa o quanto você espere, a outra janela não vai refletir a mudança a não ser que você a atualize.

Ao longo dos anos, nós nos acostumamos com a idéia de um website ser algo com o qual você se comunica apenas com pequenos, separados acessos.

Mas Meteor é parte de uma nova onda de frameworks e tecnologias que estão procurando desafiar o status quo ao fazer a web em tempo real e reativa.

O que é meteor?

Meteor é uma plataforma construída em cima de Node.js para a construção de web apps em tempo real. É o que fica entre o banco de dados do seu app e a interface do usuário e garante que ambos mantenham sincronia.

Já que é construído sobre Node.js, Meteor usa JavaScript tanto no cliente quanto no servidor. O melhor é que Meteor também consegue compartilhar código entre ambos ambientes.

O resultado disso tudo é uma plataforma que consegue ser muito poderosa e muito simples abstraindo muitos dos aborrecimentos e armadilhas comuns ao desenvolvimento de aplicativos web.

Por que Meteor?

Então por que você deveria investir seu tempo aprendendo Meteor ao invés de um outro web framework? Deixando de lado todas as características do Meteor, nós acreditamos que tudo se resume a uma única coisa: Meteor é fácil de aprender.

Mais do que qualquer outro framework, Meteor torna possível criar um web app em tempo real e funcionando na web em questão de horas. E se você já fez desenvolvimento front-end antes, você já está familiarizado com JavaScript e não será necessário nem aprender uma nova linguagem.

Meteor pode ser o framework ideal para suas necessidades, ou então novamente pode não ser. Mas já que você pode se familiarizar ao longo de algumas noites ou um fim de semana, por que não tentar e descobrir por si mesmo?

Por que este livro?

Ao longos dos últimos 6 meses, nós estivemos trabalhando em Telescope, um aplicativo Meteor open-source que permite qualquer um criar sua própria rede social de notícias (pense em Reddit ou Hacker news), onde pessoas podem enviar links e votar neles.

Nós aprendemos um bocado construindo esse aplicativo, mas não era sempre fácil encontrar as respostas para as questões. Nós tivemos de encaixar as peças de muitas fontes diferentes, e em muitos casos até inventar nossas próprias soluções. Então com este livro, nós gostaríamos de compartilhar todas essas lições, e criar um simples guia passo-a-passo que o guiará através da construção de um aplicativo em Meteor completo do zero.

O aplicativo que nós estamos construindo é uma versão ligeiramente simplificada do Telescope, o qual chamamos de Microscope. Enquanto o construimos, iremos ver todos os diferentes elementos necessários na construção de um aplicativo em Meteor, tais como contas de usuários, coleções Meteor, roteamento, e mais.

E após você ter terminado o livro, se você quiser ir adiante você poderá facilmente entender o código do Telescope, já que segue os mesmos padrões.

Sobre os Autores

Caso você esteja se perguntando quem somos nós e porque você deveria confiar em nós, aqui segue um pouco sobre a experiência de nós dois.

Tom Coleman é uma parte do Percolate Studio, uma loja de web development com foco em qualidade e experiência do usuário. Ele também é co-autor do Meteorite e da Atmosphere repositório de pacotes, e está também por trás de muitos outros projetos open-source em Meteor (tais como o Router).

Sacha Greif tem trabalhado com startups tais como Hipmunk e RubyMotion na área de design de produto e web. Ele também é criador do Telescope e Sidebar (o qual é baseado em Telescope), e também é fundador do Folyo.

Capítulos e Barras Laterais

Nós queremos que este livro seja útil tanto para novatos em Meteor quanto para programadores avançados, então nós dividimos os capítulos em duas categorias: capítulos regulares (numeradores de 1 a 14) e barras laterais (.5 números).

Capítulos regulares lhe guiarão através da construção do aplicativo, e tentarão te deixar o mais operacional o mais cedo possível explicando os mais importantes passos sem lhe aborrecer com detalhes demais.

Por outro lado, barras laterais irão mais aprofundadamente nas entranhas do Meteor, e o ajudarão a ter uma compreensão melhor do que realmente está acontecendo por trás das cortinas.

Então se você é um iniciante, sinta-se livre para ignorar as barras laterais na sua primeira leitura, e volte a elas mais tarde quando você já tiver experimentado Meteor.

Commits e Live Instances

Não há nada pior do que seguir um livro de programação e de repente perceber que o seu código está fora de sincronia com os exemplos e que nada mais funciona como deveria.

Para evitar isto, nós estabelecemos um repositório GitHub para o Microscope, e nós também providenciaremos links para os git commits a cada algumas mudanças de código. Adicionalmente, cada commit também linka para uma live instance do aplicativo nesse commit em particular, então você pode compará-la a sua cópia local. Aqui está um exemplo de como isso se parecerá:

Commit 11-2

Display notifications in the header.

Mas perceba que apesar de provermos esses commits isso não significa que você deva apenas ir de um git checkout ao próximo. Você aprenderá muito melhor se você aproveitar o tempo para digitar manualmente o código do seu aplicativo!

Alguns Outros Recursos

Se você quiser aprender mais sobre um aspecto em particular do Meteor, a documentação oficial do Meteor é o melhor lugar para começar.

Nós também recomendamos Stack Overflow para exposição de problemas e perguntas, e o #meteor IRC channel se você precisar de auxílio em tempo real.

Eu preciso de Git?

Apesar de estar familiarizado com o Git version control não é estritamente necessário para seguir ao longo deste livro, nós recomendamos muito.

Se você quiser um começo veloz, nós recomendamos Nick Farina’s Git Is Simpler Than You Think.

Se você é um novato em Git, nós também recomendamos o aplicativo GitHub for Mac, o qual permite a você clonar e administrar repositórios sem precisar usar a linha de comando.

Entrando em Contato

Começando com Meteor

2

Primeiras impressões são importantes, e o processo de instalar o Meteor deve ser relativamente indolor. Na maioria dos casos, você estará em desenvolvimento em menos de 5 minutos.

Para começar, nós podemos instalar o Meteor abrindo a janela do terminal e teclando:

$ curl https://install.meteor.com | sh

Isto instalará o executável meteor no seu sistema pronto para você usar Meteor.

Não Instalando Meteor

Se você não pode (ou não quer) instalar Meteor localmente, nós recomendamos checar Nitrous.io

Nitrous.io é um serviço que permite a você rodar aplicativos e editar o código dos mesmos direto no seu navegador, e nós escrevemos um pequeno guia para ajudá-lo a começar.

Você pode simplesmente seguir o guia até (e incluindo) o segmento “Instalando Meteor & Meteorite”, e então seguir junto com o livro novamente começando do segmento “Criando um Simples Aplicativo” deste capítulo.

Meteorite

Devido ao fato que Meteor não suporta pacotes de terceiros diretamente, Tom Coleman (um dos autores do livro) e alguns membros da comunidade criaram Meteorite, um invólucro para Meteor. Meteorite também toma conta da instalação de Meteor para você e juntando a ele qualquer pacote que você encontrar.

Já que nós necessitaremos de pacotes de terceiros para algumas das funcionalidades do Microscope, vamos instalar Meteorite.

Instalando Meteorite

Você precisará se assegurar que node e git estejam instalados na sua máquina. Instale-os da forma padrão para o seu Sistema Operacional, ou tente os links a seguir:

Segundo, vamos instalar Meteorite. Como é um executável npm (Node Packaged Module, formato padrão de módulo do Node), nós o instalaremos com:

$ npm install -g meteorite

Erros de Permissão?

Em algunas máquinas você pode precisar de root permission para instalar Meteorite. Para evitar problemas, faça questão de usar sudo -H:

$ sudo -H npm install -g meteorite

Você pode ler mais sobre esta questão na documentação do Meteorite.

É isso aí! Meteorite lidará com as coisas agora.

Nota: ainda não há suporte Windows para Meteorite, mas você pode dar uma olhada no nosso tutorial windows.

### mrt vs meteor

Meteorite instala o executável mrt, o qual nós usaremos para instalar pacotes no nosso aplicativo. Quando nós quisermos ativar nosso servidor, entretanto, nós usaremos o executável meteor.

Criando um Simples Aplicativo

Agora que nós temos o Meteorite instalado, vamos criar um aplicativo. Para fazer isto, nós usamos a ferramenta da linha de comando do Meteorite mrt:

$ mrt create microscope

Este comando carregará o Meteor, e estabelecerá um projeto Meteor básico e pronto para você usar. Após isto, você poderá ver um diretório, microscope/, contendo o seguinte:

microscope.css
microscope.html
microscope.js
smart.json

O aplicativo que o Meteor criou para você é um simples aplicativo clichê demonstrando alguns padrões simples.

Apesar do nosso aplicativo não fazer muito, nós já podemos rodá-lo. Para rodá-lo, vá até o terminal e digite:

$ cd microscope
$ meteor

Agora navegue no seu navegador para http://localhost:3000/ (ou o equivalente http://0.0.0.0:3000/) e você deverá ver algo assim:

Meteor's Hello World.
Meteor’s Hello World.

Commit 2-1

Created basic microscope project.

Parabéns! Você conseguiu rodar seu primeiro aplicativo em Meteor. Aliás, para desativar seu aplicativo tudo o que você precisa fazer é abrir a janela do terminal onde o aplicativo está rodando, e pressionar ctrl+c.

Adicionando um Pacote

Nós agora usaremos Meteorite para adicionar um pacote inteligente que nos permitirá incluir Bootstrap no nosso projeto:

$ mrt add bootstrap

Commit 2-2

Added bootstrap package.

Uma nota sobre Pacotes

Quando se fala de pacotes no contexto de Meteor, deve-se ser específico. O Meteor usa cinco tipos básicos de pacotes.

  • O núcleo do Meteor em si é dividido em diferentes pacotes nucleares. Eles estão inclusos em cada aplicativo em Meteor, e você em geral quase nunca precisará se preocupar com eles.
  • Os pacotes inteligentes do Meteor são um grupo de uns 37 pacotes (você pode ver a lista completa com meteor list) que vêm embrulhados com Meteor e que você pode opcionalmente importar para o seu próprio aplicativo. Você pode adicioná-los até quando você não está usando Meteorite, com meteor add packagename.
  • Pacotes locais são pacotes customizados que você pode criar e por no diretório /packages. Você também não precisa usar Meteorite para usá-los.
  • Pacotes inteligentes da Atmosphere são pacotes Meteor de terceiros listados em Atmosphere. Meteorite é necessário para importar e usá-los. Atmosphere.
  • Pacotes NPM (Node Packaged Modules) são pacotes do Node.js. Apesar deles não funcionarem diretamente com Meteor, eles podem ser usados pelos tipos de pacote prévios.

A Estrutura de Arquivos de um Aplicativo em Meteor

Antes de nós começarmos a programar, nós precisamos estabelecer nosso projeto apropriadamente. Para assegurar que nós tenhamos uma construção limpa, abra o diretório microscope e delete microscope.html, microscope.js, e microscope.css.

A seguir, crie cinco diretórios raíz dentro de /microscope: /client, /server, /public, /lib, e /collections, e nós também criaremos arquivos main.html e main.js vazios dentro de /client. Não se preocupe se isso quebrará o aplicativo por enquanto, nós começaremos a preencher esses arquivos no próximo capítulo.

Nós devemos mencionar que alguns desses diretórios são especiais. Em relação a arquivos, o Meteor tem algumas regras:

  • Código no diretório /server roda apenas no servidor.
  • Código no diretório /client roda apenas no cliente.
  • Todo resto roda tanto no cliente quanto no servidor.
  • Arquivos no /lib são lidos antes dos outros.
  • Qualquer arquivo main.* é lido após todo resto.
  • Seus recursos estáticos (fonts, images, etc.) vão no diretório /public.

Note que apesar do Meteor ter regras, ele realmente não força você a usar nenhuma estrutura de arquivos predefinida para o seu aplicativo se você não quiser. Então a estrutura que nós sugerimos é apenas a nossa forma de fazer as coisas, não uma regra absoluta.

Nós encorajamos que você cheque a documentação oficial do Meteor se você quiser mais detalhes sobre isso.

O Meteor é MVC?

Se você está vindo para o Meteor de outros frameworks tais como Ruby on Rails, talvez você esteja se perguntando se aplicativos em Meteor adotam o padrão MVC (Model View Controller).

A resposta curta é não. Ao invés de Rails, Meteor não impõe nenhuma estrutura predefinida para o seu aplicativo. Então neste livro nós simplesmente deixaremos o código de uma forma que faça mais sentido para nós, sem se preocupar demais com acrônimos.

Não público?

OK, nós mentimos. Nós não realmente precisamos do diretório public/ pela simples razão de que o Microscope não usa recursos estáticos! Mas já que a maioria dos aplicativos em Meteor irão incluir pelo menos um conjunto de imagens, nós pensamos que seria importante cobrir esse assunto.

Aliás, você pode também ter notado um diretório .meteor escondido. Aqui é onde o Meteor guarda seu próprio código, e modificar coisas aqui é geralmente um idéia muito ruim. Aliás, você nunca realmente precisará olhar este diretório. As únicas excessões a isto são os arquivos .meteor/packages e .meteor/release, os quais costumam respectivamente listar seus pacotes inteligentes e a versão do Meteor a se usar. Quando você adicionar pacotes e mudar versões do Meteor, pode ser útil checar as mudanças a esses arquivos.

Underscores vs CamelCase

A única coisa que nós iremos dizer sobre o antigo debate underscore (my_variable) vs camelCase (myVariable) é que realmente não importa qual você escolha desde que você permaneça nele.

Neste livro, nós usaremos camelCase porque é a forma JavaScript comum de ser fazer as coisas (até mesmo por que é JavaScript e não java_script!).

As únicas exceções a esta regra são os nomes de arquivos, os quais usaremos underscores (my_file.js), e classes em CSS, as quais usam hífens (.my-class). A razão para tanto é que no sistema de arquivos, underscores são o mais comum, enquanto a própria sintaxe CSS já usa hífens (font-family, text-align, etc).

Cuidando das CSS

Este livro não é sobre CSS. Então para evitar retardá-los com detalhes de estilização, nós decidimos disponibilizar a stylesheet desde o começo, para que você jamais precise se preocupar com isto de novo.

As CSS são lidas e minificadas automaticamente pelo Meteor, então diferentemente de outros recursos estáticos elas vão no /client, não no /public. Vá em frente e crie um diretório client/stylesheets/ agora, e ponha este arquivo style.css dentro dele:

.grid-block, .main, .post, .comments li, .comment-form {
    background: #fff;
    border-radius: 3px;
    padding: 10px;
    margin-bottom: 10px;
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
}
body {
    background: #eee;
    color: #666666;
}
.navbar { margin-bottom: 10px }
.navbar .navbar-inner {
    border-radius: 0px 0px 3px 3px;
}
#spinner { height: 300px }
.post {
    *zoom: 1;
    -webkit-transition: all 300ms 0ms;
    -webkit-transition-delay: ease-in;
    -moz-transition: all 300ms 0ms ease-in;
    -o-transition: all 300ms 0ms ease-in;
    transition: all 300ms 0ms ease-in;
    position: relative;
    opacity: 1;
}
.post:before, .post:after {
    content: "";
    display: table;
}
.post:after { clear: both }
.post.invisible { opacity: 0 }
.post .upvote {
    display: block;
    margin: 7px 12px 0 0;
    float: left;
}
.post .post-content { float: left }
.post .post-content h3 {
    margin: 0;
    line-height: 1.4;
    font-size: 18px;
}
.post .post-content h3 a {
    display: inline-block;
    margin-right: 5px;
}
.post .post-content h3 span {
    font-weight: normal;
    font-size: 14px;
    display: inline-block;
    color: #aaaaaa;
}
.post .post-content p { margin: 0 }
.post .discuss {
    display: block;
    float: right;
    margin-top: 7px;
}
.comments {
    list-style-type: none;
    margin: 0;
}
.comments li h4 {
    font-size: 16px;
    margin: 0;
}
.comments li h4 .date {
    font-size: 12px;
    font-weight: normal;
}
.comments li h4 a { font-size: 12px }
.comments li p:last-child { margin-bottom: 0 }
.dropdown-menu span {
    display: block;
    padding: 3px 20px;
    clear: both;
    line-height: 20px;
    color: #bbb;
    white-space: nowrap;
}
.load-more {
    display: block;
    border-radius: 3px;
    background: rgba(0, 0, 0, 0.05);
    text-align: center;
    height: 60px;
    line-height: 60px;
    margin-bottom: 10px;
}
.load-more:hover {
    text-decoration: none;
    background: rgba(0, 0, 0, 0.1);
}
client/stylesheets/style.css

Commit 2-3

Re-arranged file structure.

Uma nota sobre CoffeeScript

Neste livro nós iremos escrever em puro JavaScript. Mas se você preferir CoffeeScript, Meteor te dá cobertura. Apenas adicione o pacote CoffeeScript e está pronto para seguir:

mrt add coffeescript

Deployment

Sidebar 2.5

Algumas pessoas gostam de trabalhar quietas num projeto até ficar perfeito, enquanto outros não conseguem esperar para mostrar ao mundo o mais cedo possível.

Se você é do primeiro tipo de pessoa e prefere desenvolver localmente por hora, sinta-se livre de pular este capítulo. Por outro lado, se você prefere investir seu tempo aprendendo como implementar seu aplicativo em Meteor na web, nós te damos cobertura.

Nós aprenderemos como implementar um aplicativo em Meteor de diferentes formas. Sinta-se livre para usar cada uma delas em qualquer estágio do seu processo de desenvolvimento, seja trabalhando no Microscope ou qualquer outro aplicativo em Meteor. Vamos começar!

Introduzindo Barras Laterais

Este é um capítulo barra lateral. Barras laterais vão mais afundo em tópicos gerais sobre Meteor independente do resto do livro.

Então se você prefere continuar construindo Microscope, você pode seguramente pular isto por hora e voltar mais adiante.

Implementando com Meteor

Implementar com um subdomínio Meteor (vulgo http://meuaplicativo.meteor.com) é a opção mais fácil, e a primeira que nós vamos tentar. Isto pode ser útil para mostrar seu aplicativo para outros nos primeiros dias, ou para rapidamente montar um servidor de teste.

Implementando em Meteor é bem simples. Apenas abra seu terminal, vá ao diretório do seu aplicativo em Meteor, e digite:

$ meteor deploy myapp.meteor.com

Claro, você que terá que substituir “meuaplicativo” pelo nome do seu aplicativo de escolha, preferidamente um que ainda não esteja em uso. Se o nome escolhido já estiver em uso, Meteor pedirá a você uma senha. Se isso ocorrer, simplesmente cancele a operação com ctrl+c e tente novamente com um nome diferente.

Se tudo der certo, após alguns segundos você poderá acessar seu aplicativo em http://meuaplicativo.meteor.com.

Proteção com Senha

Por padrão, não há restrição para subdomínios Meteor. Qualquer um pode usar o nome de domínio de sua escolha, e rescrever qualquer aplicativo existente. Então você provavelmente irá querer por uma senha para proteger o seu nome de domínio com a opção -p, como abaixo:

$ meteor deploy myapp.meteor.com -p

Meteor irá então pedir a você para definir uma senha, e daí em diante esta senha será sempre necessária toda vez que você quiser implementar este aplicativo em particular.

Você pode checar a documentação oficial para mais informação sobre essas coisas como acessar a instância hospedada do banco de dados diretamente, ou como configurar um domínio customizado para o seu aplicativo.

Implementando em Modulus

Modulus é uma ótima opção para implementar aplicativos em NodeJS. É um dos poucos provedores PaaS (platform-as-a-service) que oficialmente suportam Meteor, e já há várias pessoas rodando aplicativos Meteor em produção nele.

Demeteorizer

Modulus liberou uma ferramenta em open-source chamada demeteorizer, a qual converte aplicativos em Meteor em aplicativos em NodeJS padrão.

Comece por criar uma conta. Para implementar nosso aplicativo no Modulus, nós então precisaremos instalar a ferramenta de linha de comando Modulus:

$ npm install -g modulus

E então autenticá-la com:

$ modulus login

Nós agora criaremos um projeto Modulus (note que você também pode fazer isso através painel de instrumentos online do Modulus):

$ modulus project create

O próximo passo será criar um banco de dados MongoDB para o nosso aplicativo. Nós podemos criar um banco de dados MongoDB com o próprio Modulus, MongoHQ ou com qualquer outro provedor MongoDB em nuvem.

Uma vez criado o banco de dados MongoDB, nós podemos pegar a MONGO_URL para o nosso banco de dados com UI online do Modulus (vá ao Dashboard > Databases > Select your database > Administration), então use-o para configurar seu aplicativo assim:

$ modulus env set MONGO_URL "mongodb://<user>:<pass>@mongo.onmodulus.net:27017/<database_name>"

Agora é hora de implementar seu aplicativo. É tão simples quanto digitar:

$ modulus deploy

Nós implementamos nosso aplicativo no Modulus com sucesso. Cheque a documentação do Modulus para mais informação sobre como acessar logs, configurando domínio customizado, e SSL.

Meteor Up

Apesar que novas soluções na nuvem têm aparecido a cada dia, elas costumam vir com suas próprias parcelas de problemas e limitações. Então até hoje, implementar no seu próprio servidor permanece a melhor forma de por seu aplicativo Meteor em produção. A única questão é, implementar você mesmo não é tão simples, especialmente se você está procurando por implementação com qualidade de produção.

Meteor Up (ou a abreviação mup) é outra tentativa de resolver essa questão, com um utilitário da linha de comando que toma conta da configuração e implementação para você. Então vamos ver como implementar Microscope com Meteor Up.

Antes de qualquer coisa, nós precisaremos de um servidor para enviar o conteúdo. Nós recomendamos o Digital Ocean, o qual começa com $5 por mês, ou AWS, o qual provê Micro instâncias de graça (você rapidamente vai se deparar com problemas de escalar, mas se você está procurando apenas experimentar Meteor Up será o suficiente).

Independente do serviço que você escolher, você deve ter três coisas: o endereço de IP do seu servidor, um login (normalmente root ou ubuntu), e uma senha. Deixe-as em algum lugar seguro, nós precisaremos delas logo!

Iniciando Meteor Up

Para começar, nós precisaremos instalar Meteor Up via npm como a seguir:

$ npm install -g mup

Nós então criaremos um diretório especial, separado que terá as configurações do nosso Meteor Up para uma implementação em particular. Nós estamos usando um diretório separado por duas razões: primeiro, é melhor evitar incluir credenciais privadas no seu repósitorio Git, especialmente se você está trabalhando num banco de códigos público.

Segundo, usando múltiplos diretórios separados, nós seremos capazes de administrar múltiplas configurações Meteor Up em paralelo. Isso será útil para implementação para instâncias de produção e teste, por exemplo.

Então vamos criar este novo diretório e usá-lo para iniciar um novo projeto Meteor Up:

$ mkdir ~/microscope-deploy
$ cd ~/microscope-deploy
$ mup init

Compartilhando com Dropbox

Uma grande forma de assegurar que você e seu time todos estão usando as mesmas configurações de implementação é criar uma pasta de configuração do Meteor Up no seu Dropbox, ou em qualquer serviço similar.

Configuração do Meteor Up

Quando começar um novo projeto, Meteor Up criará dois arquivos para você: mup.json e settings.json.

mup.json terá todas as suas configurações relacionadas à implementação, enquanto settings.json terá as configurações relacionadas ao aplicativo (OAuth tokens, analytics tokens, etc.).

O próximo passo é configurar seu arquivo mup.json. Aqui está um arquivo mup.json padrão gerado por mup init, e tudo que você precisa fazer é preencher as lacunas:

{
  //server authentication info
  "servers": [{
    "host": "hostname",
    "username": "root",
    "password": "password"
    //or pem file (ssh based authentication)
    //"pem": "~/.ssh/id_rsa"
  }],

  //install MongoDB in the server
  "setupMongo": true,

  //location of app (local directory)
  "app": "/path/to/the/app",

  //configure environmental
  "env": {
    "ROOT_URL": "http://supersite.com"
  }
}
mup.json

Vamos checar cada uma dessas configurações.

Autentificação do Servidor

Você perceberá que o Meteor Up suporta autenticação baseada em senha e chave privada (PEM), então ele pode ser usado com quase qualquer provedor em nuvem.

Nota Importante: se você escolher uma autenticação baseada em senha, tenha certeza que você instalou o sshpass antes (use este guia).

Configuração MongoDB

O próximo passo é configurar o banco de dados MongoDB para o seu aplicativo. Nós recomendamos usar MongoHQ ou qualquer outro provedor MongoDB em nuvem, já que eles oferecem suporte profissional e ferramentas administrativas melhores.

Se você decidir usar MongoHQ, defina setupMongo como false e adicione a variável ambiental MONGO_URL no bloco env de mup.json. Se você decidir hospedar MongoDB com Meteor Up, apenas defina setupMongo como true e Meteor Up cuidará do resto.

Caminho Meteor Up

Já que a nossa configuração do Meteor Up vive em outro diretório, nós precisaremos apontar o Meteor Up de volta para o nosso aplicativo usando a propriedade app. Apenas insira seu caminho local completo, o qual você pode conseguir usando o comando pwd pelo terminal quando localizado dentro do diretório do seu aplicativo.

Variáveis Ambientais

Você pode especificar todas as variáveis ambientais do seu aplicativo (tais como ROOT_URL, MAIL_URL, MONGO_URL, etc.) dentro do bloco env.

Configurando e Implementando

Antes de nós implementarmos, nós precisaremos configurar o servidor para que esteja pronto para hospedar seus aplicativos em Meteor. A mágica do Meteor Up captura este processo complexo em um único comando!

$ mup setup

Isto levará alguns minutos dependendo da performance do servidor e da conectividade da rede. Após o sucesso da instalação, nós podemos finalmente implementar nosso aplicativo com:

$ mup deploy

Isto embrulhará o aplicativo em Meteor, e o implementará no servidor que nós configuramos.

Mostrando Logs

Logs são bem importantes e o Meteor Up provê uma maneira bem fácil de lidar com eles ao emular o comando tail -f. Apenas digite:

$ mup logs -f

Isto resume a visão geral do que o Meteor Up pode fazer. Para mais informação, nós sugerimos visitar o repositório GitHub do Meteor Up.

Estas três maneiras de implementar aplicativos em Meteor deve ser o suficiente para a maioria dos casos. Claro, nós sabemos que você prefere estar em total controle e configurar seu servidor Meteor do princípio. Mas isto é um tópico para outro dia… ou talvez outro livro!

Templates

3

Para facilitar o desenvolvimento em Meteor, nós adotaremos uma abordagem de fora para dentro. Em outras palavras, nós iremos constuir uma casca externa “burra” com HTML/JavaScript primeiro, e então associar com o funcionamento interno de nosso aplicativo no subsequentes trabalhos.

Isto significa que neste capítulo nós apenas nos preocuparemos com o que está acontecendo dentro do diretório /client.

Vamos criar um novo arquivo chamado main.html dentro de nosso diretório /client, e vamos preenchê-lo com o seguinte código:

<head>
  <title>Microscope</title>
</head>
<body>
  <div class="container">
    <header class="navbar">
      <div class="navbar-inner">
        <a class="brand" href="/">Microscope</a>
      </div>
    </header>
    <div id="main" class="row-fluid">
      {{> postsList}}
    </div>
  </div>
</body>
client/main.html

Este será nosso principal template do aplicativo. Como você pode ver é HTML puro exceto por uma única tag {{> postsList}}“, a qual é um ponto de inserção para o template postList como logo veremos. Por enquanto, vamos criar mais alguns templates.

Meteor Templates

No seu âmago, um site de notícias sociais é composto de postagens organizadas em listas, e é exatamente assim que iremos organizar nossos templates.

Vamos criar um diretório /views dentro de /client. Isto irá ser onde nós colocaremos nossos templates, e para manter as coisas organizadas nós também criaremos /posts dentro de /views apenas para nossos templates relacionados com postagens.

Finding Files

O Meteor é incrível para encontrar arquivos. Não importa onde você colocou o seu código no diretório /client, o Meteor irá encontrá-lo e compilá-lo corretamente. Isto significa que você nunca precisará manualmente escrever caminhos para inclusão de arquivos JavaScript ou CSS.

Isto também significa que você poderia muito bem colocar todos os seus arquivos no mesmo diretório, ou ainda todo o seu código em um mesmo arquivo. Mas desde que o Meteor vai compilar tudo em um único arquivo minificado de qualquer forma, é melhor que mantemos as coisas bem organizadas e utilize uma estrutura clara de arquivos.

Nós finalmente estamos prontos para nosso segundo template. Dentro de client/views/posts, crie posts_list.html:

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}
  </div>
</template>
client/views/posts/posts_list.html

E post_item.html:

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
    </div>
  </div>
</template>
client/views/posts/post_item.html

Note o atributo name="postList" do elemento template. Este é o norme que será utilizado pelo Meteor para acompanhar qual template vai aonde.

É hora de introduzir o sistema de templating do Meteor, o Handlebars. Handlebars é simplesmente HTML, com a adição de três coisas: parciais (partials), expressões (expressions) e blocos de ajuda (block helpers).

As parciais usam a síntaxe {{> templateName}}, e simplemente dizem ao Meteor para substituir a parcial com o template de mesmo nome (no nosso caso postItem).

Expressões tais como {{title}} ou chamam a propriedade do objeto atual, ou retornam o valor de um auxiliar do template como definido no gerenciador atual do template (logo mais sobre isso).

Finalmente, blocos de ajuda são tags especiais que controlam o fluxo do template tais como {{#each}}...{{\each}} ou {{#if}}...{{\if}}.

Indo Além

Você pode visitar o site oficial de Handlebars ou este prático tutorial se quiser aprender mais sobre Handlebars.

Armado com este conhecimento, nós podemos facilmente entender o que está acontecendo aqui.

Primeiro, no template postsList, nós estamos iterando sobre um objeto posts com o bloco de ajuda {{#each}}...{{\each}}. Então, para cada iteração nós incluimos o template postItem.

De onde este objeto posts se origina? Boa pergunta. Se trata na verdade de um ajudante do template, e nós iremos defini-lo quando olharmos para os gerenciadores de template.

O template postItem em si mesmo é bem direto. Apenas usa três expressões: {{url}} e {{title}} ambas retornando as propriedades do documento, e {{domain}} que chama um ajudante do template.

Nós mencionamos "ajudantes do template” bastante ao longo deste capítulo sem realmente explicar o que eles fazem. Mas para consertar isso, primeiro precisamos falar sobre gerentes.

Gerentes de Template

Até agora nós estávamos lidando com Handlebars, que nada mais é do que HTML acrescido de algumas tags especiais. Diferentemente de outras linguagens de programação como o PHP (ou até mesmo páginas HTML comuns, que podem incluir JavaScript), o Meteor mantém os templates e as lógicas separadas e esses templates não fazem muita coisa por si só.

Para fazer com que eles ganhem vida é necessário um gerente. Você pode pensar em um gerente como um chef de cozinha que pega os ingredientes crus (seus dados) e prepara eles antes de entregar o prato finalizado ao garçom (o template) que o apresenta a você.

Em outras palavras, enquanto o papel do template se limita a exibir ou iterar variáveis, o gerente é aquele que faz todo o trabalho pesado atribuindo valores para cada uma dessas variáveis.

Gerentes?

Quando nós perguntarmos por aí a outros desenvolvedores Meteor como eles chamam esses “gerentes de template”, metade respondeu “controllers” e a outra metade respondeu “aqueles arquivos onde eu coloco meu código JavaScript”.

Gerentes não são de fato “controllers” (pelo menos não no sentido de controllers do MVC) e como a sigla “AAOECMCJ” (Aqueles Arquivos Onde Eu Coloco Meu Código Javascript) não é muito atraente, nós acabamos rejeitando as duas opções.

Como ainda precisávamos usar um nome para indicar aquilo a que estávamos nos referindo, nós optamos pelo termo “gerente” como uma alternativa viável já que não tinha um significado relacionado a nenhum outro framework web.

Para simplifcar as coisas, nós iremos adotar a mesma convenção de nomes utilizada para o template, com exceção da utilização da extensão .js. Vamos criar o arquivo posts_list.js dentro do diretório /client/views/posts e começar a definir nosso primeiro gerente:

var postsData = [
  {
    title: 'Introducing Telescope',
    author: 'Sacha Greif',
    url: 'http://sachagreif.com/introducing-telescope/'
  }, 
  {
    title: 'Meteor',
    author: 'Tom Coleman',
    url: 'http://meteor.com'
  }, 
  {
    title: 'The Meteor Book',
    author: 'Tom Coleman',
    url: 'http://themeteorbook.com'
  }
];
Template.postsList.helpers({
  posts: postsData
});
client/views/posts/posts_list.js

Se você fez tudo certo, algo parecido com isso irá aparecer no seu navegador:

Nosso primeiro template com dados estáticos
Nosso primeiro template com dados estáticos

Commit 3-1

Template de listagem de posts e dados estáticos adicionados.

Nós estamos fazendo duas coisas aqui. Primeiro estamos colocando alguns dados de teste no array postsData. Esses dados normalmente seriam provenientes de um banco de dados, mas como ainda não vimos como acessá-lo (aguarde até o próximo capítulo) nós estamos “trapaceando” usando alguns dados estáticos. Depois estamos usando a função Template.myTemplate.helpers() do Meteor para definir um ajudante de template chamado posts que simplesmente nos retornará o array postsData.

Definir o ajudante posts significa que ele agora estará disponível para ser usado pelo nosso template:

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}
  </div>
</template>
client/views/posts/posts_list.html

Agora será possível que nosso template itere sobre o array postsData e envie cada objeto contido dentro dele para o template postItem.

O valor do “this”

Nós iremos criar agora o gerente post_item.js

Template.postItem.helpers({
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  }
});
client/views/posts/post_item.js

Commit 3-2

Definição do ajudante `domain` no template `postItem`.

Desse vez o valor do ajudante domain não é um array, mas uma função anônima. Esse pattern é muito mais comum (e mais útil) se comparado aos nossos exemplos com dados de teste anteriores.

Exibindo os domínios de cada link.
Exibindo os domínios de cada link.

O ajudante domain recebe uma URL e retorna seu domínio utilizando um pouco da mágica do Javascript. Mas, pra começar, de onde ela pega essa URL?

Para responder a essa pergunta nós precisamos voltar ao nosso template posts_list.html. O ajudante {{#each}} não apenas itera um array mas também atribui o valor do this dentro do bloco do objeto iterado.

Isso significa que entre as duas tags {{#each}}, cada post é atribuído ao this sucessivamente e isso também se aplica ao gerente de template (post_item.js).

Agora nós entendemos o porque do this.url retornar a URL do post atual. E além disso, se nós usarmos {{title}} e {{url}} dentro do nosso template post_item.html, o Meteor sabe que isso significa this.title e this.url e retorna os valores corretos.

Mágica do Javascript

Embora isso não seja especifíco do Meteor, aqui vai uma breve explicação a respeito dessa pequena “mágica do Javascript” descrita acima. Primeiro, nós estamos criando um elemento HTML de âncora (a) vazio e armazenando-o na memória.

Depois colocamos em seu atributo href a URL do post atual (como nós já vimos, o this corresponde ao objeto sob a qual a ação está acontecendo).

Por último, nós aproveitamos o fato de que o elemento a tem uma a propriedade especial chamada hostname para recuperar o nome de domínio do link sem o restante da URL.

Se você seguiu tudo corretamente, deverá estar vendo agora uma lista de posts no seu navegador. Essa lista é composta apenas por dados estáticos, portanto ela ainda não tira vantagem dos recursos real-time do Meteor. Nós iremos mostrar como mudar isso no próximo capítulo!

Recarregamento Automático de Código

Você deve ter notado que não precisa nem recarregar a janela do seu navegador quando altera um arquivo.

Isso acontece porque o Meteor rastreia todos os arquivos dentro do diretório do seu projeto e automaticamente dá refresh no seu navegador sempre que detecta mudanças em algum desses arquivos.

O recarregamento automático de código do Meteor é bem esperto! Ele até preserva o estado da sua aplicação entre um refresh e outro!

Usando Git & GitHub

Sidebar 3.5

GitHub é um repositório social de projetos open-source baseado no sistema Git de controle de versão, e sua função principal é tornar fácil compartilhar código e colaborar em projetos. Mas também é um ótimo instrumento de ensino. Nesta barra lateral, nós vamos passar rapidamente por algumas maneiras que você pode utilizar o GitHub para acompanhar o Descubra Meteor.

Esta barra lateral supõe que você não está tão familiarizado com Git e GitHub. Se você já está confortável com ambos, sinta-se livre para pular este capítulo!

Sendo Committed

O bloco básico de trabalho de um repositório git é um commit. Você pode pensar o commit como um instantâneo do estado do seu banco de código em um dado momento no tempo.

Ao invés de simplesmente dar a você o código finalizado do Microscope, nós tiramos estes instantâneos a cada passo do caminho, e você pode ver todos eles online no GitHub.

Por exemplo, isto é o que o último commit do capítulo prévio se parece com:

A Git commit as shown on GitHub.
A Git commit as shown on GitHub.

O que você vê aqui é a “diff” (de “difference”) do arquivo post_item.js, em outras palavras as mudanças introduzidas por este commit. Neste caso, nós criamos o arquivo post_item.js do princípio, então todo seu conteúdo está destacado em verde.

Vamos comparar com um exemplo de mais adiante no livro:

Modifying code.
Modifying code.

Desta vez, apenas as linhas modificadas estão destacadas em verde.

E claro, às vezes você não está adicionando ou modificando linhas de código, mas deletando-as:

Deleting code.
Deleting code.

Então nós vimos o primeiro uso do GitHub: ver o que mudou num relance.

Procurando um Código de Commit

A vista commit do Git nos mostra as mudanças incluídas neste commit, mas às vezes você pode querer ver arquivos que não foram modificados, apenas para ter certeza de como seus códigos devem se parecer neste estágio do processo.

Mais uma vez o GitHub vem a nós. Quando você está numa página commit, clique no botão Browse code:

The Browse code button.
The Browse code button.

Você agora terá acesso ao repo como ele está num commit específico:

The repository at commit 3-2.
The repository at commit 3-2.

GitHub não nos dá muitas dicas visuais de que nós estamos olhando um commit, mas você pode comparar com a vista master “normal” e ver num relance que a estrutura do arquivo é diferente:

The repository at commit 14-2.
The repository at commit 14-2.

Acessando Um Commit Localmente

Nós acabamos de ver como procurar o código inteiro de um commit online no GitHub. Mas e se você quiser fazer o mesmo localmente? Por exemplo, você pode querer rodar o app localmente num específico commit para ver como ele deveria se comportar neste ponto do processo.

Para fazer isto, nós tomaremos nossos primeiros passos (bem, neste livro ao menos) com o utilitário de linha de comando git. Para iniciantes, tenha certeza de que você tem Git instalado. Então clone (em outras palavras, baixe a cópia localmente) o repositório do Microscope com:

$ git clone git@github.com:DiscoverMeteor/Microscope.git github_microscope

Esse github_microscope no final é simplesmente o nome do diretório local para o qual você estará clonando o app. Supondo que você já tem um diretório microscope pre-existente, apenas escolha qualquer nome diferente (não precisa ter o mesmo nome do GitHub repo).

Vamos cd para dentro do repositório para que nós possamos começar a utilizar o utilirário de linha de comando git:

$ cd github_microscope

Agora quando nós clonamos o repositório GitHub, nós baixamos todo o código do app, o que significa que nós estamos olhando o código do último commit.

Felizmente, há um jeito de voltar no tempo e “selecionar” um commit específico sem afetar os outros. Vamos tentar isto:

$ git checkout chapter3-1
Note: checking out 'chapter3-1'.

Você está no estado 'detached HEAD'. Você pode olhar ao redor, fazer mudanças experimetnais e cometê-las, e você pode descartar qualquer commits que você fez neste estado sem afetar nenhum branch ao efetuar outro checkout.

Se você quer criar um novo branch para preservar os commits que você cria, você pode fazê-lo (agora ou mais tarde) usando -b com o comando checkout de novo. Exemplo:

  git checkout -b new_branch_name

HEAD is now at a004b56... Adicionado template básicos da lista de artigos e informação estática.

Git nos informa que nós estamos no “detached HEAD”, o que significa que até onde o Git se importa, nós podemos observar commits passados mas nós não podemos modificá-los. Você pode pensar nisto como um mago inspecionando o passado através de uma bola de cristal.

(Note que o Git também tem comandos que permitem você mudar commits passados. Isto seria mais como um viagem do tempo voltando no tempo e possivelmente pisando numa borboleta, mas isto está além do escopo desta breve introdução).

A razão de porque você foi capaz de simplesmente digitar chapter3-1 é que nós pre-tagged todos os commits do Microscope com o marcador de capítulo certo. Se este não fosse o caso, você precisaria primeiro encontrar a hash do commit, ou identificador único.

Mais uma vez, o GitHub faz nossas vidas mais fácil. Você pode encontrar a hash do commit no canto inferior direito da caixa de cabeçalho azul do commit, como mostrado aqui:

Finding a commit hash.
Finding a commit hash.

Então vamos tentar isto com a hash ao invés da tag:

$ git checkout c7af59e425cd4e17c20cf99e51c8cd78f82c9932
Previous HEAD position was a004b56... Added basic posts list template and static data.
HEAD is now at c7af59e... Augmented the postsList route to take a limit

E finalmente, e se nós quisessemos parar de olhar pela bola mágia de cristal e voltar para o presente? Nós dizemos ao Git que nós queremos check out o master branch:

$ git checkout master

Note que você pode também rodar o app com o comando meteor em qualquer ponto do processo, até quando em estado “detached HEAD”. Você pode precisar rodar um rápido mrt update primeiro se o Meteor reclamar de pacotes faltando, já que o código de pacote não está incluído no Git repo do Microscope.

Perspectiva Histórica

Aqui mais outro cenário comum: você está olhando um arquivo e percebe algumas mudanças que você não tinha visto antes. A questão é, você não pode lembrar quando um arquivo mudou. Você poderia apenas olhar cada commit um a um até você encontrar o certo, mas há uma forma mais fácil graças ao utensílio História do GitHub.

Primeiro, acesse um dos arquivos do repositório no GitHub, então encontre o botão “History”:

GitHub's History button.
GitHub’s History button.

Você agora tem uma lista arrumada de todos os commits que afetaram este arquivo em particular:

Displaying a file's history.
Displaying a file’s history.

O Jogo da Culpa

Para finalizar, vamos dar uma olhada na Culpa:

GitHub's Blame button.
GitHub’s Blame button.

Esta vista arrumada nos mostra linha por linha quem modificou o arquivo, e em cada commit (em outras palavras, quem culpar quando as coisas não estão funcionando mais):

GitHub's Blame view.
GitHub’s Blame view.

Agora, Git é uma ferramenta razoavelmente complexa - e o GitHub também -, então nós não podemos esperar cobrir tudo num único capítulo. Na verdade, nós mal arranhamos a superfície do que é possível com essas ferramentas. Mas felizmente, até mesmo este pouquinho será útil assim que você seguir pro resto deste livro.

Collections

4

No primeiro capítulo, nós falamos sobre a característica central do Meteor, a sincronização automática de informação entre o cliente e o servidor.

Neste capítulo, nós olharemos mais de perto como isso funciona, e observaremos a operação da peça chave da tecnologia que permite isso, a Meteor Collection.

Nós estamos construindo um aplicativo social de notícias, então a primeira coisa que nós queremos é fazer uma lista de links que as pessoas postaram. Nós chamaremos cada um desses ítens de “post”.

Naturalmente, nós precisamos armazenar esses posts em algum lugar. Meteor vem integrado com um banco de dados Mongo que roda no servidor e é o seu banco de memória persistente.

Então, apesar do navegador de um usuário poder conter algum tipo de estado (por exemplo que página eles estão, ou o comentário que eles estão digitando agora), o servidor, e especialmente Mongo, contém a fonte de informação permanente e canônica. Por canônica, nós queremos dizer que é a mesma para todos usuários: cada usuário pode estar numa página diferente, mas a lista mestre de posts é a mesma para todos.

Esta informação é armazenada no Meteor na Coleção. Uma coleção é um tipo de estrutura de dados especial que, através de publicações e assinaturas, toma conta de sincronizar informação em tempo real de e para o navegador de cada usuário conectado e até o banco de dados Mongo. Vamos ver como.

Nós queremos que nossos posts sejam permanentes e compartilhados entre usuários, então nós começaremos criando uma coleção chamada Posts para armazená-los. Se você ainda não fez isso crie uma pasta collections/ na raíz do seu aplicativo, e então um arquivo posts.js dentro dela. Então adicione:

Posts = new Meteor.Collection('posts');
collections/posts.js

Commit 4-1

Added a posts collection

Código dentro de pastas que não são client/ ou server/ rodarão em ambos contextos. Então a coleção Posts está disponível para tanto cliente quanto servidor. Entretanto, o que a coleção faz em cada ambiente é bem diferente.

Usar Var Ou Não Usar Var?

Em Meteor, a palavra chave var limita o escopo de um objeto ao arquivo atual. Aqui, nós queremos fazer a coleção Posts disponível para todo nosso aplicativo, este é o porquê nós não estamos usando a palavra chave var.

No servidor, a coleção tem o trabalho de conversar com o banco de dados Mongo, e ler e escrever qualquer mudanças. Neste sentido, pode ser comparada a uma biblioteca de bancos de dados padrão. No cliente entretanto, a coleção é uma cópia segura de um subconjunto da real coleção canônica. A coleção do lado do cliente é constantemente e (em sua maior parte) transparentemente mantida atualiza com esse subconjunto em tempo real.

Console vs Console vs Console

Neste capítulo, nós começaremos a fazer uso do browser console, que não deve ser confundido com o terminal ou o Mongo shell. Aqui está uma rápida demão sobre cada um deles.

Terminal

The Terminal
The Terminal
  • Chamado do seu sistema operacional.
  • Lado do Servidor console.log() chama o output aqui.
  • Prompt: $.
  • Também conhecido por: Shell, Bash

Console do Navegador

The Browser Console
The Browser Console
  • Chamando de dentro do navegador, executa código JavaScript.
  • Lado do Cliente console.log() chama o output aqui.
  • Prompt: .
  • Também conhecido por: JavaScript Console, DevTools Console

Mongo Shell

The Mongo Shell
The Mongo Shell
  • Chamado pelo terminal através de meteor mongo.
  • Dá acesso direto ao banco de dados do seu aplicativo.
  • Prompt: >.
  • Também conhecido por: Mongo Console

Note que em cada caso, você não deve digitar o caracter prompt ($, , ou >) como parte do comando. E você pode assumir que qualquer linha que não comece com o prompt é o resultado do comando anterior.

Coleções do Lado do Servidor

No servidor, a coleção atua como uma API dentro do seu banco de dados Mongo. No código do servidor, isto permite que você escreva comandos Mongo como Posts.insert() ou Posts.update(), e eles farão mudanças na coleção posts armazenada dentro do Mongo.

Para ver dentro do banco de dados Mongo, abra uma segunda janela do terminal (enquanto o meteor ainda está rodando na sua primeira), e vá até o diretório do aplicativo. Então, rode o comando meteor mongo para inicar um Mongo shell, no qual você pode digitar comandos Mongo (e como costumeiramente, você pode sair com o atalho de teclado ctrl+c). Por exemplo, vamos inserir um novo post:

> db.posts.insert({title: "A new post"});

> db.posts.find();
{ "_id": ObjectId(".."), "title" : "A new post"};
The Mongo Shell

Mongo no Meteor.com

Note que quando hospedar um aplicato em *.meteor.com, você pode também acessar a Mongo shell do seu aplicativo hospedado com meteor mongo myApp.

E já que estamos falando disso, você também pode acessar os logs do seu aplicativo digitando meteor logs myApp.

A sintaxe do Mongo é familiar, já que utiliza uma interface JavaScript. Nós não faremos nenhuma outra manipulação de informação na Mongo shell, mas nós poderemos dar uma olhada nela de tempos em tempos para nos assegurar do que está lá.

Coleções do Lado do Cliente

Coleções se tornam mais interessantes do lado do cliente. Quando você declara Posts = new Meteor.Collection('posts'); no cliente, o que você está criando é um cache em tempo real no navegador da verdadeira coleção Mongo. Quando nós falamos sobre coleção do lado do cliente ser um “cache”, nós queremos dizer que ela contém um subconjunto da sua informação, e oferece um acesso muito veloz a mesma.

É importante entender este ponto já que é fundamental para o modo como o Meteor funciona. Em geral, a coleção do lado do cliente consiste em um subconjunto de todos os documentos armazenados na coleção Mongo (até mesmo porque nós geralmente não queremos mandar todo nosso banco de dados para o cliente).

Segundo, esses documentos estão armazenados na memória do navegador, o que significa que acessá-los é basicamente instantâneo. Então não há viagens lentas até o servidor ou ao banco de dados para coletar essa informação quando você chama Posts.find() no cliente, já que a informação já está pré-carregada.

Introduzindo MiniMongo

A implementação do Meteor para o Mongo no lado do cliente se chama MiniMongo. Não é uma implementação perfeita ainda, e você pode encontrar ocasionalmente características Mongo que não funcionam no MiniMongo. De qualquer forma, todas as característcas que nós cobrimos neste livro funcionam de modo similar tanto no Mongo quanto no MiniMongo.

Comunicação Cliente-Servidor

A peça chave para tudo isso é como a coleção do lado do cliente sincroniza sua informação com a coleção do lado do servidor com o mesmo nome ('posts' no nosso caso).

Ao invés de explicar isto em detalhe, vamos apenas ver o que acontece.

Comece abrindo duas janelas no navegador, e acessando o console do navegador em cada uma. Então, abra a Mongo shell na linha de comando. Neste ponto, nós devemos ver um único documento que nós criamos anteriormente em todos os três contextos.

> db.posts.find();
{title: "A new post", _id: ObjectId("..")};
The Mongo Shell
 Posts.findOne();
{title: "A new post", _id: LocalCollection._ObjectID};
First browser console

Vamos criar um novo post. Em uma das janelas do navegador, rode um comando insert:

 Posts.find().count();
1
 Posts.insert({title: "A second post"});
'xxx'
 Posts.find().count();
2
First browser console

Sem surpresas, o post chegou na coleção local. Agora vamos checar o Mongo:

❯ db.posts.find();
{title: "A new post", _id: ObjectId("..")};
{title: "A second post", _id: 'yyy'};
The Mongo Shell

Como você pode ver, o post fez todo o caminho até o banco de dados Mongo, sem nós escrevermos uma única linha de código que conectasse o cliente até o servidor (bem, estritamente falando, nós escrevemos uma única linha de código: new Meteor.Collection('posts')). Mas isto não é tudo!

Vá à segunda janela do navegador e digite isto no console do navegador:

 Posts.find().count();
2
Second browser console

O post está lá também! Mesmo apesar de nós nunca termos atualizado ou sequer interagido com o segundo navegador, e nós certamente não escrevemos nenhum código para empurrar atualizações. Tudo aconteceu magicamente – e instantaneamente também, apesar que isto se tornará mais óbvio mais tarde.

O que acontece é que a nossa coleção do lado do servidor foi informada pela nossa coleção do cliente quanto ao novo post, e tomou a missão de distribuir este post até o banco de dados Mongo e de volta para todas as outras coleções post conectadas.

Trazer posts no console do navegador não é tão útil assim. Nós aprenderemos como escrever esta informação nos nossos templates, e no processo transformar nosso simples protótipo HTML em um aplicativo funcional em tempo real.

Mantendo em Tempo Real

Olhar para os contéudos das nossas Coleções no console do browser é uma coisa, mas o que nós realmente gostaríamos de mostrar é a informação, e as mundanças a esta informação, na tela. Fazendo isto nós tornaremos nosso aplicativo de uma página web com contéudo estático, em um aplicativo web em tempo real com contéudo dinâmico e mutável.

Vamos descobrir como.

Povoando o Banco de Dados

A primeira coisa que nós faremos é por alguma informação no banco de dados. Nós o faremos como um arquivo demonstrativo que lê um conjunto de informação estruturada na coleção Posts quando o servidor inicia pela primeira vez.

Primeiro, vamos ter certeza que não há nada no banco de dados. Nós usaremos meteor reset, o qual apaga seu banco de dados e reseta seu projeto. Claro, você irá querer ser bem cuidadoso com este comando uma vez que você começar a trabalhar em projeto do mundo real.

Finalize o servidor Meteor (pressionando ctrl-c) e então, na linha de comando, rode:

$ meteor reset

O comando reset completamente limpa o banco de dados Mongo. É um comando útil em desenvolvimento, onde há uma forte possibilidade do nosso banco de dados cair em um estado inconsistente.

Agora que nosso banco de dados está vazio, nós podemos adicionar o código seguinte que irá carregar três posts sempre que o servidor iniciar e encontrar a coleção Posts vazia:

if (Posts.find().count() === 0) {
  Posts.insert({
    title: 'Introducing Telescope',
    author: 'Sacha Greif',
    url: 'http://sachagreif.com/introducing-telescope/'
  });

  Posts.insert({
    title: 'Meteor',
    author: 'Tom Coleman',
    url: 'http://meteor.com'
  });

  Posts.insert({
    title: 'The Meteor Book',
    author: 'Tom Coleman',
    url: 'http://themeteorbook.com'
  });
}
server/fixtures.js

Commit 4-2

Added data to the posts collection.

Nós colocamos este arquivo no diretório server/, então ele nunca será lido em nenhum navegador de usuário. O código rodará imediatamente quando o servidor começar, e fará chamadas insert no banco de dados para adicionar três posts simples na nossa coleção Posts. Como nós não construímos nenhuma segurança de informação ainda, não há diferença real entre fazer isso em um arquivo que roda no servidor ou no navegador.

Agora rode seu servidor de novo com meteor, e estes três posts serão carregados no banco de dados.

Conectando a informação ao nosso HTML com ajudantes

Agora, se nós abrirmos o console do navegador, nós veremos três posts carregados no MiniMongo:

 Posts.find().fetch();
Browser console

Para ter esses posts em HTML renderizado, nós podemos usar um ajudante de template. No Capítulo 3 nós vimos como o Meteor nos permite ligar um contexto de informação aos nossos templates do Spacebars para construir vistas HTML de estruturas de dados simples. Nós podemos ligar na nossa coleção informação do exato mesmo modo. Nós apenas substituiremos nosso objeto JavaScript estático postsData com uma coleção dinâmica.

Falando nisso, sinta-se livre para deletar o código postsData. Aqui está o que posts_list.js deve se parecer agora:

Template.postsList.helpers({
  posts: function() {
    return Posts.find();
  }
});
client/views/posts/posts_list.js

Commit 4-3

Wired collection into `postsList` template.

Encontre & Traga

No Meteor, find() retorna um cursor, que é uma fonte de informação reativa. Quando nós queremos logar seu contéudo, nós podemos então usar fetch() no cursor para transformá-lo num array.

Dentro do aplicativo, o Meteor é inteligente o suficiente para saber como iterar sobre cursores sem precisar convertê-los explicitamente em arrays primeiro. Esta é razão de você não ver fetch() com tanta frequência em código de Meteor (e o porquê de não o usarmos no exemplo acima).

Agora, ao invés de puxarmos uma lista de posts como um array estático de uma variável, nós retornamos um cursor para o nosso ajudante posts. Mas o que isto faz? Se nós voltarmos ao nosso navegador, nós vemos:

Using live data
Using live data

Então nós podemos claramente ver que o nosso ajudante {{#each}} iterou sobre todo o nosso Posts, e os mostrou na tela. A coleção do lado do servidor puxou os posts do Mongo, passou-o pelo fio até a nossa coleção do lado do cliente, e nosso ajudante do Spacebars passou-os para o template.

Agora, nós tomaremos um passo adiante; vamos adicionar outro post através do console:

 Posts.insert({
  title: 'Meteor Docs', 
  author: 'Tom Coleman', 
  url: 'http://docs.meteor.com'
});
Browser console

Olhe de volta ao navegador – você deve ver isto:

Adding posts via the console
Adding posts via the console

Você acabou de ver a reatividade em ação pela primeira vez. Quando nós dissêmos ao Spacebars para iterar sobre o cursor Posts.find(), ele sabia como observar este cursor quanto a mudanças, e remendar o HTML da forma mais simples para mostrar a informação correta na tela.

Inspecionando mudanças no DOM

Neste caso, a mudança mais simples possível foi a de adicionar mais outro <div class="post">...</div>. Se você quer ter certeza que isto é realmente o que aconteceu, abra o inspector do DOM e selecione o <div> correspondente a um dos posts existentes.

Agora, no console JavaScript, insira mais outro post. Quando você voltar ao inspector, você verá um <div> extra, correspondente ao novo post, mais você ainda terá o mesmo <div> existente selecionado. Isto é uma forma útil de dizer quando elementos foram re-renderizados e quando eles foram deixados de lado.

Conectando Coleções: Publicações e Assinaturas

Até então, nós tínhamos o pacote autopublish ativado, que não foi feito para aplicações em produção. Como seu nome indica, este pacote simplesmente diz que cada coleção deve ser compartilhada na íntegra com cada cliente conectado. Isto não é o que realmente queremos, então vamos desativá-lo.

Abra uma nova janela do terminal, e digite:

$ meteor remove autopublish

Isto tem um efeito instantâneo. Se você ver no seu navegador agora, você verá que todos os nossos posts desapareceram! Isto é porque nós estávamos dependendo do autopublish para assegurar que a nossa coleção de posts do lado do cliente fosse um espelho de todos os posts no banco de dados.

Eventualmente nós precisaremos assegurar que nós estamos transferindo apenas os posts que o usuário de fato precisa ver (levando em conta coisas como paginação). Mas por enquanto, nós apenas configuraremos o Posts para ser publicado na íntegra.

Para fazê-lo, nós criamos uma simples função publish() que retorna um cursor referente a todos os posts:

Meteor.publish('posts', function() {
  return Posts.find();
});
server/publications.js

No cliente, nós precisamos assinar a publicação. Nós apenas adicionaremos a linha seguinte ao main.js:

Meteor.subscribe('posts');
client/main.js

Commit 4-4

Removed `autopublish` and set up a basic publication.

Se nós checarmos o navegador novamente, nossos posts voltaram. Ufa!

Conclusão

Então o que nós alcançamos? Bem, apesar de nós não termos uma interface do usuário ainda, o que nós temos agora é um aplicativo web funcional. Nós poderíamos lançar este aplicativo à Internet, e (usando o console do navegador) começar a postar novas estórias e vê-las aparecer nos navegadores de outros usuários pelo mundo todo.

Publicações e Subscrições

Sidebar 4.5

Publicações e subscrições são uns dos mais importantes conceitos no Meteor, mas pode ser difícil você entender enquanto está começando.

Isso levou a uma série de mal-entendidos, como a crença de que o Meteor é inseguro, ou que as aplicações Meteor não podem lidar com grandes quantidades de dados.

Uma grande parcela do motivo das pessoas inicialmente acharem estes conceitos um pouco confusos é a “mágica” que o Meteor faz por nós. Embora essa mágica seja muito útil, ela pode obscurecer o que está de fato acontecendo por trás das cenas (o que uma mágica tende a fazer). Então vamos analisar as camadas dessa mágica e entender o que está acontecendo.

Os Velhos Tempos

Mas primeiro, vamos olhar para os bons velhos tempos em 2011 quando o Meteor ainda não havia sido lançado. Vamos dizer que você está fazendo um simples aplicativo Rails. Quando um usuário entra no site, o cliente (i.e seu navegador) manda uma requisição para sua aplicação, que está vivendo no servidor.

O primeiro trabalho da aplicação é perceber qual dado o usuário precisa de visualizar. Isso pode ser a página 12 dos resultados da busca, informação do perfil de usuário da Mary, os últimos 20 tweets de Bob, e por ai vai. Você pode nisso como sendo, basicamente, um atendente de livraria navegando entre os corredores para encontrar o livro que você pediu.

Uma vez que o dado correto tenha sido selecionado, o segundo trabalho da aplicação é traduzir este dado em um belo e legível HTML (ou JSON no caso de uma API).

Na metáfora da livraria, isso seria embrulhar o livro que você comprou em uma embalagem e colocá-lo em uma bela bolsa. Esta é a parte “View” (visão/vista) do famoso modelo Model-View-Controller (Modelo-Visão-Controlador).

Finalmente, a aplicação pega o código HTML e manda para o navegador. O trabalho da aplicação é finalizado, e agora que tudo está fora de suas mãos virtuais você pode pegar uma cerveja enquanto espera pela próxima requisição.

O Caminho do Meteor

Vamos rever o que faz o Meteor ser tão especial em comparação. Como vimos, a principal inovação do Meteor é que enquanto aplicações Rails estão vivas apenas no servidor, um aplicativo Meteor inclui um componente no lado do cliente que vai rodar no cliente (o navegador).

Colocando um subgrupo do banco de dados no cliente.
Colocando um subgrupo do banco de dados no cliente.

Isso é como um balconista que não apenas encontra o livro pra você, mas também te segue até em casa e lê o livro para você à noite (fato que vamos admitir soar um pouco assustador).

Essa arquitetura permite ao Meteor fazer várias coisas legais, principalmente com o que o Meteor chama de database everywhere (banco de dados em todos os lugares). Simplesmente, Meteor vai pegar uma parte do seu banco de dados e copiá-lo para o cliente.

Isso tem duas grandes implicações: primeira, ao invés de mandar código HTML para o cliente, a aplicação Meteor vai mandar o real, dado puro e o cliente vai lidar com ele (data on the wire - dado na rede). Segundo, você vai ser capaz de acessar o dado instantaneamente sem ter que esperar por uma ida e volta do servidor (latency compensation - compensação de latência).

Publicação

O banco de dados de um aplicativo pode conter dez mil documentos, sendo alguns destes privados ou sensíveis. Então obviamente não devemos simplesmente espelhar nosso banco de dados completamente no cliente, por razões de segurança e escabilidade.

Então vamos precisar de uma forma de dizer ao Meteor quais subgrupos de dados podem ser enviados ao cliente, e vamos realizar isso através de uma publicação.

Vamos voltar ao Microscope. Aqui estão todos os posts do no aplicativo situados no banco de dados:

Todos os posts contidos no nosso banco de dados.
Todos os posts contidos no nosso banco de dados.

Embora esse recurso reconhecidamente não exista no Microscope, vamos imaginar que alguns de nossos posts tenham sido marcados por linguagem abusiva. Mesmo que queiramos que eles continuem em nosso banco de dados, eles não devem estar disponíveis para os usuários (i.e enviados ao cliente).

Nossa primeira tarefa vai ser dizer ao Meteor qual dado nós vamos querer enviar ao cliente. Vamos dizer ao Meteor que desejamos publicar apenas os posts sem marcações:

Excluindo posts marcados.
Excluindo posts marcados.

Aqui temos o código correspondente, que deve estar no servidor:

// no servidor
Meteor.publish('posts', function() {
  return Posts.find({flagged: false});
});

Isso assegura para que não exista uma forma de um cliente estar apto a acessar post marcados. Isso é exatamente a forma que você torna uma aplicação Meteor segura: apenas assegure-se de publicar dados que você queira que estejam disponíveis para acesso no cliente.

DDP

Fundamentalmente, você pode pensar sobre o sistema de publicação/subscrição como um funil que transfere dados de uma coleção no lado do servidor para uma coleção no lado do cliente.

O protocolo que é utilizado neste funil é chamado DDP (que significa Distributed Data Protocol - Protocolo de Dados Distribuidos). Para aprender mais sobre DDP, você pode assistir essa palestra da Real-Time Conference por Matt DeBergalis (um dos fundadores do Meteor), ou este screencast por Chris Mather que conduz você por este conceito com um pouco mais de detalhe.

Subscrição

Mesmo que seja nossa intenção deixar qualquer publicação não marcada disponível para os clientes, nós não podemos simplesmente enviar milhares de publicações de uma vez. Nós precisamos de uma forma para os clientes especificarem qual subgrupo de dados eles precisam em um momento em particular, e é exatamente ai que as subscrições entram.

Todo dado que você se subscrever vai ser espelhado no cliente graças ao Minimongo, a implementação do MongoDB feita pelo Meteor no lado do cliente.

Por exemplo, vamos dizer que estamos atualmente navegando na página perfil de Bob Smith, e somente queremos mostrar seus posts.

Subscrevendo nos posts de Bob irá espelhá-los no cliente .
Subscrevendo nos posts de Bob irá espelhá-los no cliente .

Primeiro, iríamos alterar nossa publicação para receber um parâmetro:

// no servidor
Meteor.publish('posts', function(author) {
  return Posts.find({flagged: false, author: author});
});

E podemos então definir este parâmetro quando nos subscrevermos nesta publicação no código do nosso aplicativo no lado do cliente:

// no cliente
Meteor.subscribe('posts', 'bob-smith');

Assim é como torna uma aplicação Meteor escalável no lado do cliente: Ao invés de se subscrever para todos os dados disponíveis, você somente escolhe as partes que você precisa atualmente. Dessa forma, você vai evitar sobrecarga de memória no browser não importando o quão grande seja seu banco de dados no lado do servidor.

Encontrando (finding)

Agora os posts de Bob devem ser espalhados em mútiplas categorias (por exemplo: “JavaScript”, “Ruby” e “Python”). Talvez ainda queiramos carregar todos os posts de Bob na memória, mas nós queremos mostrar somente aqueles da categoria “JavaScript” por agora. Nessa hora que “encontrar” (find) aparece.

Selecionando um subgrupo de documentos no cliente.
Selecionando um subgrupo de documentos no cliente.

Da mesma forma que fizemos no servidor, nós vamos usar a função Posts.find() para selecionar um subgrupo de nossos dados:

// no cliente
Template.posts.helpers({
  posts: function(){
    return Posts.find({author: 'bob-smith', category: 'JavaScript'});
  }
});

Agora que nós temos uma boa compreensão de como as publicações e subscrições funcionam, vamos mergulhar e rever alguns padrões comuns de implementação.

Publicação Automática (autopublishing)

Se você criar um projero Meteor do zero (i.e usando meteor create), ele vai ter automaticamente o pacote autopublish ativado. Para começar, vamos falar sobre o que ele exatamente faz.

O objetivo do autopublish é deixar extremamente simples o início da codificação da sua aplicação Meteor, e ele faz isso espelhando automaticamente todos os dados do servidor para o cliente, tomando conta das publicações e subscrições para você.

Autopublish
Autopublish

Como isso funciona? Suponha que você tenha uma coleção chamada 'posts' no servidor. Então autopublish vai automaticamente enviar cada post que encontrar na coleção posts no Mongo para uma coleção chamada 'posts' no cliente (assumindo que exista um).

Se você estiver usando autopublish, você não precisa de pensar sobre publicações. Os dados serão ubíquos (presentes em todos os lugares), e as coisas se tornam simples. Claro, existem problema óbvios de ter uma cópia completa do banco de dados da sua aplicação cacheada na máquina de cada usuário.

Por esse motivo, autopublish é apropriado somente quando você está começando, e ainda não pensou sobre as publicações

Publicando Coleções Completas

Uma vez que você remova o autopublish, você vai rapidamente perceber que todos os seus dados desapareceram do seu cliente. Uma forma simples de tê-los novamente é duplicando o que o autopublish faz, publicando uma coleção integralmente. Por exemplo:

Meteor.publish('allPosts', function(){
  return Posts.find();
});
Publicando uma coleção completa
Publicando uma coleção completa

Nós ainda estamos publicando coleções completas, mas ao menos agora temos controle sobre quais coleções publicamos ou não. Neste caso, estamos publicando a coleção Posts mas Comments não.

Publicando Coleções Parcialmente

O próximo nível de controle é publicar somente parte de uma coleção. Por exemplo somente os posts que pertencem a um certo autor:

Meteor.publish('somePosts', function(){
  return Posts.find({'author':'Tom'});
});
Publicando uma coleção parcialmente
Publicando uma coleção parcialmente

Nos Bastidores

Se você leu a documentação do Meteor sobre publicação, você talvez deve estar sobrecarregado com as instruções de se usar added() e ready() para configurar os atributos de registros no cliente, e se deve ter se esforçado para conciliar isso com os aplicativos Meteor que você viu e nunca usam esses métodos.

A razão é que o Meteor fornece uma importante conveniência: o método _publishCursor(). Você nunca viu seu uso? Talvez não diretamente, mas se você retornar um cursor (i.e. Posts.find({'author':'Tom'})) em uma função de publicação, isso é exatamente o que o Meteor está usando.

Quando o Meteor vê que a publicação somePosts retornou um cursor, ele chama _publishCursor() também – você adivinhou – publicando este cursor automaticamente.

Aqui o que o _publishCursor() faz:

  • Checa o nome da coleção no lado do servidor.
  • Puxa e encontra documentos a partir do cursor e o envia dentro de uma coleção no lado do cliente com o mesmo nome. (Usando .added() para fazer isso).
  • Sempre que um documento for adicionado, removido ou alterado, ele envia estas mudanças para a coleção no lado do cliente. (Ele usa .observe() no cursor e .added(), .changed() e .removed() para fazer isso).

No exemplo acima, nós somos capazes de assegurar que o usuário tenha somente os posts que estiver interessado (os escritos por Tom) disponíveis em cache no lado do cliente.

Publicando Propriedades Parciais

Nós vimos como publicar somente alguns de nossos posts, mas nós podemos continuar cortando mais coisas! Vamos ver como publicar somente propriedades específicas.

Como anteriormente, vamos usar find() para retornar um cursor, mas dessa vez vamos excluir alguns campos:

Meteor.publish('allPosts', function(){
  return Posts.find({}, {fields: {
    date: false
  }});
});
Publicando propriedades parciais
Publicando propriedades parciais

Claro, nós também podemos combinar ambas as técnicas. Por exemplo, se nós quisermos retornar todos os posts de Tom enquanto deixamos de lado suas datas, podemos escrever assim:

Meteor.publish('allPosts', function(){
  return Posts.find({'author':'Tom'}, {fields: {
    date: false
  }});
});

Resumindo

Nós vimos como publicar todas as nossas propriedades de todos os documentos e de todas as coleções (com autopublish) até como publicar algumas propriedades de alguns documentos de algumas coleções.

Isso cobre o básico do que você pode fazer com as publicações no Meteor, e estas simples técnicas devem ser suficientes para a vasta maioria dos casos.

As vezes, você vai precisar ir além combinando, linkando ou fundindo publicações. Nós vamos cobrir isso em outro capítulo!

Rotas

5

Agora que temos uma lista de artigos (que eventualmente poderão ser criados pelos utilizadores), precisamos de uma página individual para cada artigo onde os nossos utilizadores poderão discutir cada artigo.

Nós gostaríamos que essas páginas pudessem ser acessível através de um permalink, um URL no formato http://myapp.com/posts/xyz (onde xyz é um identificador _id de MongoDB) único para cada artigo.

Isto significa que vamos precisar de algum tipo de roteamento para ler o que está na barra de URL do navegador e mostrar o conteúdo correto.

Adicionando o Pacote Iron Router

Iron Router é um pacote de roteamento que foi criado especificamente para aplicações Meteor.

Este não só ajuda com roteamento (ou seja, especificar caminhos), como também permite tratar de filtros (associar ações a alguns dos caminhos) e ainda gerir subscrições (controlar que caminho tem acesso a que dados). (Nota: o Iron Router foi desenvolvido em parte pelo co-autor do Discover Meteor Tom Coleman.)

Primeiro, vamos instalar o pacote a partir da Atmosphere:

$ mrt add iron-router
Consola

Este comando baixa e instala o pacote iron-router na nossa aplicação, pronto a usar. Note que por vezes pode ser necessário reiniciar a sua aplicação Meteor (usando ctrl+c para matar o processo, e depois mrt para o iniciar novamente) antes de o pacote poder ser utilizado.

Note que o Iron Router é um pacote de terceiros, ou seja é necessário ter o Meteorite para o poder instalar (meteor add iron-router não vai funcionar).

Vocabulário de Roteador

Nós vamos falar de várias características diferentes do roteador neste capítulo. Se tiver alguma experiência com uma framework como Rails, você já estará familiarizado com a maioria desses conceitos. Caso contrário, segue-se um pequeno glossário:

  • Rotas: Uma rota é o bloco base do roteamento. Básicamente, é um conjunto de instruções que dizem à aplicação onde ir e o que fazer quando esta encontra um URL.
  • Caminhos: Um caminho é um URL dentro da sua aplicação. Este pode ser estático (/terms_of_service) ou dinâmico (/posts/xyz), e ainda incluir parâmetros de pesquisa (/search?keyword=meteor).
  • Segmentos: As diferentes partes de um caminho, delimitadas por barras para a frente (/).
  • Ganchos: Ganchos são ações que você gostaria de fazer antes, depois ou até durante o processo de roteamento. Um exemplo típico seria verificar se o utilizador tem os direitos necessários antes de mostrar uma página.
  • Filtros: Os filtros são simplesmente ganchos que pode definir globalmente para uma ou mais rotas.
  • Templates de Rota: Cada rota precisa de apontar para um template. Caso não especifique um, por omissão, o roteador vai procurar por um template com o mesmo nome da rota.
  • Layout: Pode pensar num layout como uma moldura digital de fotos. Eles contêm todo o código HTML que envolve o template atual, e vai permanecer o mesmo quando o template muda.
  • Controlador: Por vezes, você vai perceber que muitos dos seus templates estão a utilizar os mesmos parâmetros. Em vez de duplicar o seu código, é possível fazer com que todas essas rotas herdem de um único controlador de roteamento que irá conter toda a lógica de roteamento.

Para mais informação sobre o Iron Router, consulte a documentação completa no GitHub.

Roteamento: Mapear URLs para Templates

Até agora, nós construímos o nosso layout utilizando inclusões de templates hard-coded (tais como {{>postsList}}). Por isso, apesar de o conteúdo da nossa aplicação poder mudar, a estrutura base da página é sempre a mesma: um cabeçalho, com uma lista de artigos por baixo deste.

O Iron Router permite-nos outra abordagem ao ficar responsável pelo que é renderizado dentro da tag HTML <body>. Ou seja, nós não precisamos de definir o conteúdo dessa tag por nós próprios, como faríamos numa página HTML normal. Em vez disso, vamos apontar o roteador para um template layout especial que contem um ajudante de template {{yield}}.

O ajudante {{yield}} vai definir uma zona dinâmica especial que vai automaticamente renderizar o template correspondente à rota atual (como convenção, vamos designar, a partir de agora, este template especial como o “template da rota”):

Layouts e templates.
Layouts e templates.

Vamos começar por criar o nosso layout e adicionar o ajudante {{yield}}. Primeiro, vamos remover a nossa tag HTML <body> do main.html, e mover o seu conteúdo para o seu próprio template, layout.html.

O nosso, agora mais magro, main.html deve ser agora algo como:

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

Enquanto que o recém criado layout.html vai agora conter o layout mais exterior da aplicação:

<template name="layout">
  <div class="container">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="brand" href="/">Microscope</a>
    </div>
  </header>
  <div id="main" class="row-fluid">
    {{yield}}
  </div>
  </div>
</template>
client/views/application/layout.html

Note que substituímos a inclusão do template postsList com uma chamada ao ajudante yield. Repare que depois desta alteração, não vemos nada no ecrã. Isto é porque ainda não dissemos ao roteador o que fazer com o URL /, e neste caso é mostrado um template vazio.

Para começar, nós podemos recuperar o comportamento antigo mapeando o URL raiz / para o template postsList . Vamos criar uma diretoria /lib na raiz do nosso projecto, e dentro desta criar router.js :

Router.configure({
  layoutTemplate: 'layout'
});

Router.map(function() {
  this.route('postsList', {path: '/'});
});
lib/router.js

Fizemos duas coisas importantes. Primeiro, dissemos ao roteador para usar o layout que acabámos de criar como o layout por omissão para todas as rotas. Segundo, definimos uma nova rota chamada postsList e mapeámo-la para o caminho /.

A diretoria /lib

Qualquer coisa que seja colocada dentro da diretoria /lib é garantidamente carregado antes que qualquer outra coisa na sua aplicação (com a possível excepção de pacotes inteligentes). Isto faz com que seja um óptimo lugar para código de ajudantes que precisa de estar sempre disponível.

Um aviso: note que como a diretoria /lib não está nem dentro de /client nem de /server, isto significa que os seus conteúdos estarão disponíveis em ambos os ambientes.

Rotas com Nome

Vamos esclarecer alguma da ambiguidade. Chamámos à nossa rota postsList, mas também temos um template chamado postsList. O que é que se está aqui a passar?

Por omissão, o Iron Roter vai procurar por um template com o mesmo nome da rota. Na realidade, ele vai até procurar por um caminho baseado no nome da rota, ou seja, se não tivéssemos definido um caminho personalizado (que fizemos ao providenciar uma opção path na nossa definição de roteador), o nosso template não estaria acessível por omissão no URL postsList.

Outra dúvida possível é porque é que precisamos sequer de dar um note às nossas rotas. Dar nomes a rotas permite-nos utilizar algumas características do Iron Router que tornam mais fácil criar links dentro da nossa aplicação. O mais útil é o ajudante Handlebars {{pathFor}}, que devolve o caminho URL de qualquer rota.

Queremos que o nosso link principal de casa aponte para a lista de artigos, por isso em vez de especificar um URL estático /, nos podemos também utilizar o ajudante Handlebars. O resultado final será o mesmo, mas esta abordagem dá-nos mais flexibilidade dado que o ajudante irá sempre fazer output do URL correto mesmo que o caminho no roteador seja alterado.

<header class="navbar">
  <div class="navbar-inner">
    <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
  </div>
</header>

//...
client/views/application/layout.html

Commit 5-1

Roteamento muito básico.

Esperando por Dados

Caso publique a versão atual da aplicação (ou lance uma instância utilizando o link acima), irá notar que a lista aparece vazia por uns momentos antes dos artigos aparecerem. Isto é porque quando a página primeiro carrega, não existem artigos para mostrar enquanto a subscrição dos posts não carrega os dados dos artigos do servidor.

Seria muito melhor para a experiência de utilização se pudéssemos disponibilizar algum feedback visual que algo está a acontecer, e que o utilizar deve esperar alguns momentos.

Por sorte, o Iron Router tem uma forma fácil de fazer isso – vamos waitOn (esperar pela) subscrição:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.map(function() {
  this.route('postsList', {path: '/'});
});
lib/router.js

Vamos por partes. Primeiro, modificámos o bloco Router.configure() para dar ao roteador o nome do template de carregando (que vamos criar a seguir) para onde se deve redirecionar enquanto esperamos por dados.

Segunda, também adicionamos uma função waitOn, que devolve a nossa subscrição posts. O que isto significa é que o roteador vai garantir que a subscrição posts está carregada antes de mandar o utilizador pela rota que ele pediu.

Note que como estamos a definir a nossa função de waitOn globalmente ao nível da rota, esta sequência apenas vai acontecer uma vez quando o utilizador acede à sua aplicação pela primeira vez. Depois disso, os dados vão estar na memória do navegador e a rota não vai precisar de esperar por eles novamente.

E como nós estamos a deixar o roteador gerir a nossa subscrição, você pode agora removê-la com segurança do main.js (que deve agora estar vazio).

Normalmente é uma boa ideia esperar pelas suas subscrições, não apenas pela experiência de utilização, mas também porque isto significa que se pode assumir com segurança que os dados vão estar disponíveis nos templates. Isto elimina a necessidade de lidar com templates serem renderizados antes de os dados subjacentes estarem disponíveis, coisa que normalmente requer abordagens não ideais.

A peça final do puzzle é o template de carregamento. Vamos usar o pacote spin para criar um template de carregamento animado engraçado. Este pode ser adicionado com o comando mrt add spin, e depois crie o template loading da seguinte forma:

<template name="loading">
  {{>spinner}}
</template>
client/views/includes/loading.html

Note que {{>spinner}} é um template parcial contido no pacote spin. Apesar de este template parcial vir de “fora” da nossa aplicação, nós podemos utilizá-lo como qualquer outro template.

Commit 5-2

Esperar na subscrição do artigo.

Um Primeiro Olhar Sobre A Reatividade

Reatividade é um conceito chave de Meteor, e apesar de ainda não termos realmente abordado o tópico, o nosso template de carregamento dá-nos um primeiro olhar sobre este conceito.

Redirecionar para um template de carregamento se os dados ainda não foram carregados está muito bem, mas como é que o roteador sabe quando deve redirecionar o utilizador de volta para a página correta assim que os dados foram carregados?

Por agora, vamos apenas dizer que isto é exatamente onde a reatividade entra, e ficar por aqui. Mas não se preocupe, vai aprender mais sobre o assunto brevemente!

Roteando Para Um Post Específico

Agora que vimos como rotear para o template postsList, vamos criar uma rota para mostrar os detalhes de um único artigo.

Existe apenas um problema: nós não podemos prosseguir e definir uma rota por artigo, já que pode haver centenas deles. Neste caso é necessário definir uma única rota dinâmica, e fazer essa rota mostrar qualquer artigo que queiramos.

Para começar, vamos criar um novo template que simplesmente renderiza o mesmo template de artigo que utilizámos anteriormente na lista de artigos.

<template name="postPage">
  {{> postItem}}
</template>
client/views/posts/post_page.html

Posteriormente vamos adicionar mais elementos a este template (como por exemplo comentários), mas por agora este vai servir simplesmente como um contentor para a nossa inclusão {{> postItem}}.

Vamos criar outra rota com nome, desta vez mapeando caminhos URL na forma /posts/<ID> para o template postPage:

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id'
  });
});

lib/router.js

A sintaxe especial :_id indica ao roteador duas coisas: primeiro, para combinar qualquer rota na forma /posts/xyz/, onde “xyz” pode ser qualquer coisa. Segundo, para por seja o que for que encontrar neste “xyz” dentro de uma propriedade _id na lista params do roteador.

Note que estamos apenas a usar _id por uma questão de conveniencia. O roteador não tem forma de saber se você lhe está a passar realmente um _id, ou uma string aleatória de caracteres.

Estamos agora a rotear para o template correto, mas ainda nos falta alguma coisa: o roteador sabe o _id do artigo que queremos mostrar, mas o template não faz ideia de qual é. Então, como é que damos a volta a este problema?

Felizmente, o roteador tem uma solução inteligente: este deixa especificar o contexto de dados do template. Pode-se pensar no contexto de dados como o recheio dentro de um bolo delicioso feito de templates e layouts. De forma simples, é o que se usa para encher o template:

O contexto de dados.
O contexto de dados.

No nosso caso, podemos ir buscar o contexto de dados adequado procurando pelo artigo usando o _id que obtivemos do URL:

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });
});

lib/router.js

Assim, cada vez que o utilizador acede a esta rota, ele vai encontrar o artigo correspondente e vai passá-lo ao template. Lembre-se que findOne devolve um único artigo que corresponde a uma pesquisa, e que providenciar apenas um _id como argumento é um atalho para {_id: id}.

Dentro da função data (dados em português) para uma rota, this corresponde à rota atual, e podemos utilizar this.params para aceder às partes com nome da rota (partes que indicámos prefixando-as com : dentro da nossa path).

Mais Sobre Contexto de Dados

Ao definir o contexto de dados de um template, pode controlar o valor de this dentro de um ajudante de template.

Isto é normalmente feito implicitamente com o iterador {{#each}}, que automaticamente define o contexo de dados de cada iteração para o item atual da interação:

{{#each widgets}}
  {{> widgetItem}}
{{/each}}

Mas também o podemos fazer explicitamente utilizando {{#with}}, que simplesmente diz “toma este objecto, e aplica-lhe este template”. Por exemplo, podemos escrever:

{{#with myWidget}}
  {{> widgetPage}}
{{/with}}

Também é possível obter o mesmo efeito passando o contexto como um argumento da chamada ao template. Ou seja, o bloco anterior de código pode ser reescrito como:

{{> widgetPage myWidget}}

Utilizando Um Ajudante de Roteador Com Nome Dinâmico

Por fim, temos de ter a certeza que estamos a apontar para o lugar certo quando queremos fazer um link para um artigo individual. Novamente, podemos usar algo como <a href="/posts/{{_id}}">, mas usar um ajudante de roteador é mais seguro.

Chamámos a rota do artigo postPage, por isso podemos usar um ajudante {{pathFor 'postPage'}}:

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

Commit 5-3

Rotear para uma única página de artigo.

Mas espera, como exatamente é que o roteador sabe onde ir buscar a parte do xyz em /posts/xyz? No final de contas, não lhe estamos a passar nenhum _id.

Na realidade, o Iron Router é suficientemente inteligente para perceber isso por si próprio. Nós estamos a dizer ao roteador para usar a rota postPage, e o roteador sabe que esta rota precisa de um _id de alguma espécie (dado que foi assim que definimos a nossa path) .

Assim, o roteador vai procurar por este _id no local mais lógico disponível: o contexto de dados do ajudante {{pathFor 'postPage'}}, ou notras palavras this. E acontece que o nosso this corresponde a um artigo, que (surpresa!) tem uma propriedade _id.

Alternativamente, também é possível dizer explicitamente ao roteador onde é que ele deve procurar pela propriedade _id, passando um segundo argumento ao ajudante (ou seja, {{pathFor 'postPage' someOtherPost}}). Um possível cenário onde este padrão poderá ser usado é ao construir o link para o artigo anterior ou seguinte numa lista.

Para verificar se funciona corretamente, navegue para a lista de artigos e clique num dos links Discuss. Deve ver algo como:

Uma única página de artigo.
Uma única página de artigo.

HTML5 pushState

Uma coisa a perceber é que estas mudanças de URL estão a ser feitas utilizando HTML5 pushState.

O Roteador apanha cliques em URLs que são internos ao site, e evita que o navegador navegue para fora da aplicação, e em vez disso faz as alterações necessárias ao estado da aplicação.

Se tudo funcionar corretamente, a página deve mudar de forma instantânea. De facto, às vezes as coisas mudam tão rápido que pode ser necessária uma transição de página. Isto está fora do ambito deste capítulo, mas não deixa de ser um tópico interessante.

A Sessão

Sidebar 5.5

Meteor é um framework reativo. O que significa é que quando os dados são alterados, coisas em sua aplicação mudam sem você ter que explicitamente fazer alguma coisa.

Nós já vimos isso em ação e como nossos templates mudam quando os dados e as rotas mudam.

Vamos mergulhar fundo e saber como isso funciona nos próximos capítulos, mas por agora, gostaríamos de introduzir algumas propriedades básicas da reatividade que são extremamente úteis para maioria das aplicações.

A Meteor “Session”

Agora no Microscope, o estado atual da aplicação do usuário está completamente contido na URL que ele está procurando (e o banco de dados).

Mas em vários casos, você vai precisar armazenar algum estado efêmero que só é relevante a versão atual do usuário da aplicação (por exemplo, se um elemento está exposto ou escondido). A Session é uma forma conveniente de se fazer isso.

A Session é um dado reativo global armazenado. Ele é global no sentido de um objeto singleton global: há uma sessão, e ela é acessível em todo lugar. Variáveis globais geralmente são vistas como coisas ruins, mas neste caso a sessão é usada como um veículo central de comunicação para diferentes partes da aplicação.

Mudando a Session

Esta “sessão” está disponível como Session. Para configurar um valor de sessão, você pode chamar:

 Session.set('pageTitle', 'A different title');
Browser console

Você pode ler o dado de volta com Session.get('mySessionProperty');. Isso é uma fonte reativa de dados, que significa que se você colocá-la em um helper, você verá a saída do helper mudando reativamente quando a variável Session for alterada.

Para testar isso, adiocione o seguinte código no modelo do layout.

<header class="navbar">
  <div class="navbar-inner">
    <a class="brand" href="{{pathFor 'postsList'}}">{{pageTitle}}</a>
  </div>
</header>
client/views/application/layout.html
Template.layout.helpers({
  pageTitle: function() { return Session.get('pageTitle'); }
});
client/views/application/layout.js

A atualização automatica do Meteor (conhecida como “hot code reload” HCR) conserva as varáveis Session, então podemos agora ver “A different title” na barra de navegação. Se não, apenas digite o comando Session.set() novamente.

Além disso se nós mudarmos o valor mais uma vez (novamente no console do browser), nós vamos ver outro título sendo mostrado:

 Session.set('pageTitle', 'A brand new title');
Browser console

Session está disponível globalmente, então estas mudanças podem ser feitas em qualquer lugar da aplicação. Isso nos dá muio poder, mas pode também ser uma armadilha se usado exageradamente.

Mudanças Idênticas

Se você modificar uma variável Session com Session.set() mas configurá-la com um valor idêntico, o Meteor é inteligente o suficiente para desviar a cadeia reativa, e evitar a chamada do método.

Introduzindo o Autorun

Vimos um exmplo de fonte de dados reativa, e também vimos isso em ação dentro de um template helper (modelo auxiliar). Mas enquanto alguns contextos no Meteor (como os template helpers) são inerentemente reativos, a maioria do nosso código Meteor continua sendo o bom e velho JavaScript não-reativo.

Vamos supor que temos o seguinte trecho de código em algum lugar de nosso aplicativo:

helloWorld = function() {
  alert(Session.get('message'));
}

Mesmo que estejamos chamando uma variável Session, o contexto em que a chamada é feita não é reativo, significando que nós não vamos ter um novo alert toda as vezes que mudarmos a variável.

Ai é onde o Autorun entra. Como o nome implica, o código dentro de um bloco autorun vai rodar automaticamente e continuar rodando toda vez que a fonte de dados reativa usada mudar.

Tente digitar isso dentro do console do navegador:

 Deps.autorun( function() { console.log('Value is: ' + Session.get('pageTitle')); } );
Value is: A brand new title
Console do navegador

Como se poderia esperar, o bloco de código fornecido dentro de autorun roda uma vez, retornando esse dado para o console. Agora, vamos tentar mudar o título:

 Session.set('pageTitle', 'Yet another value');
Value is: Yet another value
Console do navegador

Mágica! Como o valor da sessão mudou, o autorun sabia que tinha que rodar este conteúdo todo novamente, re-imprimindo o novo valor no console.

Agora voltando ao exemplo anterior, se nós queremos disparar um novo alerta toda as vezes que nossa variável Session for alterada, tudo que temos que fazer é envolver nosso código em um bloco autorun:

Deps.autorun(function() {
  alert(Session.get('message'));
});

Como acabamos de ver, autoruns pode ser muito útil para rastrear fontes de dados e reagir imperativamente com elas.

Hot Code Reload

Durante nosso desenvolvimento do Microscope, temos tirado proveito de uma das propriedades do Meteor que salvam tempo: Hot Code Reload (HCR). Sempre que salvamos um de nossos arquivos de códigos, o Meteor detecta a mudança e reinicia o servidor, informando a cada cliente do recarregamento da página.

Isso é similar a uma atualização automática da página, mas com uma importante diferença.

Para saber qual é essa diferença, comece reconfigurando a variável Session que temos usado:

 Session.set('pageTitle', 'A brand new title');
 Session.get('pageTitle');
'A brand new title'
Browser console

Se nós recarregarmos nossa janela do navegador manualmente, nossa variável Session naturalmente vai ser perdida (uma vez que seria criada uma nova sessão). Do outro lado, se nós dispararmos um hor code reload (por exemplo, salvando um de nossos arquivos de código) a página vai ser recarregada, mas a variável session vai continuar configurada. Tente agora!

 Session.get('pageTitle');
'A brand new title'
Browser console

Então se estivermos usando variáveis de sessão para rastrear exatamente o que o usuário está fazendo, o HCR deve ser transparente para o usuário, pois isso vai preservar o valor de todas as variáveis de sessão. Isso nos permite fazer o deploy de novas versões em produção de nossas aplicações Meteor com a confiança de que nossos usuários vão ser minimamente interrompidos.

Considere isso por um instante. Se nós podemos manter todo o nosso estado na URL e na sessão, nós podemos transparentemente mudar o código fonte rodando por baixo de cada aplicação do cliente com uma interrupção mínima.

Vamos checar o que acontece quando nós atualizamos a página manualmente:

 Session.get('pageTitle');
null
Browser console

Quando nós recarregamos a página, nós perdemos a sessão. Com o HCR, o Meteor salva a sessão em local storage no seu navegador e a carrega novamente depois do recarregamento. Entretanto, o comportamente alternativo no recarregamento explícito faz sentido: se um usuário recarrega a página, é como se ele tivesse ido para a mesma URL novamente, e ele deseja resetar ao ponto inicial que qualquer usuário vai ver quando visita a URL.

As importantes lições em tudo que foi visto são:

  1. Sempre armazene esados de usuário na Session ou na URL, assim os usuários serão minimamente interrompidos quando um HCR acontecer.
  2. Armazene qualquer estado que você queira que seja compartilhável entre usuários dentro da própria URL.

Adicionando Usuários

6

Até então, nós conseguimos criar e mostrar algumas informações demonstrativas e estáticas de forma sensata e conectar tudo num simples protótipo.

Nós até vimos que a nossa UI é responsiva a mudanças nas informações, e informação inserida ou modificada aparece imediatamente. Porém, nosso site está paralisado pelo fato de que nós não podemos enviar informação. Na real, nós ainda nem temos usuários!

Vamos ver como nós podemos consertar isso.

Contas: usuários de forma simples

Na maioria dos web frameworks, adicionar contas de usuário é uma questão familiar. Claro, você tem quer fazê-lo quase em cada projeto, mas não é tão fácil quanto poderia ser. Pior ainda, assim que você tem que lidar com OAuth e outros esquemas de autenticações de terceiros, as coisas tendem a ficar feias rápido.

Por sorte, Meteor te dá cobertura. Graças ao modo como os pacotes do Meteor podem contribuir com código tanto no servidor (JavaScript) quanto no cliente (JavaScript, HTML, e CSS), nós podemos ter um sistema de contas quase por nada.

Nós poderíamos apenas usar a UI nativa do Meteor para contas (com mrt add accounts-ui) mas já que nós construímos todo nosso app com Bootstrap, nós iremos usar o pacote accounts-ui-bootstrap-dropdown no lugar (não se preocupe, a única diferença é na estilização). Na linha de comando, nós digitamos:

$ mrt add accounts-ui-bootstrap-dropdown
$ mrt add accounts-password
Terminal

Esses dois comandos fazem os templates especiais de conta disponíveis para nós; nós podemos incluí-los no nosso site usando o ajudante {{loginButtons}}. Uma dica útil: você pode controlar em que lado o seu log-in dropdown aparece usando o atributo align (por exemplo: {{loginButtons align="right"}}).

Nós adicionaremos esses botões ao nosso cabeçalho (header). E já que esse cabeçalho está começando a ficar grande, vamos dar mais espaço a ele com um template próprio (nós o incluíremos em client/views/includes). Nós também usaremos alguma marcação extra e classes do Bootstrap para garantir que tudo pareça ótimo:

<template name="layout">
  <div class="container">
    {{>header}}
    <div id="main" class="row-fluid">
      {{yield}}
    </div>
  </div>
</template>
client/views/application/layout.html
<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

Agora, quando nós navegamos para o nosso aplicativo, nós vemos os botões de login das contas no canto direito do nosso site.

Meteor's built-in accounts UI
Meteor’s built-in accounts UI

Nós podemos usá-los para cadastrar, logar, requerir mudança de senha, e todo resto que um site simples precisa para contas baseadas em senha.

Para informar ao nosso sistema de contas que nós queremos que os usuários façam log-in via um username, nós simplesmente adicionamos um bloco de configuração Accounts.ui num novo arquivo config.js dentro de client/helpers:

Accounts.ui.config({
  passwordSignupFields: 'USERNAME_ONLY'
});
client/helpers/config.js

Commit 6-1

Added accounts and added template to the header

Criando Nosso Primeiro Usuário

Vá em frente e cadastre-se com uma conta: o botão “Sign in” mudará para mostrar o seu username. Isto confirma que uma conta de usuário foi criada para você. Mas de onde a informação de conta de usário está vindo?

Ao adicionar o pacote contas, o Meteor criou uma nova coleção especial, a qual pode ser acessada com Meteor.users. Para vê-la, abra o console do seu navegador e digite:

 Meteor.users.findOne();
Browser console

O console deverá retornar um objeto representando o seu objeto usuário; se você der uma olhada, você pode ver que seu username está lá, assim como uma _id que unicamente identifica você. Note que você também pode chamar o atual usuário logado com Meteor.user().

Agora log out e cadastre-se com um novo username. Meteor.user() deve retornar agora um segundo usuário. Mas espere, vamos rodar:

 Meteor.users.find().count();
1
Browser console

O console retorna 1. Espere, não deveria ser 2? O primeiro usuário foi deletado? Se você tentar logar com o primeiro usuário de novo, você verá que este não é o caso.

Vamos ter certeza e checar o armazenamento de informação canônico, o banco de dados Mongo. Nós logaremos no Mongo (meteor mongo no seu terminal) e checaremos:

> db.users.count()
2
Mongo console

Há definitivamente dois usuários. Então por que nós conseguimos ver apenas um por vez no navegador?

Um Mistério da Publicação!

Se você pensar de novo no Capítulo 4, talvez você se lembre que ao desativiar o autopublish, nós paramos as coleções de automaticamente enviar toda informação do servidor para cada versão local da coleção dos clientes. Nós precisamos criar um par de publicação e assinatura para conectar a informação.

Ainda não configuramos nenhum tipo de publicação de usuário. Então como podemos ver informação de qualquer usuário que seja?

A resposta é que o pacote accounts de fato “auto-publica” as informaçãos básicas de conta do atual usuário logado sem se importar com o resto. Se não o fizesse, então esse usuário nunca conseguiria logar no site!

O pacote accounts apenas publica o usuário atual aliás. Isto explica porque um usuário não pode ver os detalhes da conta de outro.

Então a publicação está apenas publicando um objeto usuário por usuário logado (e nenhum quando você não está logado).

Ainda mais, documentos na nossa coleção usuários não parecem conter os mesmos campos no servidor e no cliente. Em Mongo, um usuário tem um monte de informação nele. Para vê-la, apenas vá de volta ao terminal Mongo e digite:

> db.users.findOne()
{
    "createdAt" : 1365649830922,
    "_id" : "kYdBd9hr3fWPGPcii",
    "services" : {
        "password" : {
            "srp" : {
                "identity" : "qyFCnw4MmRbmGyBdN",
                "salt" : "YcBjRa7ArXn5tdCdE",
                "verifier" : "df2c001edadf4e475e703fa8cd093abd4b63afccbca48fad1d2a0986ff2bcfba920d3f122d358c4af0c287f8eaf9690a2c7e376d701ab2fe1acd53a5bc3e843905d5dcaf2f1c47c25bf5dd87764d1f58c8c01e4539872a9765d2b27c700dcdedadf5ac82521467356d3f91dbeaf9848158987c6d359c5423e6b9cabf34fa0b45"
            }
        },
        "resume" : {
            "loginTokens" : [
                {
                    "token" : "BMHipQqjfLoPz7gru",
                    "when" : 1365649830922
                }
            ]
        }
    },
    "username" : "tmeasday"
}
Mongo console

Por outro lado, no navegador o objeto usuário é muito reduzido, como você pode ver ao digitar o comando equivalente:

 Meteor.users.findOne();
Object {_id: "kYdBd9hr3fWPGPcii", username: "tmeasday"}
Browser console

Este examplo nos mostra como uma coleção local pode ser um subconjunto seguro do verdadeiro banco de dados. O usuário logado vê apenas o suficiente do conjunto de dados para funcionar (neste caso, logar). Este é um padrão útil a se aprender, como você verá mais adiante.

Isso não significa que você não possa tornar pública mais informação sobre o usuário se você quiser. Você pode checar o Meteor docs para ver como opcionalmente publicar mais campos na coleção Meteor.users.

Reatividade

Sidebar 6.5

Se as coleções são as ferramentas centrais do Meteor, então reatividade é a crosta que faz o centro útil.

Coleções transformam radicalmente a forma como seu aplicativo lida com mudanças na informação. Ao invés de ter de checar por mudanças na informação manualmente (vulgo através de chamadas AJAX) e então costurar essas mudanças no HTML, mudanças na informação podem então vir a qualquer momento e serem aplicadas à interface do usuário sem problemas.

Tome um momento para pensar nisso direito: por trás das cortinas, o Meteor é capaz de mudar qualquer parte da interface do usuário quando uma coleção subjacente é atualizada.

A forma imperativa para se fazer isso seria usar .observe(), uma função cursor que dispara callbacks quando documentos correspondentes ao cursor mudam. Nós então poderíamos fazer mudanças ao DOM (o HTML renderizado da nossa página web) através desses callbacks. O código resultante se pareceria com algo assim:

Posts.find().observe({
  added: function(post) {
    // when 'added' callback fires, add HTML element
    $('ul').append('<li id="' + post._id + '">' + post.title + '</li>');
  },
  changed: function(post) {
    // when 'changed' callback fires, modify HTML element's text
    $('ul li#' + post._id).text(post.title);
  },
  removed: function(post) {
    // when 'removed' callback fires, remove HTML element
    $('ul li#' + post._id).remove();
  }
});

Você já pode provavelmente perceber como tal código ficará complexo bem rapidamente. Imagine lidar com mudanças em cada atributo da postagem, e ter de fazer mudanças complexas ao HTML dentro dos <li> da postagem. Sem mencionar em todos os casos fronteiriços que podem surgir quando nós começamos a precisar de múltiplas fontes de informação que todas podem mudar em tempo real.

Quando devemos Usar observe()?

Usar o padrão acima é às vezes necessário, especialmente quando se lida com widgets de terceiros. Por exemplo, vamos imaginar que nós queremos adicionar e remover alfinetes de um mapa em tempo real baseado em informação de uma Coleção (digamos, para mostrar as localizações dos usuários logados agora).

Em tais casos, você precisará fazer callbacks do observe() para fazer o mapa “conversar” com a coleção do Meteor e como reagir às mudanças da informação. Por exemplo, você precisaria dos callbacks added e removed para chamar os métodos dropPin() ou removePin() da API do mapa.

Um approach declarativo

O Meteor provê a nós uma forma melhor: reatividade, a qual é em sumo um approach declarativo. Sendo declarativo ele nos permite definir a relação entre objetos uma vez e saber que eles permaneceram em sincronia, ao invés de ter de especificar comportamentos para cada possível mudança.

Este é um conceito poderoso, porque um sistema em tempo real tem muitos inputs que podem todos mudar em momentos imprevisíveis. Ao afirmar declarativamente como nós renderizamos HTML baseado em qualquer fonte de informação reativa com que nos importamos, Meteor pode tomar conta do serviço de monitorar essas fontes e transparentemente cuidar do trabalho bagunçado de manter a interface do usuário atualizada.

Tudo isso para dizer que ao invés de pensar sobre callbacks do observe, o Meteor deixa a gente escrever:

<template name="postsList">
  <ul>
    {{#each posts}}
      <li>{{title}}</li>
    {{/each}}
  </ul>
</template>

E então pega nossa lista de postagens com:

Template.postsList.helpers({
  posts: function() {
    return Posts.find();
  }
});

Por trás das cortinas, o Meteor está armando callbacks do observe() para nós, e re-desenhando seguimentos relevantes de HTML quando a informação reativa muda.

Monitoramento de Dependências em Meteor: Computações

Apesar do Meteor ser um framework reativo, em tempo real, nem todo código dentro de um aplicativo em Meteor é reativo. Se este fosse o caso, todo seu aplicativo rodaria de novo cada vez que qualquer coisa mudasse. Então, a reatividade é limitada a áreas específicas do seu código, e nós podemos chamar essas áreas de computações.

Em outras palavras, a computação é um bloco de código que roda cada vez que uma das fontes de informação reativa na qual ela depende muda. Se você tem uma fonte de informação reativa (por exemplo, uma variável de Sessão) e gostaria de responder reativamente a ela, você precisará configurar uma computação para tanto.

Note que você geralmente não precisa fazer isso explicitamente porque o Meteor dá às renderizações de cada template sua própria computação (o que significa dizer que o código nos ajudantes de template e callbacks são reativos por padrão).

Cada fonte de informação reativa monitora todas as computações que a estão usando para que poder informá-las quando seu próprio valor mudar. Para tanto, ela chama a função invalidate() na computação.

Computações são geralmente configuradas a simplesmente re-avaliar seus conteúdos on invalidation, e isto é o que ocorre nas computações do template (apesar que as computações do template também fazem alguma mágica ao tentar renderizar a página o mais eficientemente). Apesar que você pode ter mais controle quanto ao que cada computação faz on invalidation se você precisar, na prático isto não será algo que vocé fará com freqüência.

Configurando uma Computação

Agora que nós entendemos a teoria por trás das computações, configurar uma de fato parecerá desproporcionalmente fácil. Nós simplesmente usamos a função Deps.autorun para envolver um bloco de código na computação e fazê-lo reativo:

Deps.autorun(function() {
  console.log('There are ' + Posts.find().count() + ' posts');
});

Por trás das cortinas, autorun cria uma computação, e a configura para re-avaliar quando as fontes de informação na qual ela depende mudam. Nós configuramos uma computação bem simples que simplesmente informa o número de postagens para o console. Já que Posts.find() é uma fonte de informação reativa, ela tomará conta de informar a computação para re-avaliar cada vez que o número de postagens mudar.

> Posts.insert({title: 'New Post'});
There are 4 posts.

O resultado em rede de tudo isso é que nós podemos escrever código que usa informação reativa de uma forma bem natural, sabendo que por trás das cortinas o sistema de dependência toma conta de rodar novamente bem na hora certa.

Criando Artigos

7

Nós vimos como é fácil criar artigos através do console, usando a chamada Posts.insert ao banco de dados, mas nós não podemos esperar que nossos usuários abram o console para criar um novo artigo.

Eventualmente, nós precisaremos construir alguma forma de interface do usuário para permitir aos nossos usuários postar novos artigos no nosso aplicativo.

Construindo uma Nova Página de Artigo

Nós começamos por definir a rota para a nossa nova página:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { return Meteor.subscribe('posts'); }
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});
lib/router.js

Nós estamos usando a função data do roteador para configurar um contexto de informação do template postPage. Lembre-se que o quer que ponhamos nesse contexto de informação estará disponível ao this de dentro dos ajudantes de template.

Adicionando um Link ao Cabeçalho

Com essa rota definida, nós podemos agora adicionar um link a nossa página de envio no nosso cabeçalho:

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li><a href="{{pathFor 'postSubmit'}}">New</a></li>
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

Configurar a nossa rota signifca que se o usuário procurar pela URL /submit, o Meteor mostrará o template postSubmit. Então vamos escrever esse template:

<template name="postSubmit">
  <form class="main">
    <div class="control-group">
        <label class="control-label" for="url">URL</label>
        <div class="controls">
            <input name="url" type="text" value="" placeholder="Your URL"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="title">Title</label>
        <div class="controls">
            <input name="title" type="text" value="" placeholder="Name your post"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="message">Message</label>
        <div class="controls">
            <textarea name="message" type="text" value=""/>
        </div>
    </div> 

    <div class="control-group">
        <div class="controls">
            <input type="submit" value="Submit" class="btn btn-primary"/>
        </div>
    </div>
  </form>
</template>

client/views/posts/post_submit.html

Note: isto é um monte de marcação, mas ela simplesmente advém de se usar o Twitter Bootstrap. Apesar de apenas os elementos de formulário serem essenciais, a marcação extra ajudará o nosso aplicativo a parecer um pouco melhor. Agora ele deverá se parecer com algo assim:

The post submit form
The post submit form

Este é um simples formulário. Nós não precisamos nos preocupar com a ação para ele, já que nós estaremos interceptando eventos de envio no formulário e atualizando a informação via JavaScript. (Não faz sentido prover um plano reserva sem ser em JS quando se considera que um aplicativo em Meteor é completamente não funcional quando o JavaScript está desativado).

Criando Artigos

Vamos ligar um manuseador de evento ao evento submit do formulário. É melhor usar o evento submit (ao invés de digamos um evento click no botão), já que ele cobrirá todas maneiras possíves de envio (tais como apertar enter no campo da URL por exemplo).

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val(),
      message: $(e.target).find('[name=message]').val()
    }

    post._id = Posts.insert(post);
    Router.go('postPage', post);
  }
});
client/views/posts/post_submit.js

Commit 7-1

Added a submit post page and linked to it in the header.

Esta função usa jQuery para analisar os valores dos vários campos do formulário, e povoar o novo objeto artigo com os resultados. Nós precisamos assegurar que nós preventDefault no argumento do event do nosso manuseador para garantir que o navegador não vá adiante e tente enviar o formulário.

Finalmente, nós podemos redirecionar a nossa nova página do artigo. A função insert() numa coleção retorna a id gerada para o objeto que foi inserido no banco de dados, a qual a função go() do Roteador usará para construir a URL para nós navegarmos até.

O resultado em rede é que o usuário aperta enviar, o artigo é criado, e o usuário é instantaneamente levado à página de discussão para este novo artigo.

Adicionando Alguma Segurança

Criar artigos está todo certo, mas nós não queremos permitir que visitantes aleatórios o façam: nós queremos que eles precisem estar logados para tanto. Claro, nós podemos começar escondendo dos usuários não logados o formulário de novo artigo. Mesmo assim, um usuário poderia concebivelmente criar um artigo no console do navegador sem estar logado, e nós não queremos isso.

Agradecidamente a segurança de informação está inserida diretamente nas coleções Meteor; a questão é que ela fica desligada por padrão quando você cria um novo projeto. Isso permite que você comece facilmente e construa seu aplicativo deixando a parte chata para mais tarde.

Nosso aplicativo não precisa mais de rodinhas, então vamos tirá-las! Nós removeremos o pacote insecure:

$ meteor remove insecure
Terminal

Após o fazê-lo, nocê notará que o formulário de artigo não funciona mais. Isto é porque sem o pacote insecure, inserções do lado do cliente na coleção posts não são mais permitidas. Nós precisamos ou dar regras explícitas ao Meteor sobre quando é OK para um cliente inserir artigos, ou fazer todas nossas inserções de artigo pelo lado do servidor.

Permitindo Inserções de Artigo

Para começar, nós mostraremos como permitir inserções de artigos pelo lado do cliente para deixar nosso formulário funcionando de novo. Como de costume, nós eventualmente terminaremos com uma técnica diferente, mas por hora, o seguinte fará as coisas funcionarem de novo:

Posts = new Meteor.Collection('posts');

Posts.allow({
  insert: function(userId, doc) {
    // only allow posting if you are logged in
    return !! userId;
  }
});
collections/posts.js

Commit 7-2

Removed insecure, and allowed certain writes to posts.

Nós chamamos Posts.allow, que diz ao Meteor “este é um conjunto de circunstâncias onde os clientes estão permitidos a fazer coisas à coleção Posts”. Neste caso, nós estamos dizendo “clientes têm permissão de inserir artigos desde que eles tenham uma userId”.

A userId do usuário fazendo a modificação é passada às chamadas allow and deny (ou retorna null se nenhum usuário estiver logado), o que é quase sempre útil. E como as contas de usuários são amarradas ao centro do Meteor, nós podemos contar com a userId estar sempre correta.

Nós conseguimos garantir que você precisará estar logado para criar um artigo. Tente deslogar e criar um artigo; você deve ver isto no console:

Insert failed: Access denied
Insert failed: Access denied

Entretanto, nós ainda temos de lidar com algumas questões:

  • Usuários não logados ainda conseguem alcançar o formulário de criação de artigo.
  • O artigo não está ligado ao usuário de forma alguma (e não há código no servidor que garanta isso).
  • Múltiplos artigos podem ser criados que apontem para a mesma URL.

Vamos consertar esses problemas.

Protegendo o Acesso ao Formulário de Novo Artigo

Vamos começar prevenindo usuários não logados de ver o formulário de envio de artigo. Nós faremos isso no nível do roteador, definindo um gancho de rota.

Um gancho intercepta um processo de roteamento e potencialmente muda a ação que o roteador toma. Você pode pensá-lo como um segurança que checa suas credenciais antes de permitir que você entre (ou te recusar).

O que nós precisamos fazer é checar se o usuário está logado, e se eles não estiverem renderizar o template accessDenied ao invés do esperado template postSubmit (nós então paramos o roteador de fazer qualquer outra coisa). Vamos modificar o router.js para tanto:

Router.configure({
  layoutTemplate: 'layout'
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});

var requireLogin = function() {
  if (! Meteor.user()) {
    this.render('accessDenied');
    this.stop();
  }
}

Router.before(requireLogin, {only: 'postSubmit'});
lib/router.js

Nós também criamos o template da página acesso negado:

<template name="accessDenied">
  <div class="alert alert-error">You can't get here! Please log in.</div>
</template>
client/views/includes/access_denied.html

Commit 7-3

Denied access to new posts page when not logged in.

Se você tentar ir a http://localhost:3000/submit/ sem estar logado, você deve ver isto:

The access denied template
The access denied template

A coisa legal quanto a ganchos de roteamento é que eles são reativos. Isso signfica que nós podemos ser declarativos e não precisamos pensar em callbacks, ou similares, quando o usuário loga. Quando o estado de log-in do usuário muda, a página de template do Roteador muda instantaneamente de accessDenied para postSubmit sem que nós precisemos escrever nenhum código explícito para tanto.

Logue, e então tente atualizar a página. Você pode ver às vezes o template negado piscar por um breve momento antes da página de envio de artigo aparecer. A razão para tanto é que o Meteor começa renderizando templates o mais cedo possível, antes de conversar com o servidor e checar se o usuário atualmente (armazenado no banco local do navegador) sequer existe.

Para evitar este problema (que é uma classe de problemas comum que você verá mais quando você lidar com os detalhes da latência entre cliente e servidor), nós apenas mostraremos uma tela de loading pelo breve momento em que nós esperamos para ver se o usuário tem acesso ou não.

Até mesmo porque neste estágio nós não sabemos se o usário tem as credenciais log-in corretas, e nós não podemos mostrar tanto accessDenied ou o template postSubmit até que saibamos.

Então nós modificamos no nosso gancho para usar o nosso template loading embora Meteor.loggingIn() seja verdadeiro:

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});

var requireLogin = function() {
  if (! Meteor.user()) {
    if (Meteor.loggingIn())
      this.render(this.loadingTemplate);
    else
      this.render('accessDenied');

    this.stop();
  }
}

Router.before(requireLogin, {only: 'postSubmit'});
lib/router.js

Commit 7-4

Show a loading screen while waiting to login.

Escondendo o Link

A maneira mais fácil de prevenir que os usuários cheguem a esta página por engano quando eles não estão logados é esconder o link deles. Nós podemos fazer isso bem facilmente:

<ul class="nav">
  {{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{/if}}
</ul>
client/views/includes/header.html

Commit 7-5

Only show submit post link if logged in.

O ajudante currentUser nos é provido pelo pacote accounts e é o handlebar equivalent de Meteor.user(). Já que é reativo, o link irá aparecer ou desaparecer ao você logar e deslogar do aplicativo.

Método Meteor: Abstração e Segurança Melhores

Nós conseguimos proteger o acesso à página de novo artigo de usuário não logados, e negar que tais usuários criem artigos mesmo que eles trapaceiem e usem o console. Porém ainda há mais algumas coisas que nós precisamos tomar conta:

  • Timestamping os artigos.
  • Assegurar que uma mesma URL não seja publicada mais que uma vez.
  • Adicionar detalhes sobre o autor do artigo (ID, username, etc.).

Você talvez esteja pensando que nós podemos fazer tudo isso no nosso manuseador de evento submit, entretanto, nós rapidamente encontraríamos uma série de problemas:

  • Para a timestamp, nós teríamos de contar com a corretude do tempo do computador do usuário, o qual nem sempre será o caso.
  • Clientes não saberam de todas as URLs já publicadas no site. Eles apenas saberão dos artigos que eles podem ver atualmente (nós veremos como isso funciona exatamente mais tarde), então não há como garantir originalidade de URL pelo lado do cliente.
  • Finalmente, apesar que nós poderíamos adicionar detalhes de usuário pelo lado do cliente, nós não poderíamos garantir sua acurácia, o que poderia abrir o nosso aplicativo para os usuários trapaceando pelo console do navegador.

Por todas essas razões, é melhor manter nossos manuseadores de evento simples e, se nós estivermos fazendo mais do que inserções e atualizações básicas à coleção, usemos um Método.

Um Método Meteor é uma função do lado do servidor que é chamada pelo lado do cliente. Nós temos alguma familiaridade com elas – aliás, por trás das cortinas, as funções insert, update e remove da Coleção são todos Métodos. Vamos ver como criar o nosso próprio.

Vamos voltar a post_submit.js. Ao invés de inserir diretamente na coleção Posts, nós chamaremos um Método chamado post:

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val(),
      message: $(e.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error)
        return alert(error.reason);

      Router.go('postPage', {_id: id});
    });
  }
});
client/views/posts/post_submit.js

A função Meteor.call chama um Método nomeado pelo seu primeiro argumento. Você pode prover argumentos à chamada (neste caso, o objeto post que nós construímos através do formulário), e finalmente anexar um callback, o qual executará quando o Método do lado do servidor estiver finalizado. Aqui nós simplesmente alertamos o usuário se houve algum problema, ou redirecionamos o usuário à recentemente criada página de discussão do artigo caso contrário.

Nós definimos o Método no nosso arquivo collections/posts.js. Nós removeremos o bloco allow() do posts.js já que o Método Meteor ignora eles mesmo. Lembre-se que Métodos são executados no servidor, então o Meteor assume que eles sejam confiáveis.

Posts = new Meteor.Collection('posts');

Meteor.methods({
  post: function(postAttributes) {
    var user = Meteor.user(),
      postWithSameLink = Posts.findOne({url: postAttributes.url});

    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to post new stories");

    // ensure the post has a title
    if (!postAttributes.title)
      throw new Meteor.Error(422, 'Please fill in a headline');

    // check that there are no previous posts with the same link
    if (postAttributes.url && postWithSameLink) {
      throw new Meteor.Error(302, 
        'This link has already been posted', 
        postWithSameLink._id);
    }

    // pick out the whitelisted keys
    var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
      userId: user._id, 
      author: user.username, 
      submitted: new Date().getTime()
    });

    var postId = Posts.insert(post);

    return postId;
  }
});
collections/posts.js

Commit 7-6

Use a method to submit the post.

Este Método é um pouco complicado, mas felizmente você pode seguir adiante.

Primeiro, nós definimos nossa variável user e checamos se o artigo com o mesmo link já existe. Então, nós checamos para ver se o usuário está logado, jogando um erro (o qual eventualmente será alert-ado pelo navegador) caso contrário. Nós também fazemos alguma validação simples do objeto artigo para garantir que os nossos artigos tenham título.

Em seguida, se há outro artigo com a mesma URL, nós podemos lançar um erro 302 (o qual significa redirecionar) dizendo ao usuário que nós deveríamos ir e ver este artigo prévio.

A classe Error do Meteor recebe três argumentos. O primeiro (error) será o código numérico 302, o segundo (reason) é uma pequena explicação legível por humanos do erro, e o último (details) pode ser qualquer informação útil adicional.

No nosso caso, nós usaremos este terceiro argumento para passar a ID do artigo que nós acabamos de encontrar. Spoiler alert: nós usaremos isso mais tarde para redirecionar o usuário para o artigo pré-existente.

Se todas essas checagens passarem, nós agarraremos os campos que nós queremos inserir (para garantir que o usuário que esteja chamando este Método não possa por informação falsa no nosso banco de dados), e incluiremos alguma informação sobre o usuário que envia – assim como o tempo atual – ao artigo.

Finalmente, nós inserimos o artigo, e retornamos a id do novo artigo ao usuário.

Organizando Artigos

Agora que nós temos data de envio em todos os nossos artigos, faz sentido assegurar que eles estejam organizados usando esse atributo. Para tanto, nós podemos usar o operador sort do Mongo, que espera um objeto feito de chaves pelas quais organizar, e um sinal indicando se de forma ascendente ou descendente.

Template.postsList.helpers({
  posts: function() {
    return Posts.find({}, {sort: {submitted: -1}});
  }
});
client/views/posts/posts_list.js

Commit 7-7

Sort posts by submitted timestamp.

Levou um pouco de trabalho, mas nós finalmente temos uma interface do usuário para deixar nossos usuários adicionar conteúdo seguramente ao nosso aplicativo!

Mas qualquer aplicativo que permite ao usuário criar conteúdo também precisa deixá-los editá-lo e deletá-lo. Sobre isso que se tratará o capítulo Editando Artigos.

Compensação de Latência

Sidebar 7.5

No último capítulo, nós introduzimos um novo conceito no mundo Meteor: Métodos.

Without latency compensation
Without latency compensation

Um Método Meteor é uma forma de executar uma série de comandos no servidor de uma forma estruturada. No nosso exemplo, nós usamos um Método porque nós queríamos ter certeza que os novos artigos estariam marcados com o nome do autor e id assim como o tempo atual do servidor.

Entretanto, se o Meteor executasse Métodos da forma mais básica, nós teríamos um problema. Considere a seguinte seqüência de eventos (note: as timestamps são valores aleatórios selecionados por razões apenas ilustrativas):

  • +0ms: O usuário clica no botão de enviar e o navegador dispara uma chamada a um Método.
  • +200ms: O servidor faz mudanças ao banco de dados Mongo.
  • +500ms: O cliente recebe essas mudanças, e atualiza a UI para refletí-las.

Se esta fosse a forma como o Meteor operasse, então haveria um pequeno lag entre efetuar tais ações e ver os resultados (esse lag sendo mais ou menos perceptível dependendo de quão próximo se está do servidor). Nós não podemos ter isso numa aplicativo web moderno!

Compensação de Latência

With latency compensation
With latency compensation

Para evitar problemas, o Meteor introduz um conceito chamado Compensação de Latência. Quando nós definimos nosso Método post, nós o colocamos dentro de um arquivo no diretório collections/. Isto significa que ele está disponível tanto no servidor quanto no cliente – e rodará em ambos ao mesmo tempo!

Quando você faz uma chamada a um Método, o cliente envia uma chamada ao servidor, mas também simultaneamente simula a ação do Método nas coleções do cliente. Então nosso fluxo de trabalho agora se torna:

  • +0ms: O usuário clica no botão enviar e o navegador dispara uma chamada ao Método.
  • +0ms: O cliente simula a ação da chamada ao Método nas coleções do cliente e muda a UI para refletí-las.
  • +200ms: O servidor faz mudanças ao banco de dados Mongo.
  • +500ms: O cliente recebe essas mudanças e desfaz suas mudanças simuladas, trocando-as pelas mudanças do servidor (as quais são geralmente as mesmas). As mundanças na UI refletirão isso.

Isto resulta no usuário ver mudanças instantaneamente. Quando a responda do servidor retorna alguns momentos mais tarde, pode ou não haver mudanças notáveis ao passo que os documentos canônicos do servidor chegam pela conexão. Algo a se aprender com isto é que nós devemos tentar ter certeza que nós simulamos os documentos reais o mais veridicamente.

Observando a Compensação de Latência

Nós podemos fazer uma pequena mudança à chamada ao Método post para ver isso em ação. Para tanto, nós faremos alguma programação avançada com o pacote npm futures para retardar a inserção de objetos no nosso Método.

Nós usaremos isSimulation para perguntar ao Meteor se o Método está sendo atualmente invocado como um stub. Um stub é uma simulação de Método que roda no cliente em paralelo, enquanto o Método “real” está rodando no servidor.

Então nós perguntamos ao Meteor se o código está sendo executado no cliente. Se sim, nós adicionamos a string (client) ao final do título do nosso post. Caso contrário, nós adicionamos a string (server):

Meteor.methods({
  post: function(postAttributes) {
    // […]

    // pick out the whitelisted keys
    var post = _.extend(_.pick(postAttributes, 'url', 'message'), {
      title: postAttributes.title + (this.isSimulation ? '(client)' : '(server)'),
      userId: user._id, 
      author: user.username, 
      submitted: new Date().getTime()
    });

    // wait for 5 seconds
    if (! this.isSimulation) {
      var Future = Npm.require('fibers/future');
      var future = new Future();
      Meteor.setTimeout(function() {
        future.return();
      }, 5 * 1000);
      future.wait();
    }

    var postId = Posts.insert(post);

    return postId;
  }
});
collections/posts.js

Note: caso você esteja se perguntando, o this em this.isSimulation é um objeto de invocação de Método que provê acesso a várias variáveis úteis.

Como exatamente Futures funciona está fora do escopo deste livro, mas nós basicamente dissemos ao Meteor para esperar 5 segunos antes de tentar inserir na nossa coleção do servidor.

Nós também garantiremos que o envio redirecione diretamente para a list de artigos:

Template.postSubmit.events({
  'submit form': function(event) {
    event.preventDefault();

    var post = {
      url: $(event.target).find('[name=url]').val(),
      title: $(event.target).find('[name=title]').val(),
      message: $(event.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error)
        return alert(error.reason);
    });
    Router.go('postsList');
  }
});
client/views/posts/post_submit.js

Commit 7-5-1

Demonstrate the order that posts appear using a sleep.

Se nós criamos um artigo agora, nós vemos a compensação de latência claramente. Primeiro, um artigo é inserido como (cliente) em seu título (o primeiro artigo da lista, linkando para o GitHub):

Our post as first stored in the client collection
Our post as first stored in the client collection

Então, cinco segundos mais tarde, ele é claramente substituído pelo documento real que foi inserido pelo servidor:

Our post once the client receives the update from the server collection
Our post once the client receives the update from the server collection

Método na Coleção do Cliente

Você pode pensar que Métodos são complicados depois disso, mas na real eles podem ser bem simples. Nós já vimos três Métodos bem simples: os Métodos de mutação de Coleção, insert, update e remove.

Quando você define uma coleção no servidor chamada 'posts', você está implicitamente definindo três Métodos: posts/insert, posts/update e posts/delete. Em outras palavras, quando você chama Posts.insert() na coleção do cliente, você está chamando um Método de latência compensada que faz duas coisas:

  1. Checa para ver se nós podemos fazer a mutação chamando as callbacks allow e deny (entretanto isto não precisa ocorrer na simulação).
  2. De fato opera a modificação ao armazenamento de informação subjacente.

Métodos chamando Métodos

Se você está acompanhando, você pode ter percebido que o nosso Método post está chamando outro Método (posts/insert) quando nós inserimos nosso post. Como isso funciona?

Quando a simulação (versão do Método do lado do cliente) está rodando, nós rodamos a simulação do insert (então nós inserimos na nossa coleção do cliente), mas nós não chamamos o real, insert do lado do servidor, já que nós esperamos que a versão do lado do servidor do post fará isso.

Consequentemente, quando o Método post do lado do servidor chama insert não há necessidade de se preocupar quanto à simulação, e a inserção ocorre suavemente.

Editando Artigos

8

Agora que já conseguimos criar artigos, o próximo passo é conseguir edita-los e remove-los. Dado que o código de UI desta funcionalidade é bastante simples, esta é uma boa altura para falar sobre como Meteor gere permissões de utilizadores.

Vamos primeiro configurar o nosso roteador. Primeiro vamos adicionar uma rota para aceder à página de edição do artigo e vamos definir o seu contexto de dados:

Router.configure({
  layoutTemplate: 'layout'
});

Router.map(function() {
  this.route('postsList', {path: '/'});

  this.route('postPage', {
    path: '/posts/:_id',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postEdit', {
    path: '/posts/:_id/edit',
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postSubmit', {
    path: '/submit'
  });
});

var requireLogin = function() {
  if (! Meteor.user()) {
    if (Meteor.loggingIn())
      this.render('loading')
    else
      this.render('accessDenied');

    this.stop();
  }
}

Router.before(requireLogin, {only: 'postSubmit'});
lib/router.js

O Template De Edição de Artigos

Podemo-nos focar agora no template. O nosso template postEdit vai ser um formulário standard:

<template name="postEdit">
  <form class="main">
    <div class="control-group">
        <label class="control-label" for="url">URL</label>
        <div class="controls">
            <input name="url" type="text" value="{{url}}" placeholder="Your URL"/>
        </div>
    </div>

    <div class="control-group">
        <label class="control-label" for="title">Title</label>
        <div class="controls">
            <input name="title" type="text" value="{{title}}" placeholder="Name your post"/>
        </div>
    </div>

    <div class="control-group">
        <div class="controls">
            <input type="submit" value="Submit" class="btn btn-primary submit"/>
        </div>
    </div>
    <hr/>
    <div class="control-group">
        <div class="controls">
            <a class="btn btn-danger delete" href="#">Delete post</a>
        </div>
    </div>
  </form>
</template>
client/views/posts/post_edit.html

E aqui está o gestor post_edit.js que acompanha o template:

Template.postEdit.events({
  'submit form': function(e) {
    e.preventDefault();

    var currentPostId = this._id;

    var postProperties = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val()
    }

    Posts.update(currentPostId, {$set: postProperties}, function(error) {
      if (error) {
        // display the error to the user
        alert(error.reason);
      } else {
        Router.go('postPage', {_id: currentPostId});
      }
    });
  },

  'click .delete': function(e) {
    e.preventDefault();

    if (confirm("Delete this post?")) {
      var currentPostId = this._id;
      Posts.remove(currentPostId);
      Router.go('postsList');
    }
  }
});
client/views/posts/post_edit.js

Por esta altura a maioria do código deve parecer familiar. Primeiro, temos o nosso ajudante de template que carrega o artigo atual e passa-o ao template.

Depois, temos duas callbacks de eventos do template: uma para o evento submit (submeter) do formulário, e outra para o click no link de apagar.

A callback do apagar é muito simples: suprimir o evento de click por omissão, e pedir ao utilizador para confirmar. Caso isso aconteça, obtém o ID do artigo atual do contexto de dados do Template, apaga o artigo, e finalmente redireciona o utilizador para a página inicial.

A callback de edição é ligeiramente maior, mas não muito mais complicada. Depois de suprimir o evento por omissão e de ir buscar o artigo atual, lemos os novos valores dos campos do formulário da página e guardamo-los num objecto postProperties (propriedades do artigo).

Depois passamos este objecto para o Método de Meteor Collection.update(), e utilizamos a callback que ou mostra um erro se a atualização falhou, ou envia o utilizador de volta para a página do artigo caso a atualização tenha sido feita com sucesso.

Adicionando Links

Devemos ainda adicionar links de edição aos nossos artigos para que os utilizadores tenham uma forma de aceder à página de edição de artigos:

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        submitted by {{author}}
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

Naturalmente, não queremos mostrar links de edição para o formulário de outra pessoa. É aqui que o ajudante ownPost entra:

Template.postItem.helpers({
  ownPost: function() {
    return this.userId == Meteor.userId();
  },
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  }
});
client/views/posts/post_item.js
Formulário de edição de artigo.
Formulário de edição de artigo.

Commit 8-1

Formulário de edição de artigos adicionado.

O nosso formulário de edição de artigos está com bom aspeto, mas ainda não é possível editar nada. O que é que se passa?

Definindo Permissões

Dado que anteriormente removemos o pacote insecure, todas as modificações feitas do lado do cliente estão a ser recusadas.

Para corrigir isto, vamos definir alguma regras de permissões. Primeiro, crie um novo ficheiro permissions.js dentro de lib. Isto faz com que a nossa lógica de permissões seja carregada primeiro (e que esteja disponível em ambos os ambientes):

// check that the userId specified owns the documents
ownsDocument = function(userId, doc) {
  return doc && doc.userId === userId;
}
lib/permissions.js

No capitulo Criando Artigos, nós vimo-nos livres dos Métodos allow() porque estávamos apenas a inserir novos artigos através de um Método de servidor (que de qualquer forma, ignora o allow()).

Mas agora que estamos a atualizar e apagar artigos a partir do cliente, vamos voltar ao posts.js e adicionar este bloco allow():

Posts = new Meteor.Collection('posts');

Posts.allow({
  update: ownsDocument,
  remove: ownsDocument
});

Meteor.methods({
  ...
collections/posts.js

Commit 8-2

Permissão básica que verifica o dono do artigo adicionada.

Limitando Edições

Só porque pode editar os seus próprios artigos, não quer dizer que deva poder editar qualquer propriedade. Por exemplo, nós não queremos que os utilizadores sejam capazes de criar um artigo e depois associá-lo a outro utilizador.

A callback deny() do Meteor é utilizada para garantir que apenas campos específicos podem ser atualizados:

Posts = new Meteor.Collection('posts');

Posts.allow({
  update: ownsDocument,
  remove: ownsDocument
});

Posts.deny({
  update: function(userId, post, fieldNames) {
    // may only edit the following two fields:
    return (_.without(fieldNames, 'url', 'title').length > 0);
  }
});
collections/posts.js

Commit 8-3

Permitir que apenas certos campos do artigo possam ser al…

Estamos a pegar na lista fieldNames que contém os campos a serem modificados e usamos o Método without() do Underscore para devolver uma sub-lista contendo apenas os campos que não são url ou title.

Se tudo está normal, essa lista deve estar vazia e o seu tamanho deve ser 0. Se alguem está a tentar alguma coisa manhosa, o tamanho dessa lista será 1 ou mais, e a callback vai devolver true (impedindo assim a atualização).

Chamadas a Métodos vs Manipulação de Dados No Lado do Cliente

Para criar artigos, estamos a utilizar o Método post, enquanto que para os editar e apagar, estamos a chamar update e remove diretamente no cliente e limitamos o acesso usando allow e deny.

Quando é adequado usar um e não o outro?

Quando as coisas são relativamente simples e as regras podem ser exprimidas por allow e deny, é normalmente mais simples fazer as coisas diretamente no cliente.

Manipular a base de dados diretamente a partir do cliente cria uma percepção de imediato, e pode fazer com que a experiência de utilização seja melhor desde que os casos de erro sejam tratados adequadamente (isto é, quando o servidor responde dizendo que a operação afinal falhou).

No entanto, a partir do momento em que é necessário fazer coisas que devem estar fora do controlo do utilizador (tais como dar timestamps a novos artigos ou associar um artigo ao utilizador correto), é provavelmente melhor usar um Método.

Chamadas a Métodos também são mais adequadas em alguns outros casos:

  • Quando é preciso saber ou é preciso devolver valores através de uma callback em vez de esperar que a reatividade e sincronização propaguem as alterações.
  • Para operações de base de dados pesadas que implicariam enviar uma coleção grande para o cliente.
  • Para sumarizar ou agregar dados (por exemplo, contar, médias, somas).

Allow and Deny

Sidebar 8.5

O sistema de segurança do Meteor nos permite controlar modificações ao banco de dados sem precisarmos definir Métodos toda vez que nós queremos fazer mudanças.

Por nós necessitarmos fazer tarefas auxiliares como decorar o post com propriedades extras e tomar uma ação especial quando o URL do post já ter sido postada, usar um Método post específico fazia bastante sentido ao se criar um post.

Por outro lado, nós não precisamos realmente criar novos Métodos para atualizar e deletar posts. Nós apenas precisamos checar se o usuário tem permissão para fazer tais ações, e isto foi feito pelas callbacks allow e deny.

Usar estas callbacks nos permite sermos mais declarativos quanto às modificações ao banco de dados, e dizer que tipo de atualizações podem ser feitas. O fato delas se integrarem ao sistema de contas é um bônus adicional.

Callbacks múltiplas

Nós podemos definir quantas callbacks allow quanto necessárias. Nós precisamos que pelo menos uma delas retorne true para a dada mudança que está ocorrendo. Então quando Posts.insert é chamado no navegador (não importa se é do código do aplicativo do lado do cliente ou do console), o servidor irá em resposta chamar quaisquer checagens de insert permitidas que ele puder até encontrar uma que retorne true. Se ele não encontrar nenhuma, ele não permitirá o insert, e retornará um erro 403 para o cliente.

Similarmente, nós podemos definir uma ou mais callbacks deny. Se qualquer uma dessas callbacks retornar true, a mudança será cancelada e um 403 será retornado. A lógica disto significa que para um insertbem-sucedido, uma ou mais callback allow insert assim como cada callback deny insert serão executadas.

Note: n/e stands for Not Executed
Note: n/e stands for Not Executed

Em outras palavras, Meteor começa a lista de callback primeiro com deny, então com allow, e executa cada callback até que uma delas retorne true.

Um exemplo prático deste padrão poderia ser ter duas callbacks allow(), uma que checa se o post pertence ao usuário atual, e a segunda que checa se o usuário atual tem direitos administrativos. Se o usuário atual é um administrador, isto garante que ele poderá atualizar qualquer post, já que pelo menos uma dessas callbacks retornará true.

Compensação de Latência

Lembre que Métodos de mutação do banco de dados (tais como .update()) tem sua latência compensada, assim como qualquer outro Método. Por exemplo, se você tentar deletar um post que não pertence a você através do console, você verá que o post brevemente desaparece ao passo que sua coleção local perde o documento, mas então re-aparece assim que o servidor informa que, não, de fato o documento não foi deletado.

Claro que este comportamento não é um problema quando acionado pelo console (até mesmo porque se os usuários estiverem tentando bagunçar com informação no console, não é realmente um problema seu o que acontece no navegador deles). Entretanto, você precisa assegurar que isto não aconteça na sua interface do usuário. Por exemplo, você precisa tomar cuidado para assegurar que você não está mostrando aos usuários botões de deletar para documentos que eles não têm permissão para tanto.

Felizmente, já que você pode compartilhar código de permissão entre o cliente e o servidor (por exemplo, você poderia escrever uma função de biblioteca canDeletePost(user, post) e pô-la no diretório compartilhado /lib), fazer isto normalmente não requer muito código extra.

Permissões do lado do servidor

Lembre que o sistema de permissões se refere apenas às mutações do banco de dados iniciadas no cliente. No servidor, o Meteor assume que todas operações são permitidas.

Isto signfica que se você tivesse que escrever um Método Meteor deletePost do lado do servidor que pudesse ser chamado do cliente, qualquer um seria capaz de deletar qualquer post. Então você provavelmente não quer fazer isso a não ser que você cheque as permissões do usuário dentro do Método também.

Usando Deny como uma Callback

Finalmente, um truque que você pode fazer com deny é usá-la como uma callbaclk “onX”. Por exemplo, você pode conseguir uma timestamp lastModified com o código seguinte:

Posts.deny({
  update: function(userId, doc, fields, modifier) {
    doc.lastModified = +(new Date());
    return false;
  },
  transform: null
});

Como callbacks deny rodam para cada update bem-sucedido, nós sabemos que essa callback rodará e poderá fazer mudanças ao documento de uma forma estruturada.

Admitidamente, esta técnica é tipo um hack, então você pode querer fazer atualizações usando um Método no lugar. De qualquer forma, ainda é bom saber, e no futuro nós podemos esperar que algum tipo de callback beforeUpdate esteja disponível.

Erros

9

Usar meramente o diálogo alert() padrão do navegador para avisar o usuário quando há um problema com o envio é um pouco insatisfatório, e certamente não produz uma boa UX. Nós podemos fazer melhor.

Ao invés, vamos construir um mecanismo de reportagem de erro mais versátil que fará melhor o trabalho de dizer ao usuário o que está acontecendo sem interromper o fluxo.

Introduzindo Coleções Locais

Nós implementaremos um simples sistema que rastreará quais erros um usuário viu e mostrará os novos numa área “flash” do site. Este padrão UX é útil quando nós queremos informar ao usuário que algo aconteceu sem interromper o workflow demasiadamente.

O que nós criaremos é similar as mensagems flash costumeiramente encontradas em aplicativos com Ruby on Rails, mas é muito mais sútil pois é implementada do lado do cliente e sabe quando um usuário viu uma mensagem.

Para começar, nós criamos uma coleção para armazenar nossos erros. Dado que os erros são apenas relevantes para a sessão atual e não precisam ser persistentes de forma alguma, nós faremos algo novo, e criaremos uma coleção local. Isso significa que a coleção Errors existirá apenas no navegador, e não fará nenhuma tentativa de sincronizar com o servidor.

Para conseguir isto, nós simplesmente criamos o error num arquivo apenas do cliente, com o nome da coleção configurado para null. Nós criamos uma função throwError que simplesmente insere um error na nossa nova coleção local:

// Local (client-only) collection
Errors = new Meteor.Collection(null);
client/helpers/errors.js

Agora que a coleção foi criada, nós podemos adicionar uma função throwError que nós chamaremos para adicionar erros a ela. Nós não precisamos nos preocupar quanto a allow ou deny ou qualquer outra coisa assim, já que isto é uma coleção local e não será salva no banco de dados Mongo.

throwError = function(message) {
  Errors.insert({message: message})
}
client/helpers/errors.js

A vantagem de usar uma coleção local para armazenar os errors é que, como todas coleções, ela é reativa – significando que nós podemos declarativamente mostrar os erros da mesma forma que nós mostramos informação de qualquer outra coleção.

Mostrando erros

Nós vamos mostrar os erros no topo do nossos layout principal:

<template name="layout">
  <div class="container">
    {{> header}}
    {{> errors}}
    <div id="main" class="row-fluid">
      {{yield}}
    </div>
  </div>
</template>
client/views/application/layout.html

Vamos agora criar os templates errors e error em errors.html:

<template name="errors">
  <div class="errors row-fluid">
    {{#each errors}}
      {{> error}}
    {{/each}}
  </div>
</template>

<template name="error">
  <div class="alert alert-error">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{message}}
  </div>
</template>
client/views/includes/errors.html

Templates Gêmeos

Você notará que nós estamos pondo dois templates num mesmo arquivo. Até agora nós tentamos aderir à convenção “um arquivo, um template”, mas até onde o Meteor se importa pôr todos os nossos templates num único arquivo funciona igualmente bem (apesar que produziria um main.html bem confuso).

Neste caso, já que ambos templates de error são bem curtos, nós faremos uma exceção e os poremos no mesmo arquivo para fazer nosso repositório um pouco mais limpo.

Nós apenas precisamos integrar nosso ajudante de template, e estaremos prontos para ir!

Template.errors.helpers({
  errors: function() {
    return Errors.find();
  }
});
client/views/includes/errors.js

Commit 9-1

Basic error reporting.

Criando erros

Nós agora sabemos como mostrar erros, mas nós ainda precisamos criar alguns antes de vermos qualquer coisa. Erros provêm geralmente de usuários tentando enviar novo conteúdo, então nós checaremos por erros na nossa callback de criação de artigo, e mostraremos uma mensagem para qualquer erro que for levantado.

Em adição, se nós pegarmos o erro 302 (o qual indica que um artigo com a mesma URL já existe), nós redirecionaremos o usuário para o artigo existente. Nós obteremos o _id do artigo exitente do error.details (lembre-se que nós passamos o _id do artigo como o terceiro argumento de details da nossa classe Error no capítulo 7).

Template.postSubmit.events({
  'submit form': function(e) {
    e.preventDefault();

    var post = {
      url: $(e.target).find('[name=url]').val(),
      title: $(e.target).find('[name=title]').val(),
      message: $(e.target).find('[name=message]').val()
    }

    Meteor.call('post', post, function(error, id) {
      if (error) {
        // display the error to the user
        throwError(error.reason);

        if (error.error === 302)
          Router.go('postPage', {_id: error.details})
      } else {
        Router.go('postPage', {_id: id});
      }
    });
  }
});
client/views/posts/post_submit.js

Commit 9-2

Actually use the error reporting.

Experimente: tente criar um artigo e entre a URL http://meteor.com. Como essa URL já está anexada a um artigo nos preenchimentos, você pode ver:

Triggering an error
Triggering an error

Limpando Erros

Agora você pode ter tentando clicar no botão de fechar do error. Se você o fez, você gostaria de ver o erro desaparecer, apenas para logo mais ele reaparecer quando você ler uma nova página. O que está acontecendo?

O botão de fechar dispara o JavaScript embutido do Twitter Bootstrap: não tem nada a ver com o Meteor! Então o que está acontecendo é que o Bootstrap está removendo o <div> do error do DOM, mas não dá coleção Meteor. O que significa dizer que o error continuará a voltar assim que o Meteor re-rendezirar a página.

Então ao menos que nós quisermos que os erros incessantemente voltem dos mortos para nos lembrar de erros passados do usuário e lentamente levá-los à insanidade no processo, é melhor nós adicionarmos uma forma de remover os erros da coleção, também!

Primeiro, nós modificaremos a função throwError para incluir a propriedade seen. Isto será útil mais tarde para sabermos se um error foi de fato visto pelo usuário.

Uma vez feito, nós podemos condificar uma simples função clearErrors que limpa esses erros “seen”:

// Local (client-only) collection
Errors = new Meteor.Collection(null);

throwError = function(message) {
  Errors.insert({message: message, seen: false})
}

clearErrors = function() {
  Errors.remove({seen: true});
}
client/helpers/errors.js

Em seguida, nós limparemos os erros no roteador para ao se navegar para outra página nós garantirmos que esses erros desapareçam para sempre:

// ...

Router.before(requireLogin, {only: 'postSubmit'})
Router.before(function() { clearErrors() });
lib/router.js

Para nossa função clearErrors() funcionar, os erros precisam ser marcados como seen. Para fazer isso devidamente, há um caso fronteiriço que nós precisamos resolver: quando nós lançamos um erro e então redirecionamos o usuário para outro lugar (como nós fazemos quando eles tentam enviar um link duplicado), o redirecionamento acontece instantaneamente. Isso significa que o usuário nunca tem a chance de ver o erro antes de este ser limpo.

É aí que nossa propriedade seen será útil. Nós precisamos assegurar que ela só é modificada para true se o usuário de fato ter visto o erro.

Para conseguir isso, nós usaremos o Meteor.defer(). Esta função diz ao Meteor para executar seu callback “logo depois” do que quer que esteja acontecendo. Se ajudar, você pode considerar que defer() é como dizer ao navegador para esperar 1 milissegundo antes de continuar.

O que nós estamos fazendo é dizer ao Meteor para modificar seen para true 1 milissegundo após o template errors ter sido renderizado. Mas lembre-se como nós dissemos que o redirecionamento ocorre instataneamente? Isto significa que o redirecionamento ocorrerá antes da callback defer, a qual nunca terá uma chance de ser executada.

Isto é exatamente o que nós queremos: se não for executada nosso error não será marcado como visto, o que significa que não será limpo, o que significa que aparecerá na página para qual nosso usuário for redirecionado assim como queríamos!

Template.errors.helpers({
  errors: function() {
    return Errors.find();
  }
});

Template.error.rendered = function() {
  var error = this.data;
  Meteor.defer(function() {
    Errors.update(error._id, {$set: {seen: true}});
  });
};
client/views/includes/errors.js

Commit 9-3

Monitor which errors have been seen, and clear on routing.

O callback rendered dispara uma vez que o nosso template ter sido renderizado no navegador. Dentro do callback, this se refere à instância atual do template, e this.data nos permite acessar a informação do objeto que está atualmente sendo renderizado (no nosso caso, um erro).

Whew! Isto é um monte de trabalho para algo que os usuários felizmente nunca verão!

O callback rendered

O callback rendered do template dispara toda vez que é renderizado no navegador. Isto, é claro, inclui a primeira vez que ele aparece na tela, mas é importante lembrar que o callback também disparará toda vez que o template é re-renderizado, vulgo toda vez que qualquer de suas informações mudar.

Callbacks rendered normalmente dispararão ao menos duas vezes: primeiro quando o aplicativo ler pela primeira vez, e uma segunda vez quando a informação da coleção tiver sido lida. Então você deve ter cuidado ao por qualquer código que não deveria ser disparado duas vezes (tais como um alert, ou códigos de análise para rastreamento de eventos) neles.

Criando um Pacote Meteorite

Sidebar 9.5

Nós construímos um padrão re-utilizável com o nosso trabalho com erros, então porque não envovê-lo num pacote inteligente e compartilhá-lo com o resto da comunidade Meteor?

Primeiro nós precisamos criar alguma estrutura para o nosso pacote residir. Nós pomos o pacote num diretório chamado packages/errors. Isto cria um pacote personalizado que é automaticamente utilizado. (Você pode ter notado que o Meteorite instala pacotes através de symlinks no diretório packages/).

Segundo, nós criaremos package.js nessa pasta, o arquivo que informa ao Meteor como o pacote deve ser utilizado, e os símbolos que ele exporta.

Package.describe({
  summary: "A pattern to display application errors to the user"
});

Package.on_use(function (api, where) {
  api.use(['minimongo', 'mongo-livedata', 'templating'], 'client');

  api.add_files(['errors.js', 'errors_list.html', 'errors_list.js'], 'client');

  if (api.export) 
    api.export('Errors');
});
packages/errors/package.js

Vamos adicionar três arquivos ao pacote. Nós podemos puxar esses arquivos do Microscopoe sem muita mudança exceto por algum namespacing apropriado e uma API ligeiramente mais limpa:

Errors = {
  // Local (client-only) collection
  collection: new Meteor.Collection(null),

  throw: function(message) {
    Errors.collection.insert({message: message, seen: false})
  },
  clearSeen: function() {
    Errors.collection.remove({seen: true});
  }
};

packages/errors/errors.js
<template name="meteorErrors">
  {{#each errors}}
    {{> meteorError}}
  {{/each}}
</template>

<template name="meteorError">
  <div class="alert alert-error">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{message}}
  </div>
</template>
packages/errors/errors_list.html
Template.meteorErrors.helpers({
  errors: function() {
    return Errors.collection.find();
  }
});

Template.meteorError.rendered = function() {
  var error = this.data;
  Meteor.defer(function() {
    Errors.collection.update(error._id, {$set: {seen: true}});
  });
};
packages/errors/errors_list.js

Testando o pacote com Microscope

Agora nós testaremos as coisas localmente com Microscope para assegurar que o nosso código modificado funcione. Para ligar o pacote ao nosso projeto, nós rodamos meteor add errors. Então, nós precisamos deletar os arquivos existentes que se tornaram redudantes devido ao novo pacote:

$ rm client/helpers/errors.js
$ rm client/views/includes/errors.html
$ rm client/views/includes/errors.js
removing old files on the bash console

Uma outra coisa que nós precisamos fazer é algumas pequenas atualizações para utilizar a API correta:

Router.before(function() { Errors.clearSeen(); });
lib/router.js
  {{> header}}
  {{> meteorErrors}}
client/views/application/layout.html
Meteor.call('post', post, function(error, id) {
  if (error) {
    // display the error to the user
    Errors.throw(error.reason);

client/views/posts/post_submit.js
Posts.update(currentPostId, {$set: postProperties}, function(error) {
  if (error) {
    // display the error to the user
    Errors.throw(error.reason);
client/views/posts/post_edit.js

Commit 9-5-1

Created basic errors package and linked it in.

Uma vez que essas mudanças forem feitas, nós devemos ter nosso comportamento original pré-pacote de volta.

Escrevendo testes

O primeiro passo em desenvolver um pacote é testá-lo num aplicativo, mas o próximo é escrever um grupo de teste que testa propriamente o comportamento do pacote. O Meteor em si vem com Tinytest (pacote nativo de testagem), que torna fácil rodar tais testes e manter a paz espiritual ao compartilhar nosso pacote com outros.

Vamos criar um arquivo de teste que usa Tinytest para rodar alguns testes contra a nossa base de código para errors:

Tinytest.add("Errors collection works", function(test) {
  test.equal(Errors.collection.find({}).count(), 0);

  Errors.throw('A new error!');
  test.equal(Errors.collection.find({}).count(), 1);

  Errors.collection.remove({});
});

Tinytest.addAsync("Errors template works", function(test, done) {  
  Errors.throw('A new error!');
  test.equal(Errors.collection.find({seen: false}).count(), 1);

  // render the template
  OnscreenDiv(Spark.render(function() {
    return Template.meteorErrors();
  }));

  // wait a few milliseconds
  Meteor.setTimeout(function() {
    test.equal(Errors.collection.find({seen: false}).count(), 0);
    test.equal(Errors.collection.find({}).count(), 1);
    Errors.clearSeen();

    test.equal(Errors.collection.find({seen: true}).count(), 0);
    done();
  }, 500);
});
packages/errors/errors_tests.js

Nesses testes nós estamos checando se as funções básicas de Meteor.Errors estão funcionando, assim como checando duas vezes que o código rendered no template ainda está funcionando.

Nós não cobriremos as especificidades de se escrever testes para pacotes Meteor aqui (já que a API não está finalizada e sim em alto fluxo), mas felizmente é bem auto-explicativo como funciona.

Para dizer ao Meteor como rodar os teste em package.js, use o código seguinte:

Package.on_test(function(api) {
  api.use('errors', 'client');
  api.use(['tinytest', 'test-helpers'], 'client');  

  api.add_files('errors_tests.js', 'client');
});
packages/errors/package.js

Commit 9-5-2

Added tests to the package.

Então nós podemos rodar os testes com:

$ meteor test-packages errors
Terminal
Passing all tests
Passing all tests

Lançando o pacote

Agora, nós queremos lançar o pacote e torná-lo disponível para o mundo. Nós fazemos isso pondo-o em Atmosphere.

Primeiro, nós precisamos adicionar um smart.json, para dizer ao Meteorite e Atmosphere os detalhes importantes do pacote:

{
  "name": "errors",
  "description": "A pattern to display application errors to the user",
  "homepage": "https://github.com/tmeasday/meteor-errors",
  "author": "Tom Coleman <tom@thesnail.org>",
  "version": "0.1.0",
  "git": "https://github.com/tmeasday/meteor-errors.git",
  "packages": {
  }
}
packages/errors/smart.json

Commit 9-5-3

Added a smart.json

Nós pomos alguma meta-informação básica para prover informação sobre o pacote, incluindo o que ele faz, a localização git onde nós vamos hospedá-lo, e um número inicial de versão. Se nosso pacote necessita de outros pacotes Atmosphere, nós também podemos utilizar o segmento "pacotes" para informar as dependências.

Uma vez que tudo isto esteja no lugar, lançar é fácil. Nós precisaremos criar um repositório git, empurrar a um servidor git remote em algum lugar, e linkar para essa localização no nosso smart.json.

O processo de fazer isso para o GitHub é primeiro criar um novo repositório, então seguir a prática padrão para pegar o código do pacote de dentro do repositório. Então, nós usamos o comando mrt release para publicá-lo:

$ git init
$ git add -A
$ git commit -m "Created Errors Package"
$ git remote add origin https://github.com/tmeasday/meteor-errors.git
$ git push origin master
$ mrt release .
Done!
Terminal (run from within `packages/errors`)

Note: nome de pacotes devem ser únicos. Se você está seguindo palavra-por-palavra e usar o mesmo nome de pacote, haverá um conflito e não funcionará. No futuro a Atmosphere separará os pacotes com nome de autor, então você pode esperar que isso mude.

Outra coisa: Você precisará logar em http://atmosphere.meteor.com e criar um username e senha com os quais você entrará na linha de comando quando você chamar mrt release ..

Agora que o pacote foi lançado, nós podemos deletá-lo do nosso projeto e então adicioná-lo de volta diretamente usando Meteorite:

$ rm -r packages/errors
$ mrt add errors
Terminal (run from the top level of the app)

Commit 9-5-4

Removed package from development tree.

Agora nós devemos ver o Meteorite fazer download do nosso pacote pela primeira vez. Parabéns!

Comentários

10

A meta de um site social de notícias é criar uma comunidade de usuários, e será difícil fazê-la sem providenciar uma forma para as pessoas conversarem entre si. Então neste capítulo, vamos adicionar comentários!

Nós começaremos criando uma nova coleção para armazenar comentários, e adicionando alguma informação de exemplos básica na coleção.

Comments = new Meteor.Collection('comments');
collections/comments.js
// Fixture data 
if (Posts.find().count() === 0) {
  var now = new Date().getTime();

  // create two users
  var tomId = Meteor.users.insert({
    profile: { name: 'Tom Coleman' }
  });
  var tom = Meteor.users.findOne(tomId);
  var sachaId = Meteor.users.insert({
    profile: { name: 'Sacha Greif' }
  });
  var sacha = Meteor.users.findOne(sachaId);

  var telescopeId = Posts.insert({
    title: 'Introducing Telescope',
    userId: sacha._id,
    author: sacha.profile.name,
    url: 'http://sachagreif.com/introducing-telescope/',
    submitted: now - 7 * 3600 * 1000
  });

  Comments.insert({
    postId: telescopeId,
    userId: tom._id,
    author: tom.profile.name,
    submitted: now - 5 * 3600 * 1000,
    body: 'Interesting project Sacha, can I get involved?'
  });

  Comments.insert({
    postId: telescopeId,
    userId: sacha._id,
    author: sacha.profile.name,
    submitted: now - 3 * 3600 * 1000,
    body: 'You sure can Tom!'
  });

  Posts.insert({
    title: 'Meteor',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://meteor.com',
    submitted: now - 10 * 3600 * 1000
  });

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000
  });
}
server/fixtures.js

Não vamos nos esquecer de publicar e fazer assinatura à nossa nova coleção:

Meteor.publish('posts', function() {
  return Posts.find();
});

Meteor.publish('comments', function() {
  return Comments.find();
});
server/publications.js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { 
    return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
  }
});
lib/router.js

Commit 10-1

Added comments collection, pub/sub and fixtures.

Note que para ativar este código de exemplos, você precisará usar meteor reset para limpar o banco de dados. Após limpar, não esqueça de criar uma nova conta de usuário para logar de novo!

Primeiro, nós criamos alguns usuários (completamente falsos), inserindo-os no banco de dados e usando seus ids para selecioná-los no banco de dados mais tarde. Então nós adicionamos um comentário para cada usuário no primeiro artigo, ligando o comentário ao artigo (com postId), e ao usuário (com userId). Nós também adicionamos uma data de envio e um corpo a cada comentário, junto com author, um campo desnormalizado.

Também, nós melhoramos nosso roteador para esperar tanto os comentários quanto os artigos.

Mostrando comentários

Está tudo certo em por comentários no banco de dados, mas nós também precisamos mostrá-los na page de discussão. Felizmente este processo deve ser familiar a você agora, e você tem uma idéia dos passos envolvidos:

<template name="postPage">
  {{> postItem}}

  <ul class="comments">
    {{#each comments}}
      {{> comment}}
    {{/each}}
  </ul>
</template>
client/views/posts/post_page.html
Template.postPage.helpers({
  comments: function() {
    return Comments.find({postId: this._id});
  }
});
client/views/posts/post_page.js

Nós pomos o bloco {{#each comments}} dentro do template do artigo, então this é um artigo dentro do ajudante comments. Para encontrar comentários relevantes, nós checamos aqueles que estão ligados ao post através do atributo postId.

Dado que nós aprendemos sobre ajudantes e handlebars, renderizar um comentário é bem linear. Nós criaremos um novo diretório comments dentro de views para armazenar toda nossa informação de comentário:

<template name="comment">
  <li>
    <h4>
      <span class="author">{{author}}</span>
      <span class="date">on {{submittedText}}</span>
    </h4>
    <p>{{body}}</p>
  </li>
</template>
client/views/comments/comment.html

Vamos configurar um simples ajudante de template para formatar nossa informação submitted para um formato legível por humanos (a menos que você seja uma daquelas pessoas que conseguem entender códigos de UNIX timestamps e cores hexadecimais fluentemente?)

Template.comment.helpers({
  submittedText: function() {
    return new Date(this.submitted).toString();
  }
});
client/views/comments/comment.js

Então, nós mostraremos o número de comentários de cada artigo:

<template name="postItem">
  <div class="post">
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        submitted by {{author}},
        <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html

E adicionaremos o ajudante commentsCount ao nosso administrador postItem:

Template.postItem.helpers({
  ownPost: function() {
    return this.userId == Meteor.userId();
  },
  domain: function() {
    var a = document.createElement('a');
    a.href = this.url;
    return a.hostname;
  },
  commentsCount: function() {
    return Comments.find({postId: this._id}).count();
  }
});
client/views/posts/post_item.js

Commit 10-2

Display comments on `postPage`.

Nós devemos ser capazes de mostrar nossos comentários de exemplo e ver algo assim:

Displaying comments
Displaying comments

Enviando Comentários

Vamos adicionar uma forma dos nossos usuários criarem comentários. O processo que seguiremos será bem similar ao como nós criamos usuários para criar novos artigos.

Nós começaremos por adicionar uma caixa de envio no fim de cada artigo:

<template name="postPage">
  {{> postItem}}

  <ul class="comments">
    {{#each comments}}
      {{> comment}}
    {{/each}}
  </ul>

  {{#if currentUser}}
    {{> commentSubmit}}
  {{else}}
    <p>Please log in to leave a comment.</p>
  {{/if}}
</template>
client/views/posts/post_page.html

E então criar um template de formulário de comentário:

<template name="commentSubmit">
  <form name="comment" class="comment-form">
    <div class="control-group">
        <div class="controls">
            <label for="body">Comment on this post</label>
            <textarea name="body"></textarea>
        </div>
    </div>
    <div class="control-group">
        <div class="controls">
            <button type="submit" class="btn">Add Comment</button>
        </div>
    </div>
  </form>
</template>
client/views/comments/comment_submit.html
The comment submit form
The comment submit form

Para enviar nossos comentários, nós chamamos um Método comment no administrador commentSubmit que opera de uma forma similar ao administrador postSubmit:

Template.commentSubmit.events({
  'submit form': function(e, template) {
    e.preventDefault();

    var $body = $(e.target).find('[name=body]');
    var comment = {
      body: $body.val(),
      postId: template.data._id
    };

    Meteor.call('comment', comment, function(error, commentId) {
      if (error){
        throwError(error.reason);
      } else {
        $body.val('');
      }
    });
  }
});
client/views/comments/comment_submit.js

Assim como nós previamente configuramos um Método post do lado do servidor, nós configuraremos um Método Meteor comment para criar nossos comentários, cheque que tudo é legítimo, e finalmente insira o novo comentário na coleção de comentários.

Comments = new Meteor.Collection('comments');

Meteor.methods({
  comment: function(commentAttributes) {
    var user = Meteor.user();
    var post = Posts.findOne(commentAttributes.postId);
    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to make comments");

    if (!commentAttributes.body)
      throw new Meteor.Error(422, 'Please write some content');

    if (!post)
      throw new Meteor.Error(422, 'You must comment on a post');

    comment = _.extend(_.pick(commentAttributes, 'postId', 'body'), {
      userId: user._id,
      author: user.username,
      submitted: new Date().getTime()
    });

    return Comments.insert(comment);
  }
});
collections/comments.js

Commit 10-3

Created a form to submit comments.

Isto não está fazendo nada requintado, apenas checando se o usuário está logado, que o comentário tem um corpo, e que esteja ligado a um artigo.

Controlando a Assinatura dos Comentários

Como as coisas estão, nós estamos publicando todos comentários de todos os artigos para todos os clientes conectados. Isso é um desperdício. Já que, nós estamos apenas utilizando um pequeno subconjunto da informação a qualquer momento que for. Vamos melhorar nossa publicação e assinatura para controlar exatamente quais comentários são publicados.

Se nós pensarmos sobre isso, o único momento que nós precisamos fazer a assinatura para a publicação dos nossos comments é quando o usuário acessa a página individual do artigo, e nós precisamos apenas ler um subconjunto dos comentários relacionados a este artigo em particular.

O primeiro passo será mudar a forma como nós fazemos a assinatura para os comentários. Até agora, nós temos feito a assinatura no nível do roteador, o que significa que nós lemos toda nossa informação quando o roteador é inicializado.

Mas nós agora queremos que a nossa assinatura dependa de um parâmetro path, e esse parâmetro pode obviamente mudar a qualquer momento. Então nós precisaremos mover nosso código de assinatura do nível do roteador para o nível da rota.

Isto tem outra conseqüência: ao invés de ler nossa informação quando nós inicializamos nosso aplicativo, nós agora a leremos toda vez que nós alcançamos nossa rota. Isto significa que você agora terá momentos de loading enquanto navega dentro do aplicativo, é um ponto vagamente negativo a não ser que você queira ler de vez o seu conjunto de informação todo para sempre.

Assim que a nossa nova função waitOn ao nível da rota se parece:

Router.map(function() {

  //...

  this.route('postPage', {
    path: '/posts/:_id',
    waitOn: function() {
      return Meteor.subscribe('comments', this.params._id);
    },
    data: function() { return Posts.findOne(this.params._id); }
  });

  //...

});
lib/router.js

Você perceberá que nós estamos passando this.params._id como um argumento à assinatura. Então usamos essa nova informação para garantir que nós restrinjamos nosso conjunto de informação aos comentário pertencentes ao artigo atual:

Meteor.publish('posts', function() {
  return Posts.find();
});

Meteor.publish('comments', function(postId) {
  return Comments.find({postId: postId});
});
server/publications.js

Commit 10-4

Made a simple publication/subscription for comments.

Há apenas um problema: quando nós retornamos à página inicial, ela diz que todos nossos artigos tem 0 comentários:

Our comments are gone!
Our comments are gone!

Contando Comentários

A razão para isto ficará logo clara: nós apenas temos no máximo um dos nossos comentários do artigo lido, então quando nós chamamos Comments.find({postId: this._id}) no ajudante commentsCount no administrador post_item, Meteor não consegue encontrar a informação necessária do lado do cliente para nos prover um resultado.

A melhor maneira para lidar com isto é desnormalizar o número de comentários do artigo (se você não está certo do que isso significa não se preocupe, a próxima barra lateral cobrirá isso!). Como veremos, há uma pequena adição de complexidade no nosso código, o benefício de performance que nós ganhamos de não ter que publicar todos comentários para mostrar a lista de artigos vale a pena.

Nós conseguiremos isso ao adicionar uma propriedade commentsCount à informação de estrutura do post. Para começar, nós atualizamos nossos exemplos de artigo. (e meteor reset para relê-los – não esqueça de recriar sua conta de usuário depois):

var telescopeId = Posts.insert({
  title: 'Introducing Telescope',
  ..
  commentsCount: 2
});

Posts.insert({
  title: 'Meteor',
  ...
  commentsCount: 0
});

Posts.insert({
  title: 'The Meteor Book',
  ...
  commentsCount: 0
});
server/fixtures.js

Então, nós garantimos que todos novos artigos comecem com 0 comentário:

// pick out the whitelisted keys
var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
  userId: user._id, 
  author: user.username, 
  submitted: new Date().getTime(),
  commentsCount: 0
});

var postId = Posts.insert(post);
collections/posts.js

E então nós atualizamos o commentsCount relevante quando nós fazemos um novo comentário usando o operador Mongo $inc (o qual incrementa o campo numérico por um):

// update the post with the number of comments
Posts.update(comment.postId, {$inc: {commentsCount: 1}});

return Comments.insert(comment);
collections/comments.js

Finalmente, nós podemos apenas remover o ajudante commentsCount do client/views/posts/post_item.js, já que o campo está agora diretamente disponível no nosso artigo.

Commit 10-5

Denormalized the number of comments into the post.

Agora que nossos usuários podem conversar entre si, seria uma pena se eles não soubessem dos novos comentários. E sabe mais, o próximo capítulo mostrará a você como implementar notificações para previnir exatamente isto!

Desnormalização

Sidebar 10.5

Desnormalizar a informação significa não armazená-la de forma “normal”. Em outras palavras, desnormalização significa ter múltiplas cópias do mesmo pedaço de informação por aí.

No último capítulo, nós desnormalizamos a contagem do número de comentários no objeto post para evitar ter de ler todos os comentários o tempo todo. Num sentido de modelagem da informação isto é redundante, já que nós podíamos apenas contar o conjunto correto de comentário a qualquer momento para descobrir o valor (deixando de lado considerações quanto à performance).

Desnormalização geralmente significa trabalho extra para o desenvolvedor. No nosso exemplo, cada vez que nós adicionamos ou removemos um comentário nós também precisamos nos lembrar de atualizar o artigo relevante para assegurar que o campo commentsCount continue correto. Isto é exatamente o porquê de bancos de dados relacionais como MySQL franzirem as sobrancelhas para isto tipo de procedimento.

Entretanto, o procedimento normal também tem suas desvantagens: sem uma propriedade commentsCount, nós precisaríamos mandar todos os comentários pela fiação todas as vezes apenas para sermos capazes de contá-los, o que era o que estávamos fazendo no início. Desnormalizar nos permite evitar isso completamente.

Uma Publicação Especial

Seria possível criar uma publicação especial que enviaria apenas a contagem de comentários que nós estamos interessados (vulgo a contagem de comentários de artigos que nós atualmente vemos, através de consultas agregadas no servidor).

Mas é válido considerar se a complexidade de tal código de publicação não pesa mais que as dificuldades criadas por desnormalizar…

Claro, tais considerações devem ser específicas ao aplicativo: se você está escrevendo um código onde a integridade da informação é de suma importância, então evitar inconsistências na informação é bem mais importante e de uma ordem de prioridade maior para você do que ganhos de performance.

Embutindo Documentos ou Criando Coleções Múltiplas

Se você é experiente em Mongo, você pode ter se surpreendido ao ver que nós criamos uma segunda coleção apenas para os comentários: por que não apenas imbutí-los numa lista dentro do documento artigo?

A questão é que várias das ferramentas que o Meteor dá funcionam bem melhor quando se opera ao nível da coleção. Por exemplo:

  1. O ajudante {{#each}} é bem eficiente quando interando sobre um cursor (o resultado de collection.find()). O mesmo é verdadeiro quando ele intera sobre um array de objetos dentro de um documento maior.
  2. allow e deny operam no nível do documento, e então torna mais fácil assegurar que qualquer modificações em comentários individuais estão corretas de uma forma que seria mais complexa se nós operassemos no nível do artigo.
  3. DDP opera ao nível de atributos de nível superior do documento–isto significa se comments fosse uma propriedade do post, cada vez que um comentário fosse criado no artigo, o servidor enviaria a lista de comentários inteira atualizada do artigo para cada cliente conectado.
  4. Publicações e assinaturas são bem mais fáceis de controlar no nível dos documentos. Por exemplo, se nós quisermos paginar os comentários do artigo seria difícil a não ser que os comentários estivessem em sua própria coleção.

Mongo sugere documentos embutidos para reduzir o número de consultas despendiosas para pegar documentos. Entretanto, isto não é tanto a questão quando se leva em consideração a arquitetura do Meteor: a maior parte do tempo nós estamos consultando comentários no cliente, onde o acesso ao banco de dados é essencialmente livre.

As Desvantagens da Desnormalização

Há um bom argumento a ser feito sobre porque não devemos desnormalizar nossa informação. Para uma boa olhada sobre o caso contra a desnormalização, nós recomendamos Why You Should Never Use MongoDB por Sarah Mei.

Notificações

11

Agora que os usuários possam comentar nos artigos uns dos outros, seria bom permití-los saber que uma conversa começou.

Para tanto, nós notificaremos o dono do artigo de que houve um comentário em seu artigo, e providenciaremos um link para eles verem esse comentário.

Este é o tipo de utilidade onde o Meteor realmente brilha: por o Meteor ser em tempo real por padrão, nós mostraremos essas notificações instantaneamente. Nós não precisamos esperar o usuário atualizar a página ou checar manualmente, nós podemos simplesmente pipocar novas notificações sem precisar escrever nenhum código especial.

Criando notificações

Nós criaremos uma notificação quando alguém comenta nos seus artigos. No futuro, notificações poderão ser estendidas para cobrir muitos outros cenários, mas por hora isso seria o suficiente para manter os usuários informados do que está acontecendo.

Vamos criar nossa coleção Notifications, assim como uma função createCommentNotification que inserirá uma notificação correspondente a cada novo comentário em um de seus artigos:

Notifications = new Meteor.Collection('notifications');

Notifications.allow({
  update: ownsDocument
});

createCommentNotification = function(comment) {
  var post = 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
    });
  }
};
collections/notifications.js

Assim como artigos e comentários, esta coleção Notifications será compartilhada tanto pelo cliente quanto pelo servidor. Já que nós precisamos atualizar as notificações uma vez que o usuário já as viu, nós também disponibilizamos atualizações, garantindo como sempre que nós restrinjamos as permissões de atualização à própria informação do usuário.

Nós também criamos uma simples função que olha para o artigo que o usuário está comentando, descobre quem deve ser notificado a partir daí, e insere uma nova notificação.

Nós já estamos criando comentários com um Método do lado do servidor, então nós podemos apenas melhorar este Método para chamar a nossa função. Nós trocaremos return Comments.insert(comment); por comment._id = Comments.insert(comment) para salvar a _id do recém criado comentário em uma variável, então chamamos nossa função createCommentNotification:

Comments = new Meteor.Collection('comments');

Meteor.methods({
  comment: function(commentAttributes) {

    // [...]

    // create the comment, save the id
    comment._id = Comments.insert(comment);

    // now create a notification, informing the user that there's been a comment
    createCommentNotification(comment);

    return comment._id;
  }
});
collections/comments.js

Vamos também publicar as notificações, e fazer assinatura no cliente:

// [...]

Meteor.publish('notifications', function() {
  return Notifications.find();
});
server/publications.js
Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { 
    return [Meteor.subscribe('posts'), Meteor.subscribe('notifications')]
  }
});
lib/router.js

Commit 11-1

Added basic notifications collection.

Mostrando Notificações

Agora nós podemos ir em frente e adicionar uma lista de notificações ao cabeçalho:

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          {{#if currentUser}}
            <li>
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html

E criar os templates notifications e notification (eles compartilharão um mesmo arquivo notifications.html):

<template name="notifications">
  <a href="#" class="dropdown-toggle" data-toggle="dropdown">
    Notifications
    {{#if notificationCount}}
      <span class="badge badge-inverse">{{notificationCount}}</span>
    {{/if}}
    <b class="caret"></b>
  </a>
  <ul class="notification dropdown-menu">
    {{#if notificationCount}}
      {{#each notifications}}
        {{> notification}}
      {{/each}}
    {{else}}
      <li><span>No Notifications</span></li>
    {{/if}}
  </ul>
</template>

<template name="notification">
  <li>
    <a href="{{notificationPostPath}}">
      <strong>{{commenterName}}</strong> commented on your post
    </a>
  </li>
</template>
client/views/notifications/notifications.html

Nós podemos ver que o plano é que cada notificação contenha um link para o artigo que foi comentado, e o nome do usuário que comentou.

Em seguida, nós precisamos ter certeza que nós selecionamos a lista certa de notificações no nosso administrador, e atualizamos as notificações como “lidas” quando o usuário clica no link para onde elas apontam.

Template.notifications.helpers({
  notifications: function() {
    return Notifications.find({userId: Meteor.userId(), read: false});
  },
  notificationCount: function(){
    return Notifications.find({userId: Meteor.userId(), read: false}).count();
  }
});

Template.notification.helpers({
  notificationPostPath: function() {
    return Router.routes.postPage.path({_id: this.postId});
  }
})

Template.notification.events({
  'click a': function() {
    Notifications.update(this._id, {$set: {read: true}});
  }
})
client/views/notifications/notifications.js

Commit 11-2

Display notifications in the header.

Você pode pensar que notificações não são tão diferentes de erros, e é verdade que a estrutura deles é bem similar. Há uma grande diferença porém: nós criamos uma coleção do lado do cliente sincronizada. Isto significa que nossas notificações são persistentes e, desde que nós usemos a mesma conta de usuário, elas existirão ao longo de atualizações do navegador e diferentes dispositivos.

Tente: abra um segundo navegador (digamos o Firefox), crie uma nova conta de usuário, e comente num artigo que você criou com sua conta principal (a qual você deixou aberta no Chrome). Você deve ver algo assim:

Displaying notifications.
Displaying notifications.

Controlando acesso às notificações

As notificações estão funcionando devidamente. Entretanto há um pequeno problema: nossas notificações são públicas.

Se você ainda tiver seu segundo navegador aberto, tente rodar o código seguinte no console do navegador:

 Notifications.find().count();
1
Browser console

Este novo usuário (aquele que comentou) não deve ter nenhuma notificação. As notificações que eles podem ver na coleção Notifications na verdade pertencem ao nosso usuário original.

Pondo de lado questões de privacidade em potencial, nós simplesmente não podemos ter todas notificações de cada usuário sendo lidas no navegador dos outros usuários. Num site grande o suficiente, isso poderia sobrecarregar a memória disponível do navegador e começar a causar sérios problemas de performance.

Nós solucionamos essa questão com publicações. Nós podemos usar nossas publicações para especificar precisamente que parte da nossa coleção nós queremos compartilhar com cada navegador.

Para conseguir isso, nós precisamos retornar um cursor diferente de Notifications.find() na nossa publicação. Nós queremos retornar um cursor que corresponda às notificações do usuário logado.

Fazer isso é bem linear, já que uma função publish tem disponível a _id do usuário logado em this.userId:

Meteor.publish('notifications', function() {
  return Notifications.find({userId: this.userId});
});
server/publications.js

Commit 11-3

Only sync notifications that are relevant to the user.

Agora se nós checarmos nas nossas duas janelas de navegador, nós devemos ver duas coleções de notificações diferentes:

 Notifications.find().count();
1
Browser console (user 1)
 Notifications.find().count();
0
Browser console (user 2)

Na verdade, a lista de Notificações deve até mudar quando o usuário logar e deslogar do aplicativo. Isto é porque publicações automaticamente re-publicam toda vez que a conta de usuário muda.

Nosso aplicativo está se tornando cada vez mais e mais funcional, e assim que mais usuários se cadastrarem e começarem a postar links nós correremos o risco de terminar com uma homepage sem fim. Nós checaremos isso no próximo capítulo ao implementar paginação.

Reatividade Avançada

Sidebar 11.5

É raro você mesmo precisar escrever código para rastrear dependências, mas é certamente útil entendê-lo para traçar como funciona o caminho do fluxo de resolução.

Imagine que nós gostaríamos de rastrear quantos amigos de Facebook do usuário logado “gostaram” de cada artigo no Microscope. Vamos supor que nós já resolvemos os detalhes de como autenticar o usuário com Facebook, fazer as chamadas à API apropriadas, e análise da informação relevante. Nós agora temos uma função assíncrona do lado do cliente que retorna o número de likes, getFacebookLikeCount(user, url, callback).

A coisa importante a se lembrar sobre tal função é que ela é bastante não reativa e não em tempo real. Ela fará um pedido HTTP ao Facebook, trará alguma informação, e a fará disponível à aplicação num callback assíncrono, mas a função não re-rodará sozinha quando a contagem mudar no Facebook, e nossa UI não mudará quando a informação subjacente mudar.

Para consertar isso, nós podemos começar por usar setInterval para chamar nossa função a cada alguns segundos:

currentLikeCount = 0;
Meteor.setInterval(function() {
  var postId;
  if (Meteor.user() && postId = Session.get('currentPostId')) {
    getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
      function(err, count) {
        if (!err)
          currentLikeCount = count;
      });
  }
}, 5 * 1000);

A qualquer momento que nós checarmos a variável currentLikeCount, nós podemos esperar o número correto com uma margem de erro de 5 segundos. Nós podemos agora usar esta variável num ajudante assim:

Template.postItem.likeCount = function() {
  return currentLikeCount;
}

Entretanto, nada ainda diz ao nosso template para re-desenhar quando currentLikeCount mudar. Apesar da variável ser agora pseudo-em tempo real já que ela muda sozinha, ela não é reativa então ela ainda não consegue se comunicar devidamente com o resto do ecosistema do Meteor.

Rastreando Reatividade: Computações

A reatividade do Meteor é mediada por dependências, estruturas de informação que rastreiam um conjunto de computações.

Como nós vimos anteriormente na barra lateral reatividade, uma computação é um segmento do código que usa informação reativa. No nosso caso, há uma computação que está sendo criada implicitamente pelo template postItem. Cada ajudante no administrador desse template está trabalhando dentro da computação.

Você pode pensar na computação como o segmento de código que “se importa” quanto à informação reativa. Quando a informação muda, esta será a computação que será informada (via invalidate()), e é a computação que decide quando algo precisa ser feito.

Transformando uma Variável em uma Função Reativa

Para tornar nossa variável currentLikeCount em uma fonte de informação reativa, nós precisamos rastrear todas as computações que a utilizam em uma dependência. Isto requer mudá-la de uma variável em uma função (que retornará um valor):

var _currentLikeCount = 0;
var _currentLikeCountListeners = new Deps.Dependency();

currentLikeCount = function() {
  _currentLikeCountListeners.depend();
  return _currentLikeCount;
}

Meteor.setInterval(function() {
  var postId;
  if (Meteor.user() && postId = Session.get('currentPostId')) {
    getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
      function(err, count) {
        if (!err && count !== _currentLikeCount) {
          _currentLikeCount = count;
          _currentLikeCountListeners.changed();
        }
      });
  }
}, 5 * 1000);

O que nós fizemos foi configurar uma dependência _currentLikeCountListerners, a qual rastreia todas as computações dentro das quais currentLinkCount() foi utilizada. Quando o valor de _currentLikeCount muda, nós chamamos a função changed() nesta dependência, a qual invalida todas as computações rastreadas.

Essas computações podem então ir em frente e lidarem com a mudança caso a caso. Neste caso da computação do template, parece que o template re-desenha a si mesmo.

Computação de Template e Controlando o Redesenhar

A razão de porque cada template tem sua própria computação é para controlar a quantidade de redesenhar que ocorre na tela.

Quando nós chamamos um template de dentro de outro, nós estamos estabelecendo uma segunda computação dentro da primeira. Então quando a informação reativa do template interior muda, este template interior é redesenhado porém o template exterior continua intacto. Desta forma, computações são utilizadas para controlar o escopo das mundanças reativas.

O Meteor também nos dá um pouco de ajuda extra para melhorar os mecanismos básicos de aninhar templates.

Primeiro, o ajudante em bloco {{#constant}} mata a reatividade dentro de si. Então qualquer informação que é colhida pelos ajudantes dentro do bloco é apenas usada uma vez. E mesmo que o template exterior seja redesenhado, o HTML da área constante é deixado de lado já que seria renderizado de mesma exata forma. Isto faz das regiões constantes uma grande forma de adminsitrar widgets de terceiros que não estão esperando que o Meteor re-desenhe o DOM sob eles.

O segundo utilitário que pode nos ajudar a controlar a reativiadde é o ajudante em bloco {{#isolate}}, o qual configura uma nova computação dentro de um template. Em outras palavras, tem o mesmo efeito de mover um segmento do template para dentro de um sub-template em termos de reatividade e redesenho.

Então se uma das fontes de informação reativa dentro do bloco isolate mudar, a área isolada será re-desenhada, mas o remanecescente do template superior não. Se o template superior re-desenhar porém, a área isolada será redesenhada também.

Comparando Deps a Angular

Angular é uma biblioteca de renderização reativa do lado do cliente apenas, desenvolvida pelo bom pessoal do Google. É interessante comparar o modo de rastrear dependências do Meteor e do Angular, já que os modos são bem diferentes.

Nós vimos que o modelo do Meteor usa blocos de códigos chamados computações. Essas computações são rastreadas por fontes de informações “reativas” (funções) que tratam de invalidá-las quando necessário. Então a fonte de informação explicitamente informa todas as suas dependências quando eles precisam chamar invalidate(). Note que apesar que isso é geralmente quando informação muda, a fonte de informação poderia potencialmente decidir desencadear uma invalidação por outras razões.

Adicionalmente, apesar que computações usualmente apenas re-rodam quando invalidadas, você pode configurá-las para se comportar de qualquer forma que você quiser.

Em Angular, a reatividade é mediada pelo objeto scope. Um escopo pode ser pensado como um objeto JavaScript comum com alguns métodos especiais.

Quando você quer reativamente depender em um valor em um escopo, você chama scope.$watch, providenciando a expressão que você está interessado (vulgo quais partes do escopo você se importa) e uma função ouvinte que rodará cada vez que a expressão mudar. Então você explicitamente afirma exatamente o que você quer que seja feito toda vez que o valor da expressão mudar.

Voltando ao nosso exemplo do Facebook, nós escreveríamos:

$rootScope.$watch('currentLikeCount', function(likeCount) {
  console.log('Current like count is ' + likeCount);
});

Claro, assim como você raramente configura computações em Meteor, você não chama $watch explicitamente com freqüência em Angular pois diretivas ng-model e {{expressions}} automaticamente configuram watches que então cuidam de re-renderizarem quando há mudança.

Quando tal valor reativo mudar, scope.$apply() deve então ser chamado. Isto re-avalia cada observador do escopo, mas apenas chama a função ouvinte dos observadores dos quais o valor da expressão tenha mudado.

Então scope.$apply() é similar a dependency.changed(), exceto por agir no nível do escopo, ao invés de lhe dar o controle quanto a dizer precisamente quais ouvintes devem ser re-avaliados. Tendo dito isto, esta leve falta de controle dá a Angular a habilidade de ser bem inteligente e eficiente na forma como ele determina precisamente quais ouvintes precisam ser re-avaliados.

Com Angular, nosso código da função getFacebookLikeCount() se pareceria com algo assim:

Meteor.setInterval(function() {
  getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
    function(err, count) {
      if (!err) {
        $rootScope.currentLikeCount = count;
        $rootScope.$apply();
      }
    });
}, 5 * 1000);

Admitidamente, o Meteor toma conta da maior parte do trabalho pesado para nós e nos deixa colher os benefícios da reatividade sem muito trabalho da nossa parte. Mas felizmente, aprender sobre esses padrões se mostrará útil se alguma vez precisarmos levar as coisas mais adiante.

Paginação

12

O Microscope está com muito bom aspeto, e podemos esperar uma recepção calorosa quando este for lançado para o mundo.

Assim provavelmente é boa ideia pensar um pouco sobre as implicações de performance do número de artigos novos que vão ser criados no site quando este for lançado!

Anteriormente falámos de como uma coleção no lado do cliente deve conter um sub conjunto dos dados no servidor, e até conseguimos atingir isto para as nossas coleções de notificações e comentários.

No entanto, atualmente ainda estamos a publicar todos os artigos de uma vez, para todos os utilizadores ligados. Eventualmente, se milhares de links forem submetidos, isto será problemático. Para resolver isto, precisamos de paginar os artigos.

Adicionando Mais Artigos

Primeiro, nos nossos dados de teste, vamos carregar artigos suficientes para que a paginação faça realmente sentido:

// Fixture data 
if (Posts.find().count() === 0) {

  //...

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000,
    commentsCount: 0
  });

  for (var i = 0; i < 10; i++) {
    Posts.insert({
      title: 'Test post #' + i,
      author: sacha.profile.name,
      userId: sacha._id,
      url: 'http://google.com/?q=test-' + i,
      submitted: now - i * 3600 * 1000,
      commentsCount: 0
    });
  }
}
server/fixtures.js

Depois de executar meteor reset, você deve obter algo como:

Mostrando dados de teste.
Mostrando dados de teste.

Commit 12-1

Foram adicionados artigos suficientes para que a paginaçã…

Paginação Infinita

Vamos implementar uma paginação estilo “infinito”. O que queremos dizer por este termo é que vamos primeiro mostrar, por exemplo, 10 artigos no ecrã, com um link “carregar mais” no fundo. Clicar neste link vai adicionar mais 10 artigos à lista, e por ai adiante ad infinitum. Isto significa que podemos controlar todo o sistema de paginação com um único parâmetro que representa o número de artigos a mostrar no ecrã.

Agora vamos precisar de uma forma de comunicar este parâmetro ao servidor para que ele fique a saber quantos artigos enviar para o cliente. Acontece que já estamos a subscrever à publicação posts no roteador, portanto vamos tirar vantagem disto e deixar ser também o roteador lidar com a paginação.

A forma mais fácil de configurar isto é simplesmente fazendo o parâmetro limite de artigos parte do caminho, dando-nos URLs na forma http://localhost:3000/25. Um bonús adicional de usar o URL comparativamente a outros métodos é que se estamos atualmente a mostrar 25 artigos e por acaso fazemos refresh ao navegador por engano, vamos continuar a ver 25 artigos quando a página voltar a carregar.

Para fazer isto corretamente, vamos precisar de alterar a forma como subscrevemos aos artigos. Tal como previamente fizemos no capitulo Comentários, vamos precisar de mover o nosso código da subscrição do nível do roteador para o nível da rota.

Isto tudo pode ser muito para interiorizar de uma vez, mas vai ficar mais claro vendo o código.

Primeiro, vamos parar de subscrever à publicação posts no bloco Router.configure(). Basta remover Meteor.subscribe('posts'),, deixando apenas a subscrição às notifications:

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

Depois vamos adicionar um parâmetro postsLimit ao caminho da rota. Adicionar um ? depois do nome do parâmetro significa que é opcional. Ou seja, a nossa rota não só vai combinar com http://localhost:3000/50, mas também com o antigo http://localhost:3000.

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?'
  });
});
lib/router.js

É importante notar que um caminho na forma /:parameter? vai combinar com todos os caminhos possíveis. Dado que cada rota vai ser analisada sucessivamente para ver se combina com o caminho atual, temos de garantir que organizamos as nossas rotas por ordem de especificidade decrescente.

Noutras palavras, rotas que apontem para rotas mais específicas como /posts/:_id devem vir primeiro, e a nossa rota postsList deve ser movida para o fundo do ficheiro dado que basicamente combina com tudo.

Está agora na altura de lidar com o problema difícil de subscrever e encontrar os dados corretos. Precisamos de lidar com o caso em que o parâmetro postsLimit não está presente, caso em que lhe damos um valor por defeito. Vamos usar “5” para podermos ter espaço para brincar com a paginação.

Router.map(function() {
  //..

  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var postsLimit = parseInt(this.params.postsLimit) || 5; 
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: postsLimit});
    }
  });
});
lib/router.js

Vai notar que estamos agora a passar um objecto JavaScript ({limit: postsLimit}) em conjunto com o nome da nossa publicação posts (artigos). Este objecto vai servir como o parâmetro options para o código de servidor Posts.find(). Vamos passar para o nosso código de servidor para implementar isto:

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

Meteor.publish('comments', function(postId) {
  return Comments.find({postId: postId});
});

Meteor.publish('notifications', function() {
  return Notifications.find({userId: this.userId});
});
server/publications.js

Passando Parâmetros

O nosso código das publicações está com efeito a dizer ao servidor que este pode confiar em qualquer objecto JavaScript enviado pelo cliente (no nosso caso, {limit: postsLimit}) para servir como as options da operação de find(). Isto torna possível que utilizadores possam submeter qualquer opção que queiram através da consola do navegador.

No nosso caso, isto é relativamente segura, dado que tudo o que o utilizador pode fazer é re-ordenar artigos de forma diferente, ou mudar o limite (que é o que queríamos fazer em primeiro lugar).

No entanto esta abordagem não deve ser usada quando se estão a guardar dados privados em campos não publicadas, dado que o utilizador poderia manipular a opção fields para lhes aceder, e também se deve evitar usar esta abordagem no argumento do selector do find() pelas mesmas razões de segurança.

Uma abordagem mais segura poderia ser passar os parâmetros individuais em vez do objecto todo, para garantir que se tem controlo sobre os seus dados:

Meteor.publish('posts', function(sort, limit) {
  return Posts.find({}, {sort: sort, limit: limit});
});

Agora que estamos a subscrever ao nível da rota, também faz sentido definir o contexto de dados no mesmo lugar. Vamos afastar-nos um pouco da nossa abordagem anterior e fazer a função data devolver um objecto JavaScript em vez de simplesmente devolver um cursor. Isto permite-nos criar um contexto de dados com nome, a que vamos chamar posts.

O que isto significa é simplesmente que em vez estar implicitamente disponível com o this dentro do template, o nosso contexto de dados vai estar disponível em posts. Para além deste pequeno elemento, o código deverá ser familiar:

Router.map(function() {
  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
    },
    data: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return {
        posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
      };
    }
  });

  //..
});
lib/router.js

Agora que estamos a definir o contexto de dados ao nivel do roteador podemos com segurança vermo-nos livres do ajudante de template posts dentro do ficheiro posts_list.js. E dado que chamámos ao nosso contexto de dados posts (o mesmo nome do ajudante), nem precisamos de tocar no template postsList!

Vamos recapitular. Aqui está como o nosso código do router.js novo e melhorado deve parecer:

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  waitOn: function() { 
    return [Meteor.subscribe('notifications')]
  }
});

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?',
    waitOn: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
    },
    data: function() {
      var limit = parseInt(this.params.postsLimit) || 5; 
      return {
        posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
      };
    }
  });
});
lib/router.js

Commit 12-2

Rota postsList melhorada para receber um limite.

Vamos experimentar o nosso novo sistema de paginação. Temos agora a habilidade de mostrar um número arbitrário de artigos na página inicial simplesmente mudando o parâmetro de URL. Por exemplo, experimente aceder a http://localhost:3000/3. Deve ver algo como isto:

Controlando o número de artigos na página inicial.
Controlando o número de artigos na página inicial.

Porque não páginas?

Porque é que estamos a usar uma abordagem de “paginação infinita” em vez de mostrar páginas sucessivas com 10 artigos cada, como o Google faz com os resultados de pesquisas? Isto é na realidade devido ao paradigma de tempo real usado por Meteor.

Vamos imaginar que estamos a paginar a nossa coleção de Posts usando o formato de resultados paginados do Google, e que estamos atualmente na página 2, que mostra os artigos 10 a 20. O que acontece se outro utilizador apaga qualquer um dos 10 artigos anteriores?

Dados que a nossa aplicação é de tempo real, o nosso conjunto de dados iria mudar. O artigo 10 seria agora o artigo 9, e desapareceria de vista, enquanto que o artigo 11 estaria agora dentro do intervalo. O resultado final seria que o utilizador de repente veria os artigos mudar sem razão aparente!

Mesmo que tolerássemos este problema de usabilidade, paginação tradicional é complicada de implementar por motivos técnicos.

Vamos voltar ao nosso exemplo anterior. Nós publicamos os artigos 10 até 20 da coleção Posts, mas como iriamos encontrar esses artigos no cliente? Não se pode selecionar os artigos 10 até 20, dados que existem apenas 10 artigos no total no conjunto de dados do cliente.

Uma solução simples seria simplesmente publicar esses 10 artigos no servidor, e depois fazer um Posts.find() no cliente para apanhar todos os artigos publicados.

Isto funciona se tivermos apenas uma subscrição. Mas e se começarmos a ter mais que uma subscrição de artigos, como vai acontecer em breve?

Vamos supor que uma subscrição pede os artigos 10 até 20, e a outra pelos artigos 30 até 40. Temos agora 20 artigos carregados no cliente no total, sem forma nenhuma de saber quais pertencem a qual subscrição.

Por todas estas razões, paginação tradicional simplesmente não faz muito sentido ao trabalhar com Meteor.

Criando um Controlador de Rota

Pode ter reparado que estamos a repetir a linha var limit = parseInt(this.params.postsLimit) || 5; duas vezes. Além disso, ter o número “5” hard-coded não é exatamente ideal. Isto não é o fim do munda, mas dado que é sempre melhor seguir o principio DRY (Don’t Repeat Yourself, Não se repita a si próprio) se tal for possível, vamos refatorizar as coisas.

Vamos introduzir um novo aspeto do Iron Router, Controladores de Rota. Um controlador de rota é simplesmente uma forma de agrupar características de roteamento juntas num pacote reutilizável do qual qualquer rota pode herdar. Neste momento vamos apenas utilizá-lo numa única rota, mas vai ver no próximo capítulo como esta característica vai ser útil.

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5, 
  limit: function() { 
    return parseInt(this.params.postsLimit) || this.increment; 
  },
  findOptions: function() {
    return {sort: {submitted: -1}, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  data: function() {
    return {posts: Posts.find({}, this.findOptions())};
  }
});

Router.map(function() {
  //...

  this.route('postsList', {
    path: '/:postsLimit?',
    controller: PostsListController
  });
});
lib/router.js

Vamos passar por cada passo. Primeiro, estamos a criar o nosso controlador estendendo de RouteController. Depois definimos a propriedade template tal como fizemos antes, e depois uma nova propriedade increment.

Depois definimos uma nova função limit que vai devolver o limite atual, e uma função findOptions que vai devolver um objecto de opções. Isto pode parecer como um passo extra, mas vamos fazer uso dele mais tarde.

A seguir, definimos as nossas funções de waitOn de data tal como antes, excepto que estas vão agora usar as nossa nossa função findOptions.

Uma última coisa a fazer é dizer à rota postsList para rotear o nosso controlador novo, com a propriedade controller.

Commit 12-3

Rota postsList refatorizada num RouteController.

Adicionando um Link de Carregar Mais

Temos a paginação a funcionar, e o nosso código tem bom aspeto. Existe só um problema: não existe nenhuma forma de de facto usar essa paginação excepto alterando o URL manualmente. Isto definitivamente não é uma boa experiência de utilização, portanto vamos ao trabalho de corrigir isto.

O que queremos fazer é bastante simples. Vamos adicionar um botão de “carregar mais” no fundo da nossa lista de artigos, que vai aumentar o número de artigos atualmente mostrados por 5 cada vez que é clicado. Ou seja, se atualmente estou no URL http://localhost:3000/5, clicar “carregar mais” deve trazer-me para http://localhost:3000/10. Se chegou a este ponto no livro, confiamos que pode lidar com alguma aritmética!

Como dantes, vamos adicionar a nossa lógica de paginação no roteador. Lembra-se quando demos um nome explicito ao nosso contexto de dados em vez de simplesmente usar um cursor anonimo? Bem, não existe nenhuma regra que diga que a função data pode apenas passar cursores, por isso vamos usar a mesma técnica para gerar o URL do nosso botão “carregar mais”.

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5, 
  limit: function() { 
    return parseInt(this.params.postsLimit) || this.increment; 
  },
  findOptions: function() {
    return {sort: {submitted: -1}, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  posts: function() {
    return Posts.find({}, this.findOptions());
  },
  data: function() {
    var hasMore = this.posts().count() === this.limit();
    var nextPath = this.route.path({postsLimit: this.limit() + this.increment});
    return {
      posts: this.posts(),
      nextPath: hasMore ? nextPath : null
    };
  }
});
lib/router.js

Vamos olhar mais a fundo para este pedaço de magia de roteador. Lembre-se que a rota postsList (que vai herdar do controlador PostsListController no qual estamos atualmente a trabalhar) recebe um parâmetro postsLimit.

Assim quando nós passamos {postsLimit: this.limit() + this.increment} ao this.route.path(), estamos a dizer à rota postsList para construir o seu próprio caminho usando esse objeto JavaScript como contexto de dados.

Noutras palavras, isto é exatamente o mesmo que usar o ajudante {{pathFor 'postsList'}} do Handlebars, excepto que estamos a substituir o this implícito pelo nosso contexto de dados feito à medida.

Nós estamos a usar esse caminho e a adicioná-lo ao contexto de dados para o nosso template, mas apenas se existirem mais artigos para mostrar. A forma como fazemos isto é algo complicada.

Nós sabemos que this.limit() devolve o número atual de artigos que gostaríamos de mostrar, que pode ou ser o valor no URL atual, ou o nosso valor por omissão (5) se o URL não contém nenhum parâmetro.

Por outro lado, this.posts refere-se ao cursor atual, por isso this.posts.count() refere-se ao número de artigos que estão atualmente no cursor.

O que estamos a dizer aqui é que se pedimos por n artigos e recebemos n de volta, vamos continuar a mostrar o botão de “carregar mais”. Mas se pedimos por n e recebemos menos que n, então isto significa que atingimos o limite e que temos de parar de mostrar esse botão.

Tendo dito isto, o nosso sistema falha num caso: quando o número de itens na nossa base de dados é exatamente n. Se isso acontecer, o cliente vai pedir n artigos e receber n artigos de volta e continuar a mostrar o botão “carregar mais”, não sabendo que não existem mais itens.

Infelizmente, não existem formas simples de dar a volta a este problema, e por agora vamos ter de nos contentar com esta implementação menos-que-perfeita.

Tudo o que resta fazer é adicionar o link de “carregar mais” no funda da nossa lista de artigos, garantindo que só o mostramos se de facto existirem mais artigos para carregar:

<template name="postsList">
  <div class="posts">
    {{#each posts}}
      {{> postItem}}
    {{/each}}

    {{#if nextPath}}
      <a class="load-more" href="{{nextPath}}">Load more</a>
    {{/if}}
  </div>
</template>
client/views/posts/posts_list.html

Isto é como a sua lista de artigos deve parecer agora:

O botão de “carregar mais”.
O botão de “carregar mais”.

Commit 12-4

nextPath() foi adicionado ao controlador e é agora usado …

Uma Melhor Barra de Progresso

A nossa paginação está agora a funcionar perfeitamente, mas sofre de um problema irritante: cada vez que carregamos “carregar mais” e o roteador pede mais artigos, voltamos ao template de loading enquando esperamos que os novos dados cheguem. O resultado é que de cada vez somos enviados de novo para o topo da página e precisamos de fazer scroll até ao fundo para poder continuar a nossa navegação.

Seria muito, muito melhor se pudéssemos ficar na mesma página durante toda a operação, e ao mesmo tempo providenciar algum tipo de indicação de que novos dados estão a ser carregados. Felizmente, isto é precisamente o que o pacote iron-router-progress faz.

De forma semelhante ao Safari de iOS ou a sites como Medium e YouTube, iron-router-progress adiciona uma fina barra de carregamento ao topo do ecrã. Implementar isto é tão simples como adicionar o pacote à sua aplicação:

mrt add iron-router-progress
consola bash

Através da magina dos pacotes inteligentes, o nosso novo indicador de progresso funciona perfeitamente após instalar! A barra de progresso vai ser ativada para cada rota, e automaticamente completar assim que os dados de cada rota terminem de carregar.

Vamos fazer apenas um pequeno ajuste. Vamos desligar iron-router-progress para a rota postSubmit dado que esta não precisa de esperar por nenhuns dados de subscrição (no final de contas, é apenas um formulário vazio):

Router.map(function() {

  //...

  this.route('postSubmit', {
    path: '/submit',
    disableProgress: true
  });
});
lib/router.js

Commit 12-5

Usar o pacote iron-router-progress para fazer a paginação…

Acedendo Qualquer Artigo

Estamos atualmente a carregar os 5 mais recentes por omissão, mas o que acontece quando alguém navega para a página individual de um artigo?

Um template vazio
Um template vazio

Se tentar, vai ver um template de artigo vazio. Isto faz sentido: dissemos ao roteador para subscrever à publicação posts ao carregar a rota postsList, mas não lhe dissemos o que fazer com a rota postPage.

Mas até agora, tudo o que sabemos fazer é subscrever a uma lista do n últimos artigos. Como pedimos ao servidor por um único artigo específico? Vamos partilhar um pequeno segredo consigo: é possível ter mais que uma publicação para cada coleção!

Então para obter os nossos artigos em falta de volta, vamos simplesmente fazer uma publicação nova, separada, chamada singlePost que apenas publica um artigo, identificado pelo _id.

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

Meteor.publish('singlePost', function(id) {
  return id && Posts.find(id);
});
server/publications.js

Agora, vamos subscrever aos artigos corretos no lado do cliente. Já estamos a subscrever à publicação comments na função waitOn da rota postPage, por isso podemos simplesmente adicionar a subscrição a singlePost ai. E não nos vamos esquecer de também adicionar a nossa subscrição à rota postEdit, uma vez que esta precisa dos mesmo dados:

Router.map(function() {

  //...

  this.route('postPage', {
    path: '/posts/:_id',
    waitOn: function() {
      return [
        Meteor.subscribe('singlePost', this.params._id),
        Meteor.subscribe('comments', this.params._id)
      ];
    },
    data: function() { return Posts.findOne(this.params._id); }
  });

  this.route('postEdit', {
    path: '/posts/:_id/edit',
    waitOn: function() { 
      return Meteor.subscribe('singlePost', this.params._id);
    },
    data: function() { return Posts.findOne(this.params._id); }    
  });

  /...

});
lib/router.js

Commit 12-6

Usar uma subscrição a um único artigo para garantir que p…

Com a paginação implementada, a nossa aplicação já não sofre de problemas de escalabilidade, e os utilizadores de certeza vão contribuir com ainda mais links que antes. Não seria então bom ter uma forma qualquer de classificar esses links? Isto é precisamente o tópico do próximo capítulo, Votação.

Votação

13

Agora que o nosso site está ficando mais popular, encontrar os melhores links logo se tornará complicado. O que nós precisamos é de algum sistema de votação para ordenar nossos artigos.

Nós poderíamos construir um sistema complexo de votação com karma, decaimento de pontos baseado em tempo, e muitas outras coisas (muitas das quais foram implementadas em Telescope, o irmão mais velho do Microscope). Mas para o nosso aplicativo, nós manteremos as coisas simples e apenas avaliaremos os artigos pelo número de votos que eles receberam.

Vamos começar por dar aos usuários uma forma de votar nos artigos.

Modelo de Informação

Nós armazenaremos a lista de upvoters de cada artigo para que nós saibamos quando mostrar o botão upvote para os usuários, assim como prevenir pessoas de votar duas vezes.

Privacidade da Informação & Publicações

Nós estaremos publicando essas listas de upvoters para todos usuários, o que também tornará automaticamente toda essa informação publicamente acessível através do console do navegador.

Este é o tipo de problema de privacidade de informação que pode surgir da forma como as coleções funcionam. Por exemplo, nós queremos que pessoas sejam capazes de encontrar quem votou em seus artigos? No nosso caso fazer essa informação publicamente disponível não trará qualquer conseqüência, mas é importante ao menos reconhecer a questão.

Também note que se nós quiséssemos restringir parte desta informação, nós teríamos de garantir que o cliente não possa burlar as opções fields da nossa publicação, seja removendo essa propriedade pelo lado do servidor, ou ao não passar o objeto options completo do cliente pro servidor.

Nós também desnormalizaremos o número total de upvoters no artigo para tornar mais fácil a recepção dele. Então nós estaremos adicionando dois atributos aos nossos artigos, upvoters e votes. Vamos começar por adicioná-los ao nosso arquivo de exemplos:

// Fixture data 
if (Posts.find().count() === 0) {
  var now = new Date().getTime();

  // create two users
  var tomId = Meteor.users.insert({
    profile: { name: 'Tom Coleman' }
  });
  var tom = Meteor.users.findOne(tomId);
  var sachaId = Meteor.users.insert({
    profile: { name: 'Sacha Greif' }
  });
  var sacha = Meteor.users.findOne(sachaId);

  var telescopeId = Posts.insert({
    title: 'Introducing Telescope',
    userId: sacha._id,
    author: sacha.profile.name,
    url: 'http://sachagreif.com/introducing-telescope/',
    submitted: now - 7 * 3600 * 1000,
    commentsCount: 2,
    upvoters: [], votes: 0
  });

  Comments.insert({
    postId: telescopeId,
    userId: tom._id,
    author: tom.profile.name,
    submitted: now - 5 * 3600 * 1000,
    body: 'Interesting project Sacha, can I get involved?'
  });

  Comments.insert({
    postId: telescopeId,
    userId: sacha._id,
    author: sacha.profile.name,
    submitted: now - 3 * 3600 * 1000,
    body: 'You sure can Tom!'
  });

  Posts.insert({
    title: 'Meteor',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://meteor.com',
    submitted: now - 10 * 3600 * 1000,
    commentsCount: 0,
    upvoters: [], votes: 0
  });

  Posts.insert({
    title: 'The Meteor Book',
    userId: tom._id,
    author: tom.profile.name,
    url: 'http://themeteorbook.com',
    submitted: now - 12 * 3600 * 1000,
    commentsCount: 0,
    upvoters: [], votes: 0
  });

  for (var i = 0; i < 10; i++) {
    Posts.insert({
      title: 'Test post #' + i,
      author: sacha.profile.name,
      userId: sacha._id,
      url: 'http://google.com/?q=test-' + i,
      submitted: now - i * 3600 * 1000,
      commentsCount: 0,
      upvoters: [], votes: 0
    });
  }
}
server/fixtures.js

Como de costume, pare seu aplicativo, rode meteor reset, reinicie seu aplicativo, e crie uma nova conta de usuário. Vamos então também garantir que essas duas propriedades sejam inicializadas quando nossos artigos forem criados:

//...

// check that there are no previous posts with the same link
if (postAttributes.url && postWithSameLink) {
  throw new Meteor.Error(302, 
    'This link has already been posted', 
    postWithSameLink._id);
}

// pick out the whitelisted keys
var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
  userId: user._id, 
  author: user.username, 
  submitted: new Date().getTime(),
  commentsCount: 0,
  upvoters: [], 
  votes: 0
});

var postId = Posts.insert(post);

return postId;

//...
collections/posts.js

Construindo nossos Templates de Votação

Primeiro, nós adicionaremos um botão upvote a nossa parcial artigo:

<template name="postItem">
  <div class="post">
    <a href="#" class="upvote btn">⬆</a>
    <div class="post-content">
      <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
      <p>
        {{votes}} Votes,
        submitted by {{author}},
        <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
        {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
      </p>
    </div>
    <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
  </div>
</template>
client/views/posts/post_item.html
The upvote button
The upvote button

Em seguida, nós chamaremos um Método upvote do servidor quando o usuário clicar no botão:

//...

Template.postItem.events({
  'click .upvote': function(e) {
    e.preventDefault();
    Meteor.call('upvote', this._id);
  }
});
client/views/posts/post_item.js

Finalmente, nós voltaremos ao nosso arquivo collections/posts.js e adicionaremos o método Meteor do lado do servidor que irá upvote os artigos:

Meteor.methods({
  post: function(postAttributes) {
    //...
  },

  upvote: function(postId) {
    var user = Meteor.user();
    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to upvote");

    var post = Posts.findOne(postId);
    if (!post)
      throw new Meteor.Error(422, 'Post not found');

    if (_.include(post.upvoters, user._id))
      throw new Meteor.Error(422, 'Already upvoted this post');

    Posts.update(post._id, {
      $addToSet: {upvoters: user._id},
      $inc: {votes: 1}
    });
  }
});
collections/posts.js

Commit 13-1

Added basic upvoting algorithm.

Este Método é bem linear. Nós fazemos algumas verificações defensivas para assegurar que o usuário está logado e que o artigo realmente existe. Então nós checamos se o usuário já não votou neste artigo, e se ele não o tiver feito nós incrementamos o número total de votos e adicionamos o usuário a lista de upvoters.

Este passo final é interessante, já que nós usamos alguns operadores Mongo especiais. Há muito mais a se aprender, mas estes dois são extremamente úteis: $addToSet adiciona um ítem a uma propriedade array desde que ela já não exista, e $inc simplesmente incrementa um campo de inteiro.

Truques na Interface do Usuário

Se o usuário não está logado, ou já votou num artigo, eles não serão capazes de votar. Para refletir isso na nossa UI, nós usaremos um ajudante para condicionalmente adicionar uma classe CSS disabled ao botão upvote.

<template name="postItem">
  <div class="post">
    <a href="#" class="upvote btn {{upvotedClass}}">⬆</a>
    <div class="post-content">
      //...
  </div>
</template>
client/views/posts/post_item.html
Template.postItem.helpers({
  ownPost: function() {
    //...
  },
  domain: function() {
    //...
  },
  upvotedClass: function() {
    var userId = Meteor.userId();
    if (userId && !_.include(this.upvoters, userId)) {
      return 'btn-primary upvotable';
    } else {
      return 'disabled';
    }
  }
});

Template.postItem.events({
  'click .upvotable': function(e) {
    e.preventDefault();
    Meteor.call('upvote', this._id);
  }
});
client/views/posts/post_item.js

Nós estamos mudando nossa classe de .upvote para .upvotable, então não se esqueça de mudar o manuseador de evento clique também.

Greying out upvote buttons.
Greying out upvote buttons.

Commit 13-2

Grey out upvote link when not logged in / already voted.

Em seguida, você pode notar que artigos com um único voto estão escritos “1 votos”, então vamos cuidar de pluralizá-los corretamente. Pluralização pode ser um processo complicado, mas por hora nós faremos de uma forma razoavelmente simples. Nós faremos um ajudante Handlebars genérico que nós podemos usar em qualquer lugar:

Handlebars.registerHelper('pluralize', function(n, thing) {
  // fairly stupid pluralizer
  if (n === 1) {
    return '1 ' + thing;
  } else {
    return n + ' ' + thing + 's';
  }
});
client/helpers/handlebars.js

Os ajudantes que nós criamos antes estavam ligados a um administrador e template ao qual eles se aplicavam. Mas ao utilizar Handlebars.registerHelper, nós criamos um ajudante global que pode ser utilizado dentro de qualquer template:

<template name="postItem">
//...
<p>
  {{pluralize votes "Vote"}},
  submitted by {{author}},
  <a href="{{pathFor 'postPage'}}">{{pluralize commentsCount "comment"}}</a>
  {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
</p>
//...
</template>
client/views/posts/post_item.html
Perfecting Proper Pluralization (now say that 10 times)
Perfecting Proper Pluralization (now say that 10 times)

Commit 13-3

Added pluralize helper to format text better.

Nós devemos agora ver “1 vote”.

Algoritmo de Votação mais inteligente

Nosso código de votação está com uma cara boa, mas nós ainda podemos fazer melhor. No Método upvote, nós fazemos duas chamadas ao Mongo: um para pegar o artigo, então outra para atualizá-lo.

Há duas questões pertinentes a isso. Primeiramente, é um tanto ineficiente ir ao banco de dados duas vezes. Porém ainda mais importante, isso introduz uma condição de corrida. Nós estamos seguindo o seguinte algoritmo:

  1. Pegar um artigo do banco de dados.
  2. Checar se o usuário votou.
  3. Caso contrário, faça um voto pelo usuário.

E se o mesmo usuário votasse de novo entre as etapas 1 e 3? Nosso código atual abre a porta para o usuário ser capaz de votar no mesmo artigo duas vezes. Felizmente, Mongo nos permite ser mais inteligente e combinar as etapas 1-3 num simples comando Mongo:

Meteor.methods({
  post: function(postAttributes) {
    //...
  },

  upvote: function(postId) {
    var user = Meteor.user();
    // ensure the user is logged in
    if (!user)
      throw new Meteor.Error(401, "You need to login to upvote");

    Posts.update({
      _id: postId, 
      upvoters: {$ne: user._id}
    }, {
      $addToSet: {upvoters: user._id},
      $inc: {votes: 1}
    });
  }
});
collections/posts.js

Commit 13-4

Better upvoting algorithm.

O que nós estamos dizendo é “encontre todos os artigos com este id onde o este usuário ainda não votou, e atualize-os desta forma”. Se o usuário não votou ainda, claro que ele encontrará o artigo com essa id. Por outro lado se o usuário votou, então a consulta não encontrará nenhum documento, e consequentemente nada ocorrerá.

A única desvantagem é que agora nós não podemos dizer ao usuário que ele já votou no artigo (já que nós nos livramos da chamada ao banco de dados que checava isso). Mas eles devem saber disso pelo o botão “upvote” estar desativado na interface do usuário.

Compensação de Latência

Vamos dizer que você tentou roubar e enviou um dos seus artigos para o topo da lista modificando o número de votos:

> Posts.update(postId, {$set: {votes: 10000}});
Browser console

(onde postIdé a id de um seus artigos)

Esta tentativa descarada de burlar o sistema seria pega pela nossa callback deny() (em collections/posts.js, lembra?) e imediatamente negada.

Mas se você olhar cuidadosamente, você pode ser capaz de ver a compensação de latência em ação. Pode ser veloz, mas o artigo brevemente saltará para o topo da lista antes de voltar a sua posição.

O que aconteceu? Na nossa coleção Posts local, o update foi aplicado sem incidente. Isto ocorre instantaneamente, então o artigo disparou para o topo da lista. Enquanto isso, no servidor, o update foi negado. Então algum tempo mais tarde (contado em milissegundos se você está rodando Meteor na sua máquina), o servidor retornou um error, deixando a coleção local se reverter sozinha.

O resultado final: enquanto se espera o servidor responder, a interface do usuário não pode fazer nada além de confiar na coleção local. Assim que o servidor voltar e negar a modificação, a interface do usuário se atualiza para refletir isso.

Ranqueando os Artigos da Página Principal

Agora que nós temos uma pontuação para cada artigo baseada no número de votos, vamos mostrar uma lista dos artigos mais votados. Para tanto, nós veremos como administrar duas assinaturas separadas contra a coleção de artigo, e fazer no nosso template postsList um pouco mais geral.

Para começar, nós queremos ter duas assinaturas, uma para cada organização. O truque aqui é que ambas assinaturas irão assinar para a mesma publicação posts, porém com argumentos diferentes!

Nós também criaremos duas novas rotas chamadas newPosts e bestPosts, acessível nas URLs /new e /best respecitvamente (junto com /new/5 e /best/5 para nossa paginação, claro).

Para fazer isso, nós iremos estender nosso PostsListController em dois controladores distintos NewPostsListController e BestPostsListController. Isto nós permitirá re-utilizar a exata mesma opção de rota tanto para a rota home quanto para a rota newPosts, ao nos dar um único NewPostsListController do qual herdar. E adicionalmente, é apenas uma boa ilustração de quão flexível o Iron Router pode ser.

PostsListController = RouteController.extend({
  template: 'postsList',
  increment: 5, 
  limit: function() { 
    return parseInt(this.params.postsLimit) || this.increment; 
  },
  findOptions: function() {
    return {sort: this.sort, limit: this.limit()};
  },
  waitOn: function() {
    return Meteor.subscribe('posts', this.findOptions());
  },
  posts: function() {
    return Posts.find({}, this.findOptions());
  },
  data: function() {
    var hasMore = this.posts().fetch().length === this.limit();
    return {
      posts: this.posts(),
      nextPath: hasMore ? this.nextPath() : null
    };
  }
});

NewPostsListController = PostsListController.extend({
  sort: {submitted: -1, _id: -1},
  nextPath: function() {
    return Router.routes.newPosts.path({postsLimit: this.limit() + this.increment})
  }
});

BestPostsListController = PostsListController.extend({
  sort: {votes: -1, submitted: -1, _id: -1},
  nextPath: function() {
    return Router.routes.bestPosts.path({postsLimit: this.limit() + this.increment})
  }
});

Router.map(function() {
  this.route('home', {
    path: '/',
    controller: NewPostsListController
  });

  this.route('newPosts', {
    path: '/new/:postsLimit?',
    controller: NewPostsListController
  });

  this.route('bestPosts', {
    path: '/best/:postsLimit?',
    controller: BestPostsListController
  });
  // ..
});
lib/router.js

Note que agora que nós temos mais de uma rota, nós tiramos a lógica nextPath para fora do PostsListController e para dentro de NewPostsListController e BestPostsListController, já que o caminho será diferente em qualquer um dos casos.

Adicionalmente, quando nós organizamos por votes, nós temos uma organização secundária por timestamp submetida para assegurar que a ordem está correta.

Nós também adicionaremos links no cabeçalho:

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'home'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li>
            <a href="{{pathFor 'newPosts'}}">New</a>
          </li>
          <li>
            <a href="{{pathFor 'bestPosts'}}">Best</a>
          </li>
          {{#if currentUser}}
            <li>
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/include/header.html

Com tudo isso feito, nós agora ganhamos uma lista de melhores artigos:

Ranking by points
Ranking by points

Commit 13-5

Added routes for post lists, and pages to display them.

Um Cabeçalho Melhor

Agora que nós temos duas páginas de artigos, pode ser difícil saber qual lista você está vendo atualmente. Então vamos revisitar nosso cabeçalho para fazer isto óbvio. Nós criaremos um administrador header.js e criaremos um ajudante que usa o caminho atual e uma ou mais rotas nomeadas para configurar uma classe ativa nos nossos ítens de navegação:

A razão para nós querermos suportar múltiplas rotas nomeadas é que tanto nossas rotas home e newPosts (as quais correspondem às URLs / e /new respectivamente) chamam o mesmo template. Significando que nossa activeRouteClass deve ser inteligente o suficiente para fazer a tag <li> ativa em ambos os casos.

<template name="header">
  <header class="navbar">
    <div class="navbar-inner">
      <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </a>
      <a class="brand" href="{{pathFor 'home'}}">Microscope</a>
      <div class="nav-collapse collapse">
        <ul class="nav">
          <li class="{{activeRouteClass 'home' 'newPosts'}}">
            <a href="{{pathFor 'newPosts'}}">New</a>
          </li>
          <li class="{{activeRouteClass 'bestPosts'}}">
            <a href="{{pathFor 'bestPosts'}}">Best</a>
          </li>
          {{#if currentUser}}
            <li class="{{activeRouteClass 'postSubmit'}}">
              <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
            </li>
            <li class="dropdown">
              {{> notifications}}
            </li>
          {{/if}}
        </ul>
        <ul class="nav pull-right">
          <li>{{loginButtons}}</li>
        </ul>
      </div>
    </div>
  </header>
</template>
client/views/includes/header.html
Template.header.helpers({
  activeRouteClass: function(/* route names */) {
    var args = Array.prototype.slice.call(arguments, 0);
    args.pop();

    var active = _.any(args, function(name) {
      return Router.current().route.name === name
    });

    return active && 'active';
  }
});
client/views/includes/header.js
Showing the active page
Showing the active page

Argumentos de Ajudantes

Nós não usamos esse padrão específico até agora, mas assim como qualquer outra tag de Handlerbar, tags de templates ajudantes podem receber argumentos.

E como você pode claro passar argumentos nomeados específicos para sua função, você também pode passar um número não especificado de parâmetros anônimos e recebê-los ao chamar o objeto arguments dentro de uma função.

Neste último caso, você provavelmente gostará de converter o objeto arguments em um array JavaScript regular e então chamar pop() para se livrar da hash adicionada ao fim pelo Handlebars.

Para cada ítem de navegação, o ajudante activeRouteClass recebe uma lista de nomes de rota, e então usa o ajudante any() do Underscore para ver se alguma das rotas passa no teste (vulgo a URL correspondente ser igual ao caminho atual).

Se qualquer uma das rotas corresponder com o caminho atual, any() retornará true. Finalmente, nós estamos fazendo uso do padrão JavaScript boolen && string onde false && myString retorna false, mas true && myString retorna myString.

Commit 13-6

Added active classes to the header.

Agora que usuários podem votar em artigos em tempo real, você verá ítens pulando para cima e para baixo na nossa homepage quando os ranks mudam. Mas não seria legal se houvesse uma forma de suavizar tudo isso com algums animações no momento certo?

Publicações Avançadas

Sidebar 13.5

Agora você já deve ter entendido como publicações e assinaturas interagem. Então vamos nos livrar das rodinhas e examinar alguns cenários mais avançados.

Publicando uma Coleção Múltiplas Vezes

Na nossa primeira barra lateral sobre publicações, nós vimos alguns dos padrões mais comuns de publicação e assinatura, e nós aprendemos como a função _publishCursor os fez bem fáceis de implementar nos nossos próprios sites.

Primeiro, vamos lembrar o que _publishCursor faz para nós exatamente: ela pega todos os documentos que combinam com um dado cursor, e os puxam para a coleção do lado do cliente do mesmo nome. Note que o nome da publicação não está de forma alguma envolvido.

Isto significa que nós podemos ter mais de uma publicação ligando as versões do cliente e do servidor de qualquer coleção.

Nós já encontramos esse padrão no nosso capítulo sobre paginação, quando nós publicamos um subconjunto paginado de todos os artigos em adição ao artigo atualmente disposto.

Outro caso similar seria publicar uma visão geral de um conjunto grande de documentos, assim como os detalhes completos de um único ítem:

Publishing a collection twice
Publishing a collection twice
Meteor.publish('allPosts', function() {
  return Posts.find({}, {fields: {title: true, author: true}});
});

Meteor.publish('postDetail', function(postId) {
  return Posts.find(postId);
});

Agora quando o cliente assina essas duas publicações (usando autorun para assegurar que o postId correto está sendo enviado à assinatura postDetail), sua coleção 'posts' fica povoada por duas fontes: uma lista de títulos e nomes de autor da primeira assinatura, e os detalhes completos de um artigo da segunda.

Você pode perceber que o artigo publicado por postDetail também está sendo publicado por allPosts (apesar que apenas com um subconjunto de suas propriedades). Entretanto, o Meteor toma conta de fundir os campos e assegurar que não haja nenhum artigo duplicado.

Isto é ótimo, porque agora quando nós renderizamos a lista de resumo dos artigos, nós estamos lidando com objetos de informação que que tem a exata quantidade de informação para nós mostrarmos o que precisamos. Entretanto, quando nós renderizamos a página de um artigo único, nós temos tudo o que precisamos para mostrá-la. Claro, nós precisamos tomar conta no cliente para não se esperar todos os campos estarem disponíveis de todos os artigos neste caso – isto é uma pegadinha comum!

Deve ser notado que você não está limitado a variar as propriedades dos documentos. Você poderia normalmente publicar as mesmas propriedades em ambas publicações, mas organizar os ítens diferentemente.

Meteor.publish('newPosts', function(limit) {
  return Posts.find({}, {sort: {submitted: -1}, limit: limit});
});

Meteor.publish('bestPosts', function(limit) {
  return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
});
server/publications.js

Assinando uma Publicação Múltiplas Vezes

Nós acabamos de ver como você pode publicar uma única coleção mais de uma vez. Aliás você pode conseguir um resultado bem similar com outro padrão: criar uma única publicação, mas assiná-la múltiplas vezes.

Em Microscope, nós assinamos à publicação posts múltiplas vezes, mas Iron Router configura e desfaz cada assinatura para nós. Porém não há razão para que nós não possamos assinar múltiplas vezes simultaneamente.

Por exemplo, vamos dizer que nós gostaríamos de ler ambos os artigos mais novos e melhores na memória ao mesmo tempo:

Subscribing twice to one publication
Subscribing twice to one publication

Nós estamos configurando uma única publicação:

Meteor.publish('posts', function(options) {
  return Posts.find({}, options);
});

E nós então assinamos à publicação múltiplas vezes. Na verdade isto é mais ou menos exatamente o que estamos fazendo em Microscope:

Meteor.subscribe('posts', {submitted: -1, limit: 10});
Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});

Então o que está acontecendo exatamente? Cada navegador está abrindo duas assinaturas diferentes, cada uma conectando-se a mesma publicação do servidor.

Cada assinatura provê argumentos diferentes à publicação, mas fundamentalmente, cada vez um conjunto (diferente) de documentos é puxado da coleção posts e enviado pela rede à coleção do lado do cliente.

Múltiplas Coleções numa Única Assinatura

Diferente de bancos de dados tradicionais relacionais como MySQL que trabalham com joins (junções), bancos de dados NoSQL como o Mongo são baseados em desnormalização e embutimento. Vamos ver como isso funciona no contexto do Meteor.

Vamos ver um exemplo concreto. Nós adicionamos comentários aos nossos artigos, e até agora, nós estivemos contentes com publicar apenas os comentários no artigo único que o usuário está observando.

Entretanto, supomos que nós gostaríamos de mostrar comentário em todos os artigos da primeira página (tendo em mente que esses artigos vão mudar aos paginarmos através deles). Este cenário de uso apresenta uma boa razão para embutir comentários nos artigos, e na verdade é o que nos levou as desnormalizar a contagem de comentários.

Claro que nós poderíamos sempre apenas embutir os comentários nos artigos, nos livrando da coleção Comments de uma vez por todas. Mas como nós vimos previamente no capítulo Desnormalização, ao fazer tanto nós perderíamos alguns dos benefícios extras de se trabalhar com coleções separadas.

Mas aliás há um truque envolvendo assinaturas que torna possível embutir nossos comentários enquanto preservamos coleções separadas.

Vamos supor que junto com a nossa lista de artigos da página-inicial, nós gostaríamos de assinar a uma lista dos dois comentários superiores de cada artigo.

Seria difícil conseguir isso com uma publicação de comentários independente, especialmente se a lista de artigos fosse limitada de alguma forma (vamos dizer, os 10 mais recentes). Nós teríamos de escrever uma publicação que se parecesse com algo assim:

Two collections in one subscription
Two collections in one subscription
Meteor.publish('topComments', function(topPostIds) {
  return Comments.find({postId: topPostIds});
});

Isso seria um problema de um ponto de vista de performance, já que a publicação precisaria ser desfeita e re-estabelecida cada vez que a lista de topPostIds mudasse.

Há um jeito de se resolver isso aliás. Nós apenas usamos o fato que não só nós podemos ter mais de uma publicação por coleção, mas nós também podemos ter mais de uma coleção por publicação:

Meteor.publish('topPosts', function(limit) {
  var sub = this, commentHandles = [], postHandle = null;

  // send over the top two comments attached to a single post
  function publishPostComments(postId) {
    var commentsCursor = Comments.find({postId: postId}, {limit: 2});
    commentHandles[post._id] = 
      Meteor.Collection._publishCursor(commentsCursor, sub, 'comments');
  }

  postHandle = Posts.find({}, {limit: limit}).observeChanges({
    added: function(id, post) {
      publishPostComments(post._id);
      sub.added('posts', id, post);
    },
    changed: function(id, fields) {
      sub.changed('posts', id, fields);
    },
    removed: function(id) {
      // stop observing changes on the post's comments
      commentHandles[id] && commentHandles[id].stop();
      // delete the post
      sub.removed('posts', id);
    }
  });

  sub.ready();

  // make sure we clean everything up (note `_publishCursor`
  //   does this for us with the comment observers)
  sub.onStop(function() { postsHandle.stop(); });
});

Note que nós não estamos retornando nada nesta publicação, já que nós manualmente enviamos mensagens ao sub nós mesmos (via .added() e amigos). Então nós não precisamos pedir a _publishCursor para fazê-lo por nós ao retornar um cursor.

Agora, cada vez que nós publicamos um artigo nós também automaticamente publicamos os dois comentários superiores anexados a ele. E tudo com uma única chamada de assinatura!

Apesar do Meteor ainda não fazer esse processo bem linear, você pode também checar o pacote publish-with-relations no Atmosphere, o qual busca fazer esse padrão mais fácil.

Ligando coleções diferentes

O que mais pode nosso conhecimento recém descoberto das assinaturas fazer por nós? Bem, se nós não usamos o _publishCursor, nós não precisamos seguir as restrições que a coleção fonte no servidor precisa para ter o mesmo nome como alvo na coleção no cliente.

One collection for two subscriptions
One collection for two subscriptions

Uma razão para porque nós gostaríamos de fazer isso é Herança de Tabela Única.

Suponha que nós gostaríamos de referenciar vários tipos de objetos dos nossos artigos, cada um deles armazenando campos em comum mas também ligeiramente diferentes em conteúdo. Por exemplo, nós poderíamos estar construindo um mecanismo de blog como o Tumblr onde cada artigo possui a clássica ID, timestamp, e título; mas em adição pode também ter uma imagem, video, link, ou apenas texto.

Nós poderíamos armazenar todas esses objetos numa única coleção 'resources', usando um atributo type para indicar que tipo de objeto eles são. (video, image, link, etc.).

E apesar que nós teríamos uma única coleção Resources no servidor, nós poderíamos transformar essa única coleção em múltiplas coleções Videos, Images, etc. no cliente com o tantinho de mágica seguinte:

  Meteor.publish('videos', function() {
    var sub = this;

    var videosCursor = Resources.find({type: 'video'});
    Meteor.Collection._publishCursor(videosCursor, sub, 'videos');

    // _publishCursor doesn't call this for us in case we do this more than once.
    sub.ready();
  });

Nós estamos dizendo ao _publishCursor para publicar nossos vídeos (assim como retornar) o cursor faria, mas ao invés de publicar a coleção resources para o cliente, nós podemos publicar de 'resources' para 'videos'.

É uma boa fazer isso? Não cabe a nos julgar. Em qualquer caso, é bom saber o que é possível para se poder usar o Meteor ao máximo!

Animações

14

Nós agora temos votação, contagem e ranque em tempo real. Entretanto, isto leva a uma experiência do usuário errática e chocante já que os artigos pipocam na homepage. Nós usaremos animações para suavizar isso.

Meteor & o DOM

Antes de podermos começar a parte divertida (fazer as coisas se moverem), nós precisamos entender como o Meteor interage com o DOM (Document Object Model – a coleção de elementos HTML que produzem os conteúdos de uma página).

O ponto crucial a se manter em mente é que os elementos não podem ser movidos. Eles podem apenas ser deletados e criados (note que isto é uma limitação do próprio DOM, não do Meteor). Então para dar a ilusão dos elementos A e B estarem trocando de lugar, Meteor na verdade deletará o elemento B e inserirá uma nova cópia (B’) antes do elemento A.

Isto torna complicado fazer animações, já que não se pode simplesmente animar B até uma nova posição, porque B desaparecerá assim que o Meteor re-renderizar a página (o que sabemos que acontece instantaneamente, graças à reatividade). Então, você tem que animar o recém criado B’ ao movê-lo da posição do B antigo para sua nova posição antes de A.

Para trocar os artigos A e B (posicionados nas posições p1 e p2, respectivamente), nós seguiríamos os seguintes passos:

  1. Delete B
  2. Crie B’ antes de A no DOM
  3. Mova B’ para p2
  4. Mova A para p1
  5. Anime A para p2
  6. Anime B'para p1

O seguinte diagrama explica esses passos em mais detalhe:

Swtiching two posts
Swtiching two posts

Note que nos passos 3 e 4 nós não estamos animando A e B’ para suas posições mas “teletransportando” eles para lá instantaneamente. Já que isto é instantâneo, dará a ilusão de que B nunca foi deletado, e posicionará apropriadamente ambos elementos para serem animados de volta a suas novas posições.

Felizmente, o Meteor toma conta dos passos 1 & 2 para nós, então nós apenas precisamos nos preocupar quanto aos passos 3 ao 6.

Além disso, nos passos 5 e 6 tudo que nós estamos fazendo é mover os elementos para seus devidos lugares. Para que a única parte que nós realmente precisemos nos preocupar seja os passos 3 e 4, vulgo enviar os elementos para o ponto de início da animação.

Timing Certo

Até agora nós falamos sobre como animar nossos artigos mas não sobre quando animá-los.

Para os passos 3 e 4, a resposta é na callback de template rendered do Meteor dentro do administrador post_item.js, a qual é ativada toda vez que uma propriedade do artigo muda (no nosso caso, ranque).

Os passos 5 e 6 são um pouco mais complicados. Pense-os dessa forma: se você dissesse a um andróide perfeitamente lógico para andar para o norte por 5 minutos, e então quando isso terminar andar para o sul por 5 minutos, ele provavelmente deduziria que já que ele terminará no mesmo lugar, ele pode simplesmente salvar energia e não andar.

Então se você quer garantir que seu andróide ande durante os 10 minutos completos, você tem de esperar até que ele ande os primeiros 5 minutos, e então dizê-lo para voltar.

O navegador funciona de uma forma similar: se nós simplesmente dermos a ele as intruções simultaneamente, as novas coordenadas simplesmente substituiriam as antigas e nada aconteceria. Em outras palavras, o navegador precisa registrar as mudanças na posição como dois pontos separados no tempo, caso contrário ele não será capaz de animá-los.

O Meteor não provê uma callback justAfterRendered, mas nós podemos falsificá-la usando Meteor.defer(), a qual simplesmente recebe uma função e adia sua execução apenas o suficiente para registrá-la como um evento diferente.

Posicionamento CSS

Para animar os artigos sendo reordenados na página, nós teremos de nos aventurar em território CSS. Um review rápido de posicionamento CSS pode ser necessário.

Elementos numa página usam posicionamento estático por padrão. Elementos estaticamente posicionados se encaixam no fluxo da página, e suas coordenadas na página não podem ser mudadas ou animadas.

Posicionamento relativo por outro lado significa que o elemento também se encaixa no fluxo da página, mas pode ser posicionado relativamente a sua posição original.

Posicionamento absoluto vai um passo mais adiante e deixa você dar ao elemento coordenadas x/y específicas relativas ao documento ou ao primeiro elemento paterno posicionado absoluta -relativamente.

Nós utilizaremos posicionamento relativo para animar nossos artigos. Nós já tomamos conta das CSS para você, mas se você precisasse fazer você mesmo tudo que você precisaria fazer seria adicionar este código às suas folhas de estilo:

.post{
  position:relative;
  transition:all 300ms 0ms ease-in;
}
client/stylesheets/style.css

Isto faz os passos 5 e 6 bem fáceis: tudo que nós precisamos fazer é configurar top para 0px (seu valor padrão) e nossos artigos escorrerão de volta a suas posições “normais”.

Isto significa que nosso único desafio é descobrir para onde animá-los dos (passos 3 e 4) em relação a suas novas posições. Em outras palavras, como contrabalanceá-los. Mas isto não é muito difícil também: o contrabalanço correto é simplesmente a posição prévia do artigo menos sua nova.

Posição:absoluta

Nós poderíamos também usar position:absolute com um pai relativo para posicionar nossos elementos. Mas a grande desvantagem de elementos posicionados absolutamente é que eles estão completamente removidos do fluxo da página, causando o colapso de seu container paterno como se ele estivesse vazio.

Isso então significa que nós precisaríamos artificialmente configurar a altura do container via JavaScript, ao invés de deixar o navegador refluir os elementos naturalmente. Consequentemente, sempre que possível é melhor ficar com posicionamento relativo.

Recordação Total

Nós temos mais um problema ainda. Enquanto o elemento A persiste no DOM e pode então “lembrar” sua posição prévia, o elemento B experiencia reencarnação e volta à vida como B’ com sua memória completamente vazia.

Felizmente o Meteor vem nos resgastar nos dando acesso ao objeto instância do template na callback rendered. Como a documentação do Meteor afirma:

No corpo da callback, this é um objeto da instância do template que é único a esta ocorrência do template e persiste ao longo de re-renderizações.

Então o que nós faremos é encontrar a posição atual do artigo na página, e então armazená-la no objeto de instância do template. Desta forma, mesmo quando o artigo é deletado e recriado, nós ainda saberemos de onde nós devemos animá-lo.

Instância do template também nos deixa acessar a informação da coleção através da propriedade data. Isto será útil para pegar o ranque do artigo.

Ranqueando Artigos

Nós estivemos falando sobre ranque de artigos, mas este “ranque” não existe de fato como uma propriedade do artigo, já que é uma conseqüência da ordem na qual os artigos são listados na nossa coleção. Se nós quisermos animar os artigos de acordo com seu ranque, nós teremos que de alguma forma conjurar esta propriedade do nada.

Note que nós não podemos por essa propriedade ranque no próprio banco de dados, já que o raque é uma propriedade relativa que depende em como você organiza seus artigos (vulgo um artigo pode ser ranqueado primeiro quando organizado por data, mas terceiro quando organizado por pontos).

Nós idealmente poríamos essa propriedade nas nossas coleções newPosts e topPosts, mas o Meteor ainda não oferece um mecanismo conveniente para fazer isso.

Então, nós inseriremos rank no último passo possível, o administrador de template postList:

Template.postsList.helpers({
  postsWithRank: function() {
    this.posts.rewind();
    return this.posts.map(function(post, index, cursor) {
      post._rank = index;
      return post;
    });
  }
});
/client/views/posts/posts_list.js

Ao invés de simplesmente retornar o cursor Posts.find({}, {sort: {submitted: -1}, limit: postsHandle.limit()}) como nosso ajudante prévio posts, postsWithRank recebe o cursor e adiciona a propriedade _rank a cada um dos seus documentos.

Não esqueça de atualizar o template postsList também:

<template name="postsList">
  <div class="posts">
    {{#each postsWithRank}}
      {{> postItem}}
    {{/each}}

    {{#if nextPath}}
      <a class="load-more" href="{{nextPath}}">Load more</a>
    {{/if}}
  </div>
</template>
/client/views/posts/posts_list.html

Seja bom, Rebobine

O Meteor é um dos web frameworks mais progressistas e inovadores por aí. Mas um de seus utilitários parece um retrocesso aos dias dos VCRs e gravadores em vídeo cassete, a função rewind().

Sempre que você usa um cursor com forEach(), map() ou fetch(), você precisará rebobinar o cursor depois antes que ele esteja pronto para ser utilizado de novo.

E em alguns casos, é melhor estar do lado seguro e rewind() o cursor preventivamente ao invés de arriscar um erro.

Juntando tudo

Nós podemos agora por tudo junto ao utilizar a callback do templete rendered do administrador do post_item.js para a nossa lógica de animação:

Template.postItem.helpers({
  //...
});

Template.postItem.rendered = function(){
  // animate post from previous position to new position
  var instance = this;
  var rank = instance.data._rank;
  var $this = $(this.firstNode);
  var postHeight = 80;
  var newPosition = rank * postHeight;

  // if element has a currentPosition (i.e. it's not the first ever render)
  if (typeof(instance.currentPosition) !== 'undefined') {
    var previousPosition = instance.currentPosition;
    // calculate difference between old position and new position and send element there
    var delta = previousPosition - newPosition;
    $this.css("top", delta + "px");
  }

  // let it draw in the old position, then..
  Meteor.defer(function() {
    instance.currentPosition = newPosition;
    // bring element back to its new original position
    $this.css("top",  "0px");
  }); 
};

Template.postItem.events({
  //...
});
/client/views/posts/post_item.js

Commit 14-1

Added post reordering animation.

Não deve ser muito difícil acompanhar se você usar nosso diagrama prévio como referência.

Note que já que nós configuramos a propriedade currentPosition na instância do template na callback defer, isto significa que esta propriedade não existirá na primeira renderização do fragmento de template. Mas isto não é um problema já que nós não estamos interessados em animar a primeira renderização mesmo.

Agora abra seu site e comece a votar. Você deve agora ver os artigos gentilmente se moverem para cima e para baixo com uma graça de balé!

Animando Novos Artigos

Nossos artigos agora estão se reordenando apropriadamente, mas nós não temos realmente uma animação “new post” ainda. Ao invés de simplesmente ter novos artigos pipocando no topo da nossa lista, vamos fade them in.

Isto é na verdade mais complicado do que parece. O problema é que a callback rendered do Meteor é disparada em dois casos separados:

  1. Quando um novo template é inserido no DOM
  2. Toda vez que a informação subjacente de um template muda

Apenas o caso 1 deve ser animado, a não ser que você queria que a interface do usuário se ilumine como uma árvore de natal toda vez que a informação mudar.

Vamos assegurar que nós apenas animemos os artigos quando eles de fato são novos, e não quando eles são re-renderizados porque a informação mudou. Nós já estamos testando pela presença de uma variável de instância (a qual é apenas definida após a primeira renderização), então nós apenas precisamos voltar a nossa callback rendered e adicionar um bloco else:

Template.postItem.helpers({
  //...
});

Template.postItem.rendered = function(){
  // anima o artigo da posição prévia para a nova posição
  var instance = this;
  var rank = instance.data._rank;
  var $this = $(this.firstNode);
  var postHeight = 80;
  var newPosition = rank * postHeight;

  // se o elemento tem uma currentPosition (vulgo não é a primeira renderização)
  if (typeof(instance.currentPosition) !== 'undefined') {
    var previousPosition = instance.currentPosition;
    // calcule a diferença entra a posição antiga e a nova posição e envie o elemento para lá
    var delta = previousPosition - newPosition;
    $this.css("top", delta + "px");
  } else {
    // é a primeira renderização, então esconda o elemento
    $this.addClass("invisible");
  }

  // deixe desenhar na posição antiga, então..
  Meteor.defer(function() {
    instance.currentPosition = newPosition;
    // traga elemento de volta a sua nova posição original
    $this.css("top",  "0px").removeClass("invisible");
  }); 
};

Template.postItem.events({
  //...
});
/client/views/posts/post_item.js

Commit 14-2

Fade items in when they are drawn.

Note que a removeClass("invisible") que nós adicionamos na função defer() rodará para cada renderização. Mas apenas fará alguma coisa se a classe .invisible estiver presente no elemento, o que será verdadeiro apenas a primeira vez que for renderizado.

CSS & JavaScript

Você pode ter notado que nós estamos usando uma classe CSS .invisible para disparar a animação ao invés de animar a propriedade CSS opacity diretamente como nós fizemos para top. Isto é porque para top, nós precisavamos animar a propriedade para um valor específico que dependia da informação da instância.

Por outro lado, aqui nós apenas queremos mostrar e esconder um elemento independente de sua informação. Já que é uma boa idéia manter sua CSS longe do seu JavaScript o máximo possível, nós apenas adicionaremos e removeremos a classe aqui, e especificaremos os detalhes da animação na nossa stylesheet.

Nós devemos finalmente ter o comportamento de animação que nós queríamos. Rode seu aplicativo e experimente! E agora você também pode brincar com as classes .post e .post.invisible para ver se você consegue desenvolver outras transições. Dica: CSS easing functions é um bom lugar para começar!

Vocabulário Meteor

Sidebar 14.5

Neste livro você irá encontrar algumas palavras que podem ser novas, ou pelo menos usadas de uma forma nova no contexto de Meteor. Vamos usar este capítulo para as definir.

Cliente

Quando falamos do Cliente, estamos a referir código a correr no navegador web dos utilizadores, seja isso um navegador tradicional como o Firefox ou o Safari, ou algo tão complexo como uma UIWebView numa aplicação de iPhone nativa.

Coleção

Uma Coleção Meteor é a entidade gestora de dados que automaticamente sincroniza entre cliente e servidor. As Coleções têm um nome (como por exemplo posts), e normalmente existem tanto no cliente como no servidor. Apesar de terem comportamentos diferentes, elas têm uma API comum baseada na de Mongo.

Computação

Uma computação é um bloco de código que corre cada vez que uma fonte de dados reativa de que ela depende muda. Se tiver uma fonte de dados reativa (por exemplo, uma variável de Sessão) e quiser responder-lhe reativamente, vai ser necessário estabelecer uma computação para ela.

Cursor

Um cursor é o resultado de correr uma pesquisa numa coleção de Mongo. No cliente, um cursor não é simplesmente uma lista de resultados, mas um objecto reactivo que pode ser observado enquanto objetos na coleção relevante são adicionados, removidos ou atualizados.

DDP

DDP é o Protocolo de Dados Distribuídos (Distributed Data Protocol) de Meteor, o protocolo de baixo nível utilizado para sincronizar coleções e fazer chamadas a Métodos. O DDP foi desenhado com a intenção de ser um protocolo genérico, que substitui o HTTP para aplicações de tempo real com grandes quantidades de dados.

Deps

Deps é o sistema de reatividade do Meteor. Deps é usado internamente para manter o HTML sincronizado com o modelo de dados subjacente.

Documento

O Mongo é uma fonte de dados baseada em documentos, nesse sentido os objetos que saem das coleções são chamados “documentos”. Estes são objetos JavaScript simples (que não podem, no entanto, conter funções) com uma única propriedade especial, o _id, que é utilizado pelo Meteor para gerir as suas propriedades através do DDP.

Ajudante

Quando um template precisa de renderizar algo mais complexo que uma propriedade de um documento ele pode chamar um ajudante, uma função que é utilizada para ajudar no processo de renderizar.

Compensação de Latência

É uma técnica utilizada para permitir simular chamadas a Métodos no cliente, para evitar demoras enquanto se espera que o servidor responda.

Método

Um Método de Meteor é uma chamada a uma função remota do cliente para o servidor, com alguma lógica especial para gerir alterações nas coleções e suportar Compensação de Latência.

MiniMongo

A coleção no lado do cliente é uma base de dados em memória que disponibiliza uma API semelhante à de Mongo. A biblioteca que suporta este comportamento é chamada “MiniMongo”, para indicar que é uma versão mais pequena do Mongo que corre completamente em memória.

Pacote

Um pacote Meteor pode consistir de

  1. Código JavaScript para correr no servidor.
  2. Código JavaScript para correr no cliente.
  3. Instruções sobre como processar recursos (como SASS para CSS).
  4. Recursos para serem processados.

Um pacote é como uma biblioteca com super-poderes. O Meteor vem com um conjunto grande de pacotes base. Existe ainda o projecto Atmosphere, que é uma coleção de pacotes de terceiros criado pela comunidade.

Publicação

Uma publicação é um conjunto de dados com um nome e que é customizado por cada utilizador que o subscreve. As publicações são definidas no servidor.

Servidor

O servidor Meteor é um servidor HTTP e DDP que corre em node.js. Consiste de todas as bibliotecas de Meteor, e do código JavaScript de servidor escrito por si. Quando você inicia um servidor Meteor, este liga-se a uma base de dados Mongo (que também é iniciada automaticamente quando em desenvolvimento).

Sessão

A Sessão em Meteor é uma fonte de dados reativa no cliente que é usada pela sua aplicação para manter o estado em que o utilizador está.

Subscrição

Uma subscrição é uma ligação a uma publicação para um cliente específico. A subscrição é código que corre no navegador que comunica com uma publicação no servidor e mantém os dados sincronizados.

Template

Um template é um método de gerar HTML em JavaScript. Por omissão, Meteor suporta Handlebars, um sistema de templates sem lógica, apesar de haver planos para suportar outros no futuro.

Contexto de Dados do Template

Quando um template é renderizado, este baseia-se num objecto JavaScript que fornece dados específicos para a renderização em questão. Tipicamente tais objectos são plain-old-JavaScript-objects (POJOs), normalmente documentos de uma coleção, apesar de poderem ser mais complexos e ter funções.