Rotas

5

Porcentagem Traduzida

Neste capítulo, você irá:

  • Aprenda sobre rotas no Meteor.
  • Crie uma página de artigos de discussão, com URLs únicas.
  • Aprenda como linkar propriamente essas URLs.
  • 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.