Os 8 erros mais comuns que os desenvolvedores do Ember.js cometem
Publicados: 2022-03-11Ember.js é uma estrutura abrangente para criar aplicativos complexos do lado do cliente. Um de seus princípios é “convenção sobre configuração” e a convicção de que há uma grande parte do desenvolvimento comum à maioria dos aplicativos da Web e, portanto, uma única maneira melhor de resolver a maioria desses desafios diários. No entanto, encontrar a abstração correta e cobrir todos os casos exige tempo e informações de toda a comunidade. De acordo com o raciocínio, é melhor dedicar um tempo para obter a solução para o problema central e depois colocá-la na estrutura, em vez de jogar as mãos para o alto e deixar que todos se defendam quando precisam encontrar uma solução.
O Ember.js está em constante evolução para tornar o desenvolvimento ainda mais fácil. Mas, como em qualquer estrutura avançada, ainda existem armadilhas em que os desenvolvedores do Ember podem cair. Com o seguinte post, espero fornecer um mapa para evitá-los. Vamos pular direto!
Erro comum nº 1: esperar que o gancho do modelo seja acionado quando todos os objetos de contexto são passados
Vamos supor que temos as seguintes rotas em nossa aplicação:
Router.map(function() { this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
A rota de band
tem um segmento dinâmico, id
. Quando o aplicativo é carregado com uma URL como /bands/24
, 24
é passado para o gancho do model
da rota correspondente, band
. O gancho do modelo tem a função de desserializar o segmento para criar um objeto (ou um array de objetos) que pode ser usado no template:
// app/routes/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); // params.id is '24' } });
Até agora tudo bem. No entanto, existem outras maneiras de inserir rotas além de carregar o aplicativo na barra de navegação do navegador. Um deles é usar o link-to
helper de templates. O trecho a seguir percorre uma lista de bandas e cria um link para suas respectivas rotas de band
:
{{#each bands as |band|}} {{link-to band.name "band" band}} {{/each}}
O último argumento para link-to, band
, é um objeto que preenche o segmento dinâmico da rota e, portanto, seu id
se torna o segmento de id da rota. A armadilha em que muitas pessoas caem é que o gancho do modelo não é chamado nesse caso, já que o modelo já é conhecido e foi passado. Faz sentido e pode salvar uma solicitação para o servidor, mas, reconhecidamente, não é intuitivo. Uma maneira engenhosa de contornar isso é passar, não o objeto em si, mas seu id:
{{#each bands as |band|}} {{link-to band.name "band" band.id}} {{/each}}
Plano de Mitigação da Ember
Componentes roteáveis chegarão ao Ember em breve, provavelmente na versão 2.1 ou 2.2. Quando eles pousam, o gancho do modelo sempre será chamado, não importa como se faça a transição para uma rota com um segmento dinâmico. Leia o RFC correspondente aqui.
Erro comum nº 2: esquecer que os controladores orientados por rota são singletons
Rotas em Ember.js configuram propriedades em controladores que servem como contexto para o modelo correspondente. Esses controladores são singletons e, consequentemente, qualquer estado definido neles persiste mesmo quando o controlador não está mais ativo.
Isso é algo que é muito fácil de ignorar e eu tropecei nisso também. No meu caso, eu tinha um aplicativo de catálogo de músicas com bandas e músicas. O sinalizador songCreationStarted
no controlador de songs
indicava que o usuário começou a criar uma música para uma determinada banda. O problema era que, se o usuário mudasse para outra banda, o valor de songCreationStarted
persistia e parecia que a música semi-acabada era para a outra banda, o que era confuso.
A solução é redefinir manualmente as propriedades do controlador que não queremos demorar. Um lugar possível para fazer isso é o gancho setupController
da rota correspondente, que é chamado em todas as transições após o gancho afterModel
(que, como o próprio nome sugere, vem depois do gancho model
):
// app/routes/band.js export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); controller.set('songCreationStarted', false); } });
Plano de Mitigação da Ember
Mais uma vez, o surgimento de componentes roteáveis resolverá esse problema, pondo fim aos controladores por completo. Uma das vantagens dos componentes roteáveis é que eles têm um ciclo de vida mais consistente e sempre são derrubados ao fazer a transição para longe de suas rotas. Quando eles chegarem, o problema acima desaparecerá.
Erro comum nº 3: não chamar a implementação padrão no setupController
As rotas no Ember têm um punhado de ganchos de ciclo de vida para definir o comportamento específico do aplicativo. Já vimos model
que é usado para buscar dados para o template correspondente e setupController
, para configurar o controller, o contexto do template.
Este último, setupController
, tem um padrão sensato, que é atribuir o modelo, a partir do gancho do model
, como a propriedade do model
do controlador:
// ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }
( context
é o nome usado pelo pacote ember-routing
para o que chamo de model
acima)
O gancho setupController
pode ser substituído para vários propósitos, como redefinir o estado do controlador (como no erro comum nº 2 acima). No entanto, se alguém esquecer de chamar a implementação pai que copiei acima no Ember.Route, pode-se ter uma longa sessão de coçar a cabeça, pois o controlador não terá sua propriedade de model
definida. Então sempre chame this._super(controller, model)
:
export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); // put the custom setup here } });
Plano de Mitigação da Ember
Como afirmado anteriormente, os controladores e, com eles, o gancho setupController
, desaparecerão em breve, portanto, essa armadilha não será mais uma ameaça. No entanto, há uma lição maior a ser aprendida aqui, que é estar atento às implementações em ancestrais. A função init
, definida em Ember.Object
, a mãe de todos os objetos em Ember, é outro exemplo que você deve observar.
Erro comum nº 4: usando this.modelFor
com rotas não-pais
O roteador Ember resolve o modelo para cada segmento de rota à medida que processa a URL. Vamos supor que temos as seguintes rotas em nossa aplicação:
Router.map({ this.route('bands', function() { this.route('band', { path: ':id' }, function() { this.route('songs'); }); }); });
Dada uma URL de /bands/24/songs
, o gancho do model
de bands
, bands.band
e então bands.band.songs
são chamados, nesta ordem. A API de rota tem um método particularmente útil, modelFor
, que pode ser usado em rotas filhas para buscar o modelo de uma das rotas pai, pois esse modelo certamente foi resolvido até esse ponto.
Por exemplo, o código a seguir é uma maneira válida de buscar o objeto band na rota bands.band
:
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { var bands = this.modelFor('bands'); return bands.filterBy('id', params.id); } });
Um erro comum, no entanto, é usar um nome de rota em modelFor que não seja pai da rota. Se as rotas do exemplo acima foram ligeiramente alteradas:
Router.map({ this.route('bands'); this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
Nosso método para buscar a banda designada na URL seria interrompido, pois a rota de bands
não é mais pai e, portanto, seu modelo não foi resolvido.
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { var bands = this.modelFor('bands'); // `bands` is undefined return bands.filterBy('id', params.id); // => error! } });
A solução é usar modelFor
apenas para rotas pai e usar outros meios para recuperar os dados necessários quando modelFor
não puder ser usado, como buscar no armazenamento.
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); } });
Erro comum nº 5: confundir o contexto em que uma ação de componente é acionada
Componentes aninhados sempre foram uma das partes mais difíceis de raciocinar do Ember. Com a introdução de parâmetros de bloco no Ember 1.10, muito dessa complexidade foi aliviada, mas em muitas situações, ainda é difícil ver rapidamente em qual componente uma ação, disparada de um componente filho, será acionada.
Vamos supor que temos um componente band-list
que contém band-list-items
e podemos marcar cada banda como favorita na lista.
// app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band faveAction="setAsFavorite"}} {{/each}}
O nome da ação que deve ser invocada quando o usuário clica no botão é passado para o componente band-list-item
e se torna o valor de sua propriedade faveAction
.

Vamos agora ver o template e a definição do componente de band-list-item
:
// app/templates/components/band-list-item.hbs <div class="band-name">{{band.name}}</div> <button class="fav-button" {{action "faveBand"}}>Fave this</button>
// app/components/band-list-item.js export default Ember.Component.extend({ band: null, faveAction: '', actions: { faveBand: { this.sendAction('faveAction', this.get('band')); } } });
Quando o usuário clica no botão “Fave this”, a ação faveBand
é acionada, que dispara a faveAction
do componente que foi passado ( setAsFavorite
, no caso acima), em seu componente pai , band-list
.
Isso atrapalha muitas pessoas, pois elas esperam que a ação seja disparada da mesma maneira que as ações dos modelos orientados por rota são, no controlador (e, em seguida, borbulhando nas rotas ativas). O que torna isso pior é que nenhuma mensagem de erro é registrada; o componente pai apenas engole o erro.
A regra geral é que as ações são disparadas no contexto atual. No caso de templates não componentes, esse contexto é o controlador atual, enquanto no caso de templates de componentes, é o componente pai (se houver um), ou novamente o controlador atual se o componente não estiver aninhado.
Portanto, no caso acima, o componente band-list
teria que disparar novamente a ação recebida do band-list-item
para borbulhar para o controlador ou rota.
// app/components/band-list.js export default Ember.Component.extend({ bands: [], favoriteAction: 'setFavoriteBand', actions: { setAsFavorite: function(band) { this.sendAction('favoriteAction', band); } } });
Se a band-list
de bandas foi definida no template de bands
, então a ação setFavoriteBand
teria que ser tratada no controlador de bands
ou na rota de bands
(ou em uma de suas rotas pai).
Plano de Mitigação da Ember
Você pode imaginar que isso fica mais complexo se houver mais níveis de aninhamento (por exemplo, tendo um componente fav-button
dentro band-list-item
). Você precisa perfurar várias camadas de dentro para fora para transmitir sua mensagem, definindo nomes significativos em cada nível ( setAsFavorite
, favoriteAction
, faveAction
, etc.)
Isso é simplificado pelo “RFC de Ações Melhoradas”, que já está disponível no branch master, e provavelmente será incluído no 1.13.
O exemplo acima seria então simplificado para:
// app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band setFavBand=(action "setFavoriteBand")}} {{/each}}
// app/templates/components/band-list-item.hbs <div class="band-name">{{band.name}}</div> <button class="fav-button" {{action "setFavBand" band}}>Fave this</button>
Erro comum nº 6: usando propriedades de matriz como chaves dependentes
As propriedades computadas do Ember dependem de outras propriedades, e essa dependência precisa ser definida explicitamente pelo desenvolvedor. Digamos que temos uma propriedade isAdmin
que deve ser verdadeira se e somente se uma das funções for admin
. É assim que se pode escrever:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles')
Com a definição acima, o valor de isAdmin
só é invalidado se o próprio objeto da matriz de roles
for alterado, mas não se os itens forem adicionados ou removidos da matriz existente. Existe uma sintaxe especial para definir que adições e remoções também devem acionar um recálculo:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]')
Erro comum nº 7: não usar métodos amigáveis ao observador
Vamos estender o exemplo (agora corrigido) do erro comum nº 6 e criar uma classe User em nosso aplicativo.
var User = Ember.Object.extend({ initRoles: function() { var roles = this.get('roles'); if (!roles) { this.set('roles', []); } }.on('init'), isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]') });
Quando adicionamos a função de admin
a um User
, temos uma surpresa:
var user = User.create(); user.get('isAdmin'); // => false user.get('roles').push('admin'); user.get('isAdmin'); // => false ?
O problema é que os observadores não serão acionados (e, portanto, as propriedades computadas não serão atualizadas) se os métodos Javascript padrão forem usados. Isso pode mudar se a adoção global do Object.observe
nos navegadores melhorar, mas até lá, temos que usar o conjunto de métodos que o Ember fornece. No caso atual, pushObject
é o equivalente amigável de push
para observadores:
user.get('roles').pushObject('admin'); user.get('isAdmin'); // => true, finally!
Erro Comum Nº 8: Mutação Passada em Propriedades em Componentes
Imagine que temos um componente star-rating
que exibe a classificação de um item e permite a configuração da classificação do item. A classificação pode ser para uma música, um livro ou habilidade de drible de um jogador de futebol.
Você usaria assim em seu modelo:
{{#each songs as |song|}} {{star-rating item=song rating=song.rating}} {{/each}}
Vamos supor ainda que o componente exiba estrelas, uma estrela cheia para cada ponto e estrelas vazias depois disso, até uma classificação máxima. Quando uma estrela é clicada, uma ação set
é acionada no controlador e deve ser interpretada como o usuário que deseja atualizar a classificação. Poderíamos escrever o seguinte código para conseguir isso:
// app/components/star-rating.js export default Ember.Component.extend({ item: null, rating: 0, (...) actions: { set: function(newRating) { var item = this.get('item'); item.set('rating', newRating); return item.save(); } } });
Isso faria o trabalho, mas há alguns problemas com isso. Primeiro, ele assume que o item passado tem uma propriedade de rating
e, portanto, não podemos usar esse componente para gerenciar a habilidade de drible de Leo Messi (onde essa propriedade pode ser chamada score
).
Em segundo lugar, altera a classificação do item no componente. Isso leva a cenários em que é difícil ver por que uma determinada propriedade é alterada. Imagine que temos outro componente no mesmo modelo onde essa classificação também é usada, por exemplo, para calcular a pontuação média do jogador de futebol.
O slogan para mitigar a complexidade desse cenário é “Data down, actions up” (DDAU). Os dados devem ser transmitidos (da rota ao controlador para os componentes), enquanto os componentes devem usar ações para notificar seu contexto sobre alterações nesses dados. Então, como o DDAU deve ser aplicado aqui?
Vamos adicionar um nome de ação que deve ser enviado para atualização da classificação:
{{#each songs as |song|}} {{star-rating item=song rating=song.rating setAction="updateRating"}} {{/each}}
E então use esse nome para enviar a ação:
// app/components/star-rating.js export default Ember.Component.extend({ item: null, rating: 0, (...) actions: { set: function(newRating) { var item = this.get('item'); this.sendAction('setAction', { item: this.get('item'), rating: newRating }); } } });
Por fim, a ação é tratada a montante, pelo controlador ou pela rota, e é aqui que a classificação do item é atualizada:
// app/routes/player.js export default Ember.Route.extend({ actions: { updateRating: function(params) { var skill = params.item, rating = params.rating; skill.set('score', rating); return skill.save(); } } });
Quando isso acontece, essa alteração é propagada para baixo através da ligação passada para o componente star-rating
, e o número de estrelas completas exibidas muda como resultado.
Dessa forma, a mutação não acontece nos componentes, e como a única parte específica do aplicativo é a manipulação da ação na rota, a reutilização do componente não sofre.
Também poderíamos usar o mesmo componente para habilidades de futebol:
{{#each player.skills as |skill|}} {{star-rating item=skill rating=skill.score setAction="updateSkill"}} {{/each}}
Palavras finais
É importante notar que alguns (a maioria?) dos erros que eu vi as pessoas cometerem (ou eu mesmo), incluindo aqueles sobre os quais escrevi aqui, vão desaparecer ou ser bastante atenuados no início da série 2.x de Ember.js.
O que resta é abordado pelas minhas sugestões acima, então uma vez que você esteja desenvolvendo em Ember 2.x, você não terá desculpa para cometer mais erros! Se você quiser este artigo em pdf, vá até o meu blog e clique no link na parte inferior do post.
Sobre mim
Cheguei ao mundo do front-end com o Ember.js há dois anos e estou aqui para ficar. Fiquei tão entusiasmado com o Ember que comecei a blogar intensamente tanto em guest posts quanto em meu próprio blog, além de apresentar em conferências. Eu até escrevi um livro, Rock and Roll with Ember.js , para quem quer aprender Ember. Você pode baixar um capítulo de amostra aqui.