Um guia para criar seu primeiro aplicativo Ember.js
Publicados: 2022-03-11À medida que os aplicativos da Web modernos fazem cada vez mais no lado do cliente (o próprio fato de agora nos referirmos a eles como "aplicativos da Web" em oposição a "sites da Web" é bastante revelador), tem havido um interesse crescente em estruturas do lado do cliente . Existem muitos players nesse campo, mas para aplicativos com muitas funcionalidades e muitas partes móveis, dois deles se destacam em particular: Angular.js e Ember.js.
Já publicamos um [tutorial Angular.js] abrangente][https://www.toptal.com/angular-js/a-step-by-step-guide-to-your-first-angularjs-app], então nós Vamos focar em Ember.js neste post, no qual construiremos um aplicativo Ember simples para catalogar sua coleção de músicas. Você será apresentado aos principais blocos de construção da estrutura e terá um vislumbre de seus princípios de design. Se você quiser ver o código-fonte durante a leitura, ele está disponível como rock-and-roll no Github.
O que vamos construir?
Veja como será nosso aplicativo Rock & Roll em sua versão final:
À esquerda, você verá que temos uma lista de artistas e, à direita, uma lista de músicas do artista selecionado (você também pode ver que tenho bom gosto musical, mas discordo). Novos artistas e músicas podem ser adicionados simplesmente digitando na caixa de texto e pressionando o botão adjacente. As estrelas ao lado de cada música servem para avaliá-la, à la iTunes.
Poderíamos dividir a funcionalidade rudimentar do aplicativo nas seguintes etapas:
- Clicar em 'Adicionar' adiciona um novo artista à lista, com um nome especificado no campo 'Novo artista' (o mesmo vale para músicas de um determinado artista).
- Esvaziar o campo 'Novo Artista' desativa o botão 'Adicionar' (o mesmo vale para músicas de um determinado artista).
- Clicar no nome de um artista lista suas músicas à direita.
- Clicar nas estrelas classifica uma determinada música.
Temos um longo caminho a percorrer para que isso funcione, então vamos começar.
Rotas: a chave para o aplicativo Ember.js
Uma das características distintivas do Ember é a forte ênfase que ele coloca em URLs. Em muitas outras estruturas, ter URLs separados para telas separadas está faltando ou é adicionado como uma reflexão tardia. No Ember, o roteador – o componente que gerencia urls e transições entre eles – é a peça central que coordena o trabalho entre os blocos de construção. Consequentemente, também é a chave para entender o funcionamento interno dos aplicativos Ember.
Aqui estão as rotas para a nossa aplicação:
App.Router.map(function() { this.resource('artists', function() { this.route('songs', { path: ':slug' }); }); });
Definimos uma rota de recursos, artists
e uma rota de songs
aninhadas dentro dela. Essa definição nos dará as seguintes rotas:
Eu usei o ótimo plugin Ember Inspector (existe tanto para Chrome quanto para Firefox) para mostrar as rotas geradas de forma fácil de ler. Aqui estão as regras básicas para rotas Ember, que você pode verificar para o nosso caso particular com a ajuda da tabela acima:
Há uma rota de
application
implícita.Isso é ativado para todas as solicitações (transições).
Há uma rota
index
implícita.Isso é inserido quando o usuário navega para a raiz do aplicativo.
Cada rota de recurso cria uma rota com o mesmo nome e cria implicitamente uma rota de índice abaixo dela.
Essa rota de índice é ativada quando o usuário navega para a rota. No nosso caso,
artists.index
é acionado quando o usuário navega para/artists
.Uma rota aninhada simples (sem recursos) terá seu nome de rota pai como seu prefixo.
A rota que definimos como
this.route('songs', ...)
teráartists.songs
como nome. Ele é acionado quando o usuário navega para/artists/pearl-jam
ou/artists/radiohead
.Se o caminho não for fornecido, supõe-se que seja igual ao nome da rota.
Se o caminho contiver um
:
, ele será considerado um segmento dinâmico .O nome atribuído a ele (no nosso caso,
slug
) corresponderá ao valor no segmento apropriado da url. O segmentoslug
acima terá o valorpearl-jam
,radiohead
ou qualquer outro valor que foi extraído do URL.
Exibir a lista de artistas
Como primeiro passo, vamos construir a tela que exibe a lista de artistas à esquerda. Esta tela deve ser mostrada aos usuários quando eles navegam para /artists/
:
Para entender como essa tela é renderizada, é hora de apresentar outro princípio abrangente de design do Ember: convenção sobre configuração . Na seção acima, vimos que /artists
ativa a rota dos artists
. Por convenção, o nome desse objeto de rota é ArtistsRoute
. É responsabilidade deste objeto de rota buscar dados para o aplicativo renderizar. Isso acontece no gancho do modelo da rota:
App.ArtistsRoute = Ember.Route.extend({ model: function() { var artistObjects = []; Ember.$.getJSON('http://localhost:9393/artists', function(artists) { artists.forEach(function(data) { artistObjects.pushObject(App.Artist.createRecord(data)); }); }); return artistObjects; } });
Nesse trecho, os dados são buscados por meio de uma chamada XHR do back-end e, após a conversão em um objeto de modelo, enviados para um array que podemos exibir posteriormente. No entanto, as responsabilidades da rota não se estendem ao fornecimento de lógica de exibição, que é tratada pelo controlador. Vamos dar uma olhada.
Hmmm—na verdade, não precisamos definir o controlador neste momento! O Ember é inteligente o suficiente para gerar o controlador quando necessário e definir o atributo M.odel
do controlador para o valor de retorno do próprio gancho do modelo, ou seja, a lista de artistas. (Novamente, isso é resultado do paradigma 'convenção sobre configuração'.) Podemos diminuir uma camada e criar um modelo para exibir a lista:
<script type="text/x-handlebars" data-template-name="artists"> <div class="col-md-4"> <div class="list-group"> {{#each model}} {{#link-to "artists.songs" this class="list-group-item artist-link"}} {{name}} <span class="pointer glyphicon glyphicon-chevron-right"></span> {{/link-to}} {{/each}} </div> </div> <div class="col-md-8"> <div class="list-group"> {{outlet}} </div> </div> </script>
Se isso parece familiar, é porque o Ember.js usa modelos Handlebars, que têm uma sintaxe e auxiliares muito simples, mas não permitem lógica não trivial (por exemplo, termos OR ou AND em uma condicional).
No modelo acima, iteramos pelo modelo (configurado anteriormente pela rota para um array que contém todos os artistas) e para cada item nele, renderizamos um link que nos leva à rota artists.songs
desse artista. O link contém o nome do artista. O #each
helper no Handlebars altera o escopo dentro dele para o item atual, então {{name}}
sempre se referirá ao nome do artista que está atualmente em iteração.
Rotas aninhadas para visualizações aninhadas
Outro ponto de interesse no snippet acima: {{outlet}}
, que especifica os slots no modelo onde o conteúdo pode ser renderizado. Ao aninhar rotas, o modelo para a rota de recurso externa é renderizado primeiro, seguido pela rota interna, que renderiza seu conteúdo de modelo no {{outlet}}
definido pela rota externa. É exatamente isso que acontece aqui.
Por convenção, todas as rotas renderizam seu conteúdo no modelo de mesmo nome. Acima, o atributo data-template-name
do modelo acima é artists
, o que significa que ele será renderizado para a rota externa, artists
. Ele especifica uma saída para o conteúdo do painel direito, no qual a rota interna, artists.index
renderiza seu conteúdo:
<script type="text/x-handlebars" data-template-name="artists/index"> <div class="list-group-item empty-list"> <div class="empty-message"> Select an artist. </div> </div> </script>
Em resumo, uma rota ( artists
) renderiza seu conteúdo na barra lateral esquerda, sendo seu modelo a lista de artistas. Outra rota, artists.index
renderiza seu próprio conteúdo no slot fornecido pelo template de artists
. Ele pode buscar alguns dados para servir como modelo, mas neste caso tudo o que queremos exibir é texto estático, então não precisamos.
Crie um artista
Parte 1: vinculação de dados
Em seguida, queremos ser capazes de criar artistas, não apenas olhar para uma lista chata.
Quando mostrei aquele template de artists
que renderiza a lista de artistas, trapaceei um pouco. Eu cortei a parte de cima para focar no que é importante. Agora, vou adicionar isso de volta:
<script type="text/x-handlebars" data-template-name="artists"> <div class="col-md-4"> <div class="list-group"> <div class="list-group-item"> {{input type="text" class="new-artist" placeholder="New Artist" value=newName}} <button class="btn btn-primary btn-sm new-artist-button" {{action "createArtist"}} {{bind-attr disabled=disabled}}>Add</button> </div> < this is where the list of artists is rendered > ... </script>
Usamos um auxiliar Ember, input
, com tipo text para renderizar uma entrada de texto simples. Nele, vinculamos o valor da entrada de texto à propriedade newName
do controlador que faz backup desse modelo, ArtistsController
. Por consequência, quando a propriedade value da entrada muda (em outras palavras, quando o usuário digita texto nela), a propriedade newName
no controlador será mantida em sincronia.
Também informamos que a ação createArtist
deve ser acionada quando o botão for clicado. Por fim, vinculamos a propriedade disabled do botão à propriedade disabled
do controlador. Então, como é o controlador?
App.ArtistsController = Ember.ArrayController.extend({ newName: '', disabled: function() { return Ember.isEmpty(this.get('newName')); }.property('newName') });
newName
é definido como vazio no início, o que significa que a entrada de texto ficará em branco. (Lembra o que eu disse sobre ligações? Tente alterar newName
e veja-o refletido como o texto no campo de entrada.)

disabled
é implementado de forma que quando não houver texto na caixa de entrada, ele retornará true
e, portanto, o botão será desativado. A chamada .property
no final torna esta uma “propriedade computada”, outra fatia deliciosa do bolo Ember.
Propriedades computadas são propriedades que dependem de outras propriedades, que podem ser “normais” ou computadas. Ember armazena em cache o valor destes até que uma das propriedades dependentes seja alterada. Em seguida, ele recalcula o valor da propriedade computada e a armazena em cache novamente.
Aqui está uma representação visual do processo acima. Resumindo: quando o usuário insere o nome de um artista, a propriedade newName
atualizada, seguida da propriedade disabled
e, por fim, o nome do artista é adicionado à lista.
Desvio: Uma única fonte de verdade
Pense nisso por um momento. Com a ajuda de associações e propriedades computadas, podemos estabelecer (modelar) dados como a única fonte de verdade . Acima, uma alteração no nome do novo artista aciona uma alteração na propriedade do controlador, que por sua vez aciona uma alteração na propriedade desabilitada. Conforme o usuário começa a digitar o nome do novo artista, o botão fica habilitado, como que por mágica.
Quanto maior o sistema, mais alavancagem ganhamos com o princípio da 'fonte única da verdade'. Ele mantém nosso código limpo e robusto, e nossas definições de propriedade, mais declarativas.
Algumas outras estruturas também enfatizam que os dados do modelo sejam a única fonte de verdade, mas não vão tão longe quanto o Ember ou falham em fazer um trabalho tão completo. Angular, por exemplo, tem ligações bidirecionais, mas não possui propriedades computadas. Ele pode “emular” propriedades computadas por meio de funções simples; o problema aqui é que ele não tem como saber quando atualizar uma “propriedade computada” e, portanto, recorre à verificação suja e, por sua vez, leva a uma perda de desempenho, especialmente notável em aplicativos maiores.
Se você quiser saber mais sobre o tópico, eu recomendo que você leia o post do blog do eviltrout para uma versão mais curta ou esta pergunta do Quora para uma discussão mais longa na qual os principais desenvolvedores de ambos os lados pesam.
Parte 2: gerenciadores de ação
Vamos voltar para ver como a ação createArtist
é criada após ser disparada (após pressionar o botão):
App.ArtistsRoute = Ember.Route.extend({ ... actions: { createArtist: function() { var name = this.get('controller').get('newName'); Ember.$.ajax('http://localhost:9393/artists', { type: 'POST', dataType: 'json', data: { name: name }, context: this, success: function(data) { var artist = App.Artist.createRecord(data); this.modelFor('artists').pushObject(artist); this.get('controller').set('newName', ''); this.transitionTo('artists.songs', artist); }, error: function() { alert('Failed to save artist'); } }); } } });
Os manipuladores de ações precisam ser agrupados em um objeto de actions
e podem ser definidos na rota, no controlador ou na visualização. Optei por defini-lo na rota aqui porque o resultado da ação não se limita ao controlador, mas sim, “global”.
Não há nada extravagante acontecendo aqui. Depois que o back-end nos informou que a operação de salvamento foi concluída com sucesso, fazemos três coisas, em ordem:
- Adicione o novo artista ao modelo do modelo (todos os artistas) para que ele seja renderizado novamente e o novo artista apareça como o último item da lista.
- Limpe o campo de entrada por meio da ligação
newName
, evitando que tenhamos que manipular o DOM diretamente. - Transição para uma nova rota (
artists.songs
), passando o artista recém-criado como modelo para aquela rota.transitionTo
é a maneira de se mover entre as rotas internamente. (O auxiliarlink-to
serve para fazer isso por meio da ação do usuário.)
Exibir músicas para um artista
Podemos exibir as músicas de um artista clicando no nome do artista. Passamos também o artista que vai se tornar o modelo da nova rota. Se o objeto do modelo for assim passado, o gancho do model
da rota não será chamado, pois não há necessidade de resolver o modelo.
A rota ativa aqui é artists.songs
e, portanto, o controller e o template serão ArtistsSongsController
e artists/songs
, respectivamente. Já vimos como o modelo é renderizado na saída fornecida pelo modelo de artists
para que possamos nos concentrar apenas no modelo em mãos:
<script type="text/x-handlebars" data-template-name="artists/songs"> (...) {{#each songs}} <div class="list-group-item"> {{title}} {{view App.StarRating maxRating=5}} </div> {{/each}} </script>
Observe que retirei o código para criar uma nova música, pois seria exatamente o mesmo que para criar um novo artista.
A propriedade de songs
é configurada em todos os objetos de artista a partir dos dados retornados pelo servidor. O mecanismo exato pelo qual isso é feito tem pouco interesse para a discussão atual. Por enquanto, basta saber que cada música tem um título e uma classificação.
O título é exibido diretamente no modelo e a classificação é representada por estrelas, por meio da visualização StarRating
. Vamos ver isso agora.
Widget de classificação por estrelas
A classificação de uma música fica entre 1 e 5 e é mostrada ao usuário por meio de uma visualização, App.StarRating
. As visualizações têm acesso ao seu contexto (neste caso, a música) e ao seu controlador. Isso significa que eles podem ler e modificar suas propriedades. Isso contrasta com outro bloco de construção Ember, componentes, que são controles isolados e reutilizáveis com acesso apenas ao que foi passado para eles. (Poderíamos usar um componente de classificação por estrelas neste exemplo também.)
Vamos ver como a visualização exibe o número de estrelas e define a classificação da música quando o usuário clica em uma das estrelas:
App.StarRating = Ember.View.extend({ classNames: ['rating-panel'], templateName: 'star-rating', rating: Ember.computed.alias('context.rating'), fullStars: Ember.computed.alias('rating'), numStars: Ember.computed.alias('maxRating'), stars: function() { var ratings = []; var fullStars = this.starRange(1, this.get('fullStars'), 'full'); var emptyStars = this.starRange(this.get('fullStars') + 1, this.get('numStars'), 'empty'); Array.prototype.push.apply(ratings, fullStars); Array.prototype.push.apply(ratings, emptyStars); return ratings; }.property('fullStars', 'numStars'), starRange: function(start, end, type) { var starsData = []; for (i = start; i <= end; i++) { starsData.push({ rating: i, full: type === 'full' }); }; return starsData; }, (...) });
rating
, fullStars
e numStars
são propriedades computadas que discutimos anteriormente com a propriedade disabled
do ArtistsController
. Acima, usei a chamada macro de propriedades computadas, cerca de uma dúzia das quais são definidas em Ember. Eles tornam as propriedades computadas típicas mais sucintas e menos propensas a erros (para escrever). Eu defini a rating
para ser a classificação do contexto (e, portanto, da música), enquanto defini as propriedades fullStars
e numStars
para que elas leiam melhor no contexto do widget de classificação por estrelas.
O método das stars
é a principal atração. Ele retorna uma matriz de dados para as estrelas em que cada item contém uma propriedade de rating
(de 1 a 5) e um sinalizador ( full
) para indicar se a estrela está cheia. Isso torna extremamente simples percorrê-los no modelo:
<script type="text/x-handlebars" data-template-name="star-rating"> {{#each view.stars}} <span {{bind-attr data-rating=rating}} {{bind-attr class=":star-rating :glyphicon full:glyphicon-star:glyphicon-star-empty"}} {{action "setRating" target=view}}> </span> {{/each}} </script>
Este trecho contém vários pontos de observação:
- Primeiro,
each
auxiliar designa que ele usa uma propriedade view (em oposição a uma propriedade no controlador) prefixando o nome da propriedade comview
. - Em segundo lugar, o atributo
class
da tag span tem classes dinâmicas e estáticas atribuídas. Qualquer coisa prefixada por um:
torna-se uma classe estática, enquanto afull:glyphicon-star:glyphicon-star-empty
é como um operador ternário em JavaScript: se a propriedade full for verdadeira, a primeira classe deve ser atribuída; se não, o segundo. - Finalmente, quando a tag é clicada, a ação
setRating
deve ser acionada - mas o Ember a procurará na visualização, não na rota ou no controlador, como no caso de criar um novo artista.
A ação é assim definida na visão:
App.StarRating = Ember.View.extend({ (...) actions: { setRating: function() { var newRating = $(event.target).data('rating'); this.set('rating', newRating); } } });
Obtemos a classificação do atributo de dados de rating
que atribuímos no modelo e definimos isso como a rating
da música. Observe que a nova classificação não é mantida no back-end. Não seria difícil implementar essa funcionalidade com base em como criamos um artista e fica como exercício para o leitor motivado.
Envolvendo tudo
Provamos vários ingredientes do bolo Ember acima mencionado:
- Vimos como as rotas são o cerne dos aplicativos Ember e como elas servem como base para convenções de nomenclatura.
- Vimos como as associações de dados bidirecionais e as propriedades computadas tornam nossos dados de modelo a única fonte de verdade e nos permitem evitar a manipulação direta do DOM.
- E vimos como disparar e manipular ações de várias maneiras e construir uma visualização personalizada para criar um controle que não faz parte do nosso HTML.
Bonito, não é?
Lendo mais (e assistindo)
Há muito mais em Ember do que eu poderia colocar neste post sozinho. Se você quiser ver uma série de screencasts sobre como eu construí uma versão um pouco mais desenvolvida do aplicativo acima e/ou saber mais sobre o Ember, você pode se inscrever na minha lista de discussão para receber artigos ou dicas semanalmente.
Espero ter aguçado seu apetite para aprender mais sobre o Ember.js e que você vá muito além do aplicativo de exemplo que usei neste post. À medida que você continua aprendendo sobre Ember.js, não deixe de dar uma olhada em nosso artigo sobre Ember Data para saber como usar a biblioteca ember-data. Divirta-se construindo!