Les 8 erreurs les plus courantes commises par les développeurs Ember.js
Publié: 2022-03-11Ember.js est un cadre complet pour la création d'applications complexes côté client. L'un de ses principes est «la convention plutôt que la configuration», et la conviction qu'il existe une très grande partie du développement commune à la plupart des applications Web, et donc une seule meilleure façon de résoudre la plupart de ces défis quotidiens. Cependant, trouver la bonne abstraction et couvrir tous les cas demande du temps et la contribution de toute la communauté. Selon le raisonnement, il est préférable de prendre le temps de trouver la bonne solution au problème central, puis de l'intégrer au cadre, au lieu de baisser les bras et de laisser tout le monde se débrouiller seul lorsqu'il a besoin de trouver une solution.
Ember.js évolue constamment pour rendre le développement encore plus facile. Mais, comme pour tout framework avancé, il existe encore des pièges dans lesquels les développeurs Ember peuvent tomber. Avec le post suivant, j'espère fournir une carte pour y échapper. Allons droit au but !
Erreur courante n° 1 : s'attendre à ce que le hook de modèle se déclenche lorsque tous les objets de contexte sont transmis
Supposons que nous ayons les routes suivantes dans notre application :
Router.map(function() { this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
La route de band
a un segment dynamique, id
. Lorsque l'application est chargée avec une URL telle que /bands/24
, 24
est transmis au hook de model
de la route correspondante, band
. Le hook de modèle a pour rôle de désérialiser le segment pour créer un objet (ou un tableau d'objets) qui pourra ensuite être utilisé dans le modèle :
// app/routes/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); // params.id is '24' } });
Jusqu'ici tout va bien. Cependant, il existe d'autres moyens d'entrer des itinéraires que de charger l'application à partir de la barre de navigation du navigateur. L'un d'eux utilise le link-to
assistant des modèles. L'extrait suivant parcourt une liste de bandes et crée un lien vers leurs itinéraires de band
respectifs :
{{#each bands as |band|}} {{link-to band.name "band" band}} {{/each}}
Le dernier argument de link-to, band
, est un objet qui remplit le segment dynamique de la route, et ainsi son id
devient le segment id de la route. Le piège dans lequel beaucoup de gens tombent est que le crochet du modèle n'est pas appelé dans ce cas, puisque le modèle est déjà connu et a été transmis. Cela a du sens et cela pourrait sauver une requête au serveur mais ce n'est, certes, pas intuitif. Un moyen ingénieux de contourner cela consiste à transmettre non pas l'objet lui-même, mais son identifiant :
{{#each bands as |band|}} {{link-to band.name "band" band.id}} {{/each}}
Plan d'atténuation d'Ember
Les composants routables arriveront bientôt sur Ember, probablement en version 2.1 ou 2.2. Lorsqu'ils atterrissent, le crochet du modèle sera toujours appelé, quelle que soit la manière dont on passe à une route avec un segment dynamique. Lisez le RFC correspondant ici.
Erreur courante n° 2 : oublier que les contrôleurs pilotés par les routes sont des singletons
Les routes dans Ember.js configurent les propriétés sur les contrôleurs qui servent de contexte pour le modèle correspondant. Ces contrôleurs sont des singletons et par conséquent, tout état défini sur eux persiste même lorsque le contrôleur n'est plus actif.
C'est quelque chose qui est très facile à oublier et je suis tombé là-dedans aussi. Dans mon cas, j'avais une application de catalogue de musique avec des groupes et des chansons. L'indicateur songCreationStarted
sur le contrôleur de songs
indique que l'utilisateur a commencé à créer une chanson pour un groupe particulier. Le problème était que si l'utilisateur passait ensuite à un autre groupe, la valeur de songCreationStarted
persistait et il semblait que la chanson à moitié terminée était pour l'autre groupe, ce qui était déroutant.
La solution consiste à réinitialiser manuellement les propriétés du contrôleur que nous ne voulons pas conserver. Un endroit possible pour le faire est le hook setupController
de la route correspondante, qui est appelé sur toutes les transitions après le hook afterModel
(qui, comme son nom l'indique, vient après le hook model
) :
// app/routes/band.js export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); controller.set('songCreationStarted', false); } });
Plan d'atténuation d'Ember
Encore une fois, l'avènement des composants routables résoudra ce problème, mettant fin aux contrôleurs. L'un des avantages des composants routables est qu'ils ont un cycle de vie plus cohérent et qu'ils sont toujours détruits lors de la transition hors de leurs itinéraires. Quand ils arriveront, le problème ci-dessus disparaîtra.
Erreur courante n° 3 : ne pas appeler l'implémentation par défaut dans setupController
Les routes dans Ember ont une poignée de crochets de cycle de vie pour définir le comportement spécifique à l'application. Nous avons déjà vu model
qui est utilisé pour récupérer des données pour le template correspondant et setupController
, pour configurer le contrôleur, le contexte du template.
Ce dernier, setupController
, a une valeur par défaut sensible, qui affecte le modèle, à partir du hook de model
, en tant que propriété de model
du contrôleur :
// ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }
( context
est le nom utilisé par le package ember-routing
pour ce que j'appelle le model
ci-dessus)
Le hook setupController
peut être remplacé à plusieurs fins, comme la réinitialisation de l'état du contrôleur (comme dans l'erreur commune n ° 2 ci-dessus). Cependant, si l'on oublie d'appeler l'implémentation parente que j'ai copiée ci-dessus dans Ember.Route, on peut se retrouver dans une longue session de casse-tête, car le contrôleur n'aura pas sa propriété de model
définie. Alors appelez toujours this._super(controller, model)
:
export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); // put the custom setup here } });
Plan d'atténuation d'Ember
Comme indiqué précédemment, les contrôleurs, et avec eux, le crochet setupController
, vont bientôt disparaître, donc ce piège ne sera plus une menace. Cependant, il y a une plus grande leçon à apprendre ici, qui est d'être conscient des implémentations dans les ancêtres. La fonction init
, définie dans Ember.Object
, la mère de tous les objets dans Ember, est un autre exemple auquel vous devez faire attention.
Erreur courante n° 4 : utiliser this.modelFor
avec des routes non parentes
Le routeur Ember résout le modèle pour chaque segment de route lorsqu'il traite l'URL. Supposons que nous ayons les routes suivantes dans notre application :
Router.map({ this.route('bands', function() { this.route('band', { path: ':id' }, function() { this.route('songs'); }); }); });
Étant donné une URL de /bands/24/songs
, le hook de model
de bands
, bands.band
puis bands.band.songs
sont appelés, dans cet ordre. L'API de route a une méthode particulièrement pratique, modelFor
, qui peut être utilisée dans les routes enfants pour récupérer le modèle de l'une des routes parentes, car ce modèle a sûrement été résolu à ce stade.
Par exemple, le code suivant est un moyen valide de récupérer l'objet band dans la route 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); } });
Une erreur courante, cependant, consiste à utiliser un nom de route dans modelFor qui n'est pas un parent de la route. Si les itinéraires de l'exemple ci-dessus ont été légèrement modifiés :
Router.map({ this.route('bands'); this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
Notre méthode pour récupérer la bande désignée dans l'URL échouerait, car la route des bands
n'est plus un parent et donc son modèle n'a pas été résolu.
// 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! } });
La solution consiste à utiliser modelFor
uniquement pour les routes parentes et à utiliser d'autres moyens pour récupérer les données nécessaires lorsque modelFor
ne peut pas être utilisé, comme la récupération à partir du magasin.
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); } });
Erreur courante n° 5 : Confondre le contexte sur lequel une action de composant est déclenchée
Les composants imbriqués ont toujours été l'une des parties les plus difficiles d'Ember à raisonner. Avec l'introduction des paramètres de bloc dans Ember 1.10, une grande partie de cette complexité a été soulagée, mais dans de nombreuses situations, il est toujours difficile de voir en un coup d'œil sur quel composant une action, déclenchée à partir d'un composant enfant, sera déclenchée.
Supposons que nous ayons un composant band-list
band-list-items
, et nous pouvons marquer chaque bande comme favorite dans la liste.
// app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band faveAction="setAsFavorite"}} {{/each}}
Le nom de l'action qui doit être invoquée lorsque l'utilisateur clique sur le bouton est transmis au composant band-list-item
et devient la valeur de sa propriété faveAction
.

Voyons maintenant le modèle et la définition des composants 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')); } } });
Lorsque l'utilisateur clique sur le bouton "Fave this", l'action faveBand
est déclenchée, ce qui déclenche la faveAction
du composant qui a été transmise ( setAsFavorite
, dans le cas ci-dessus), sur son composant parent , band-list
.
Cela dérange beaucoup de gens car ils s'attendent à ce que l'action soit déclenchée de la même manière que les actions des modèles pilotés par les routes, sur le contrôleur (puis bouillonnant sur les routes actives). Ce qui aggrave la situation, c'est qu'aucun message d'erreur n'est enregistré ; le composant parent avale simplement l'erreur.
La règle générale est que les actions sont déclenchées sur le contexte actuel. Dans le cas des modèles non-composants, ce contexte est le contrôleur actuel, tandis que dans le cas des modèles de composants, il s'agit du composant parent (s'il y en a un), ou encore du contrôleur actuel si le composant n'est pas imbriqué.
Ainsi, dans le cas ci-dessus, le composant band-list
devrait relancer l'action reçue de band-list-item
afin de la faire remonter au contrôleur ou à la route.
// app/components/band-list.js export default Ember.Component.extend({ bands: [], favoriteAction: 'setFavoriteBand', actions: { setAsFavorite: function(band) { this.sendAction('favoriteAction', band); } } });
Si la band-list
de bandes était définie dans le modèle de bands
, l'action setFavoriteBand
devrait être gérée dans le contrôleur de bands
ou la route des bands
(ou l'une de ses routes parentes).
Plan d'atténuation d'Ember
Vous pouvez imaginer que cela devient plus complexe s'il y a plus de niveaux d'imbrication (par exemple, en ayant un composant fav-button
dans band-list-item
). Vous devez percer un trou à travers plusieurs couches de l'intérieur pour faire passer votre message, en définissant des noms significatifs à chaque niveau ( setAsFavorite
, favoriteAction
, faveAction
, etc.)
Ceci est rendu plus simple par le "Amélioré Actions RFC", qui est déjà disponible sur la branche master, et sera probablement inclus dans 1.13.
L'exemple ci-dessus serait alors simplifié en :
// 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>
Erreur courante n° 6 : utiliser les propriétés du tableau comme clés dépendantes
Les propriétés calculées d'Ember dépendent d'autres propriétés, et cette dépendance doit être explicitement définie par le développeur. Supposons que nous ayons une propriété isAdmin
qui devrait être vraie si et seulement si l'un des rôles est admin
. Voici comment on pourrait l'écrire :
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles')
Avec la définition ci-dessus, la valeur de isAdmin
n'est invalidée que si l'objet tableau roles
lui-même change, mais pas si des éléments sont ajoutés ou supprimés au tableau existant. Il existe une syntaxe spéciale pour définir que les ajouts et les suppressions doivent également déclencher un recalcul :
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]')
Erreur courante n° 7 : ne pas utiliser de méthodes conviviales pour les observateurs
Étendons l'exemple (maintenant corrigé) de l'erreur commune n° 6 et créons une classe User dans notre application.
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.[]') });
Lorsque nous ajoutons le rôle admin
à un tel User
, nous sommes surpris :
var user = User.create(); user.get('isAdmin'); // => false user.get('roles').push('admin'); user.get('isAdmin'); // => false ?
Le problème est que les observateurs ne se déclencheront pas (et donc les propriétés calculées ne seront pas mises à jour) si les méthodes Javascript standard sont utilisées. Cela pourrait changer si l'adoption globale d' Object.observe
dans les navigateurs s'améliore, mais jusque-là, nous devons utiliser l'ensemble de méthodes fournies par Ember. Dans le cas actuel, pushObject
est l'équivalent convivial de push
:
user.get('roles').pushObject('admin'); user.get('isAdmin'); // => true, finally!
Erreur courante n° 8 : mutation transmise dans les propriétés des composants
Imaginez que nous ayons un composant star-rating
qui affiche l'évaluation d'un élément et permet de définir l'évaluation de l'élément. La note peut être pour une chanson, un livre ou la compétence de dribble d'un joueur de football.
Vous l'utiliseriez comme ceci dans votre modèle :
{{#each songs as |song|}} {{star-rating item=song rating=song.rating}} {{/each}}
Supposons en outre que le composant affiche des étoiles, une étoile pleine pour chaque point, puis des étoiles vides, jusqu'à une note maximale. Lorsqu'une étoile est cliquée, une action set
est déclenchée sur le contrôleur et doit être interprétée comme l'utilisateur souhaitant mettre à jour la note. Nous pourrions écrire le code suivant pour y parvenir :
// 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(); } } });
Cela permettrait de faire le travail, mais cela pose quelques problèmes. Tout d'abord, il suppose que l'élément transmis a une propriété de rating
, et nous ne pouvons donc pas utiliser ce composant pour gérer la compétence de dribble de Leo Messi (où cette propriété pourrait être appelée score
).
Deuxièmement, il modifie la note de l'élément dans le composant. Cela conduit à des scénarios où il est difficile de voir pourquoi une certaine propriété change. Imaginez que nous ayons un autre composant dans le même modèle où cette note est également utilisée, par exemple, pour calculer le score moyen du joueur de football.
Le slogan pour atténuer la complexité de ce scénario est « Data down, actions up » (DDAU). Les données doivent être transmises (de la route au contrôleur aux composants), tandis que les composants doivent utiliser des actions pour informer leur contexte des modifications de ces données. Alors, comment DDAU devrait-il être appliqué ici ?
Ajoutons un nom d'action qui doit être envoyé pour mettre à jour la note :
{{#each songs as |song|}} {{star-rating item=song rating=song.rating setAction="updateRating"}} {{/each}}
Et puis utilisez ce nom pour envoyer l'action :
// 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 }); } } });
Enfin, l'action est gérée en amont, par le contrôleur ou la route, et c'est là que la note de l'item est mise à jour :
// 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(); } } });
Lorsque cela se produit, ce changement est propagé vers le bas via la liaison transmise au composant star-rating
, et le nombre d'étoiles pleines affichées change en conséquence.
De cette façon, la mutation ne se produit pas dans les composants, et puisque la seule partie spécifique à l'application est la gestion de l'action dans la route, la réutilisabilité du composant n'en souffre pas.
Nous pourrions tout aussi bien utiliser le même composant pour les compétences de football :
{{#each player.skills as |skill|}} {{star-rating item=skill rating=skill.score setAction="updateSkill"}} {{/each}}
Derniers mots
Il est important de noter que certaines (la plupart ?) des erreurs que j'ai vues commettre (ou que j'ai moi-même commises), y compris celles que j'ai écrites ici, vont disparaître ou être grandement atténuées au début de la série 2.x de Ember.js.
Ce qui reste est traité par mes suggestions ci-dessus, donc une fois que vous développez dans Ember 2.x, vous n'aurez plus aucune excuse pour faire d'autres erreurs ! Si vous souhaitez cet article au format pdf, rendez-vous sur mon blog et cliquez sur le lien en bas de l'article.
À propos de moi
Je suis arrivé dans le monde du front-end avec Ember.js il y a deux ans, et je suis là pour rester. Je suis devenu si enthousiaste à propos d'Ember que j'ai commencé à bloguer intensément à la fois dans des articles d'invités et sur mon propre blog, ainsi qu'à faire des présentations lors de conférences. J'ai même écrit un livre, Rock and Roll with Ember.js , pour tous ceux qui veulent apprendre Ember. Vous pouvez télécharger un exemple de chapitre ici.