À medida que os aplicativos da web modernos fazem mais e 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 jogadores neste 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 de Angular.js] [https://www.toptal.com/angular-js/a-step-by-step-guide-to-your-first-angularjs-app] abrangente, 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 .
Esta é a aparência de 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 estou divagando). 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:
Temos um longo caminho a percorrer para fazer isso funcionar, então vamos começar.
Uma das características distintivas do Ember é a grande ênfase que ele dá aos URLs. Em muitas outras estruturas, ter URLs separados para telas separadas está ausente ou foi 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. Conseqüentemente, também é a chave para entender o funcionamento interno dos aplicativos Ember.
Aqui estão as rotas para nosso aplicativo:
App.Router.map(function() { this.resource('artists', function() { this.route('songs', { path: ':slug' }); }); });
Definimos uma rota de recurso, artists
e um songs
rota aninhada dentro dela. Essa definição nos dará as seguintes rotas:
Eu usei o ótimo plugin Ember Inspector (ele existe para cromada e Raposa de fogo ) para mostrar as rotas geradas de uma forma facilmente legível. Aqui estão as regras básicas para as rotas Ember, que você pode verificar para nosso caso particular com a ajuda da tabela acima:
Existe um application
implícito | rota.
Isso é ativado para todos solicitações (transições).
Existe um index
implícito | rota.
Isso é inserido quando o usuário navega até a raiz do aplicativo.
Cada rota de recurso cria uma rota com o mesmo nome e cria implicitamente uma rota de índice abaixo dela.
Esta rota de índice é ativada quando o usuário navega para a rota. Em nosso caso, artists.index
é acionado quando o usuário navega para /artists
.
Uma rota aninhada simples (sem recurso) terá seu nome de rota primária como seu prefixo.
A rota que definimos como this.route('songs', ...)
terá artists.songs
como seu nome. Ele é acionado quando o usuário navega para /artists/pearl-jam
ou /artists/radiohead
.
Se o caminho não for fornecido, ele será considerado igual ao nome da rota.
Se o caminho contiver um :
, é considerado um segmento dinâmico .
O nome atribuído a ele (no nosso caso, slug
) corresponderá ao valor no segmento apropriado da url. O slug
segmento acima terá o valor pearl-jam
, radiohead
ou qualquer outro valor que foi extraído do URL.
Em uma primeira etapa, construiremos 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 de design abrangente do Ember: convenção sobre configuração . Na seção acima, vimos que /artists
ativa o artists
rota. Por convenção, o nome dessa rota objeto é ArtistsRoute
. É responsabilidade desse 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; } });
Neste snippet, 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 uma matriz que podemos exibir posteriormente. No entanto, as responsabilidades da rota não se estendem a fornecer lógica de exibição, que é tratada pelo controlador. Vamos dar uma olhada.
Hmmm - na verdade, não precisamos definir o controlador neste momento! Ember é inteligente o suficiente para gerar o controlador quando necessário e defina o controlador M.odel
atributo para o valor de retorno do próprio gancho do modelo, ou seja, a lista de artistas. (Novamente, isso é o resultado do paradigma de 'convenção sobre configuração'.) Podemos descer uma camada e criar um modelo para exibir a lista:
{{#each model}} {{#link-to 'artists.songs' this class='list-group-item artist-link'}} {{name}} {{/link-to}} {{/each}} {{outlet}}
Se isso parece familiar, é porque o Ember.js usa Guidão modelos, que têm uma sintaxe e auxiliares muito simples, mas não permitem lógica não trivial (por exemplo, termos ORing ou ANDing em uma condicional).
No modelo acima, iteramos através do 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 ao artists.songs
rota para esse artista. O link contém o nome do artista. O #each
helper em Handlebars altera o escopo dentro dele para o item atual, então {{name}}
sempre se referirá ao nome do artista que está atualmente sob iteração.
Outro ponto de interesse no trecho acima: {{outlet}}
, que especifica slots no modelo onde o conteúdo pode ser renderizado. Ao aninhar rotas, o modelo para a rota externa de recurso é renderizado primeiro, seguido pela rota interna, que renderiza o conteúdo do modelo no {{outlet}}
definido pela rota externa. Isso é exatamente o que acontece aqui.
Por convenção, todas as rotas renderizam seu conteúdo no modelo de mesmo nome. Acima, o data-template-name
atributo do modelo acima é artists
o que significa que será renderizado para a rota externa, artists
. Ele especifica uma saída para o conteúdo do painel direito, em que a rota interna, artists.index
renderiza seu conteúdo:
Select an artist.
Em resumo, uma rota (artists
) renderiza seu conteúdo na barra lateral esquerda, seu modelo sendo a lista de artistas. Outra rota, artists.index
renderiza seu próprio conteúdo no slot fornecido pelo artists
modelo. Ele poderia buscar alguns dados para servir como seu modelo, mas, neste caso, tudo o que queremos exibir é texto estático, então não precisamos fazer isso.
Em seguida, queremos ser capazes de criar artistas, não apenas olhar para uma lista chata.
Quando mostrei que artists
template que renderiza a lista de artistas, trapacei um pouco. Cortei a parte superior para me concentrar no que é importante. Agora, vou adicionar de volta:
{{input type='text' class='new-artist' placeholder='New Artist' value=newName}} Add ...
Usamos um auxiliar Ember, input
, com tipo de texto para renderizar uma entrada de texto simples. Nele, nós ligar o valor da entrada de texto para newName
propriedade do controlador que faz backup deste modelo, ArtistsController
. Por conseqüência, quando a propriedade value da entrada muda (em outras palavras, quando o usuário digita texto nela) o newName
propriedade no controlador será mantida em sincronia.
Também informamos que o createArtist
a ação deve ser disparada quando o botão é clicado. Finalmente, vinculamos a propriedade disabled do botão ao disabled
propriedade 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-se do que eu disse sobre vinculações? Tente alterar newName
e veja como é refletido como o texto no campo de entrada.)
disabled
é implementado de forma que, quando não houver texto na caixa de entrada, ela retornará true
e assim o botão será desabilitado. O .property
chamada no final torna isso uma “propriedade computada”, outra fatia deliciosa do bolo de brasa.
Propriedades computadas são propriedades que dependem de outras propriedades, que podem ser “normais” ou calculadas. O Ember armazena em cache o valor desses até que uma das propriedades dependentes mude. Em seguida, ele recalcula o valor da propriedade computada e o armazena em cache novamente.
Aqui está uma representação visual do processo acima. Para resumir: quando o usuário insere o nome de um artista, o newName
atualizações de propriedades, seguidas de disabled
propriedade e, por fim, o nome do artista é adicionado à lista.
Pense nisso por um momento. Com a ajuda de ligações e propriedades computadas, podemos estabelecer (modelar) dados como o única fonte de verdade . Acima, uma mudança no nome do novo artista aciona uma mudança na propriedade do controlador, que por sua vez aciona uma mudança na propriedade disabled. 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 vantagem 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 da verdade, mas não vão tão longe quanto Ember ou deixam de fazer um trabalho tão completo. Angular, por exemplo, tem ligações bidirecionais - mas não tem propriedades calculadas. 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ê deseja aprender mais sobre o assunto, recomendo que leia Postagem do blog do eviltrout para uma versão mais curta ou esta questão Quora para uma discussão mais longa na qual os principais desenvolvedores de ambos os lados participam.
Vamos voltar para ver como o createArtist
a ação é criada depois de 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ção precisam ser agrupados em um actions
objeto e pode ser definido na rota, controlador ou vista. Escolhi defini-lo na rota aqui porque o resultado da ação não se limita ao controlador, mas sim, “global”.
Não há nada sofisticado acontecendo aqui. Depois que o back-end nos informa que a operação de salvamento foi concluída com sucesso, fazemos três coisas, em ordem:
newName
vinculativo, evitando que tenhamos que manipular o DOM diretamente.artists.songs
), passando no artista recém-criado como o modelo para essa rota. transitionTo
é a maneira de se mover entre as rotas internamente. (O auxiliar link-to
serve para fazer isso por meio da ação do usuário.)Podemos exibir as músicas de um artista clicando no nome do artista. Passamos também no artista que vai se tornar o modelo do novo roteiro. Se o objeto do modelo for transmitido dessa forma, o model
o gancho da rota não será chamado, pois não há necessidade de resolver o modelo.
A rota ativa aqui é artists.songs
e, portanto, o controlador e o modelo serão ArtistsSongsController
e artists/songs
, respectivamente. Já vimos como o template é renderizado no outlet fornecido pelo artists
modelo para que possamos nos concentrar apenas no modelo em questão:
(...) {{#each songs}} {{title}} {{view App.StarRating maxRating=5}} {{/each}}
Observe que eliminei o código para criar uma nova música, pois seria exatamente o mesmo que para criar um novo artista.
O songs
propriedade é 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, é suficiente sabermos que cada música tem um título e uma classificação.
O título é exibido diretamente no modelo e a avaliação é representada por estrelas, por meio do StarRating
Visão. Vamos ver isso agora.
A avaliaçã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 seu controlador. Isso significa que eles podem ler e modificar suas propriedades. Isso está em contraste com outro bloco de construção do Ember, os componentes, que são controles isolados e reutilizáveis com acesso apenas ao que foi passado para eles. (Também poderíamos usar um componente de avaliação com estrelas neste exemplo.)
Vamos ver como a visualização exibe o número de estrelas e define a avaliaçã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 calculadas que discutimos anteriormente com disabled
propriedade do ArtistsController
. Acima, usei uma chamada macro de propriedade computada, cerca de uma dúzia das quais são definidas no Ember. Eles tornam as propriedades típicas computadas mais sucintas e menos sujeitas a erros (para gravação). Eu defino rating
para ser a classificação do contexto (e, portanto, da música), enquanto eu defini tanto o fullStars
e numStars
propriedades para que possam ser lidas melhor no contexto do widget de classificação por estrelas.
O stars
método é a atração principal. Ele retorna uma matriz de dados para as estrelas em que cada item contém um rating
propriedade (de 1 a 5) e uma bandeira (full
) para indicar se a estrela está cheia. Isso torna extremamente simples percorrê-los no modelo:
{{#each view.stars}}
{{/each}} Este snippet contém vários pontos importantes:
each
helper designa que ele usa uma propriedade de visualização (em oposição a uma propriedade no controlador) prefixando o nome da propriedade com view
.class
atributo da tag span tem classes dinâmicas e estáticas atribuídas. Qualquer coisa com o prefixo :
torna-se uma classe estática, enquanto full:glyphicon-star:glyphicon-star-empty
notação é como um operador ternário em JavaScript: se a propriedade completa for verdadeira, a primeira classe deve ser atribuída; se não, o segundo.setRating
a ação deve ser disparada - mas o Ember irá procurá-la na vista, não na rota ou controlador, como no caso da criação de um novo artista.A ação é assim definida na visualização:
App.StarRating = Ember.View.extend({ (...) actions: { setRating: function() { var newRating = $(event.target).data('rating'); this.set('rating', newRating); } } });
Obtemos a classificação de rating
Atributo de dados que atribuímos no modelo e então o definimos como rating
para a 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 é deixada como um exercício para o leitor motivado.
Nós provamos vários ingredientes do referido bolo de brasa:
Lindo, não é?
Há muito mais em Ember do que eu poderia caber neste post sozinho. Se você gostaria de ver uma série de screencast sobre como eu construí uma versão um pouco mais desenvolvida do aplicativo acima e / ou aprender mais sobre o Ember, você pode inscreva-se na minha lista de discussão para obter artigos ou dicas semanalmente.
Espero ter aguçado seu apetite para aprender mais sobre Ember.js e que você vá muito além do aplicativo de amostra que usei nesta postagem. Conforme você continua a aprender sobre o Ember.js, certifique-se de dar uma olhada em nosso artigo sobre Ember Data para aprender a usar a biblioteca de dados ember . Divirta-se construindo!
Relacionado: Ember.js e os 8 erros mais comuns cometidos pelos desenvolvedores