Die 8 häufigsten Fehler, die Ember.js-Entwickler machen
Veröffentlicht: 2022-03-11Ember.js ist ein umfassendes Framework zum Erstellen komplexer clientseitiger Anwendungen. Einer ihrer Grundsätze ist „Konvention vor Konfiguration“ und die Überzeugung, dass die meisten Webanwendungen einen sehr großen Teil der Entwicklung gemeinsam haben und daher einen einzigen besten Weg, um die meisten dieser alltäglichen Herausforderungen zu lösen. Die richtige Abstraktion zu finden und alle Fälle abzudecken, erfordert jedoch Zeit und den Input der gesamten Community. Wie die Argumentation lautet, ist es besser, sich die Zeit zu nehmen, um die Lösung für das Kernproblem richtig zu finden, und sie dann in den Rahmen einzubacken, anstatt die Hände hochzuwerfen und jeden für sich selbst sorgen zu lassen, wenn er eine Lösung finden muss.
Ember.js wird ständig weiterentwickelt, um die Entwicklung noch einfacher zu machen. Aber wie bei jedem fortgeschrittenen Framework gibt es immer noch Fallstricke, in die Ember-Entwickler tappen können. Mit dem folgenden Beitrag hoffe ich, eine Karte bereitzustellen, um diese zu umgehen. Lass uns gleich einsteigen!
Häufiger Fehler Nr. 1: Erwarten, dass der Modell-Hook ausgelöst wird, wenn alle Kontextobjekte übergeben werden
Nehmen wir an, wir haben die folgenden Routen in unserer Anwendung:
Router.map(function() { this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
Die band
hat ein dynamisches Segment, id
. Wenn die App mit einer URL wie /bands/24
geladen wird, wird 24
an den model
Hook der entsprechenden Route, band
, übergeben. Der Modell-Hook hat die Aufgabe, das Segment zu deserialisieren, um ein Objekt (oder ein Array von Objekten) zu erstellen, das dann in der Vorlage verwendet werden kann:
// app/routes/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); // params.id is '24' } });
So weit, ist es gut. Es gibt jedoch andere Möglichkeiten, Routen einzugeben, als die Anwendung über die Navigationsleiste des Browsers zu laden. Einer von ihnen verwendet den link-to
Helfer aus Vorlagen. Der folgende Ausschnitt geht durch eine Liste von Bands und erstellt einen Link zu ihren jeweiligen band
:
{{#each bands as |band|}} {{link-to band.name "band" band}} {{/each}}
Das letzte Argument für link-to, band
, ist ein Objekt, das das dynamische Segment für die Route ausfüllt, und somit wird seine id
zum ID-Segment für die Route. Die Falle, in die viele Leute tappen, ist, dass der Modell-Hook in diesem Fall nicht aufgerufen wird, da das Modell bereits bekannt ist und übergeben wurde. Es macht Sinn und spart möglicherweise eine Anfrage an den Server, ist es aber zugegebenermaßen nicht intuitiv. Ein genialer Weg, das zu umgehen, besteht darin, nicht das Objekt selbst, sondern seine ID zu übergeben:
{{#each bands as |band|}} {{link-to band.name "band" band.id}} {{/each}}
Embers Minderungsplan
Routingfähige Komponenten werden in Kürze zu Ember kommen, wahrscheinlich in Version 2.1 oder 2.2. Wenn sie landen, wird immer der Modellhaken gerufen, egal wie man auf eine Route mit einem dynamischen Segment übergeht. Lesen Sie hier den entsprechenden RFC.
Häufiger Fehler Nr. 2: Vergessen, dass routengesteuerte Controller Singletons sind
Routen in Ember.js richten Eigenschaften auf Controllern ein, die als Kontext für das entsprechende Template dienen. Diese Controller sind Singletons und folglich bleibt jeder auf ihnen definierte Zustand auch dann bestehen, wenn der Controller nicht mehr aktiv ist.
Das ist etwas, das sehr leicht zu übersehen ist, und ich bin auch darüber gestolpert. In meinem Fall hatte ich eine Musikkatalog-App mit Bands und Songs. Das Flag songCreationStarted
auf dem songs
-Controller zeigt an, dass der Benutzer begonnen hat, einen Song für eine bestimmte Band zu erstellen. Das Problem war, dass, wenn der Benutzer dann zu einer anderen Band wechselte, der Wert von songCreationStarted
bestehen blieb und es so aussah, als wäre der halb fertige Song für die andere Band, was verwirrend war.
Die Lösung besteht darin, die Controller-Eigenschaften, die wir nicht verweilen möchten, manuell zurückzusetzen. Ein möglicher Ort dafür ist der setupController
Hook der entsprechenden Route, der bei allen Transitionen nach dem afterModel
Hook aufgerufen wird (der, wie der Name schon sagt, nach dem model
Hook kommt):
// app/routes/band.js export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); controller.set('songCreationStarted', false); } });
Embers Minderungsplan
Auch hier wird das Aufkommen von routebaren Komponenten dieses Problem lösen und Controllern ein Ende setzen. Einer der Vorteile von routebaren Komponenten besteht darin, dass sie einen konsistenteren Lebenszyklus haben und immer abgerissen werden, wenn sie von ihren Routen abweichen. Wenn sie ankommen, wird das obige Problem verschwinden.
Häufiger Fehler Nr. 3: Nichtaufrufen der Standardimplementierung in setupController
Routen in Ember haben eine Handvoll Lebenszyklus-Hooks, um anwendungsspezifisches Verhalten zu definieren. Wir haben bereits model
gesehen, das zum Abrufen von Daten für das entsprechende Template und setupController
verwendet wird, um den Controller, den Kontext des Templates, einzurichten.
Letzteres, setupController
, hat einen vernünftigen Standard, der das Modell aus dem model
Hook als model
des Controllers zuweist:
// ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }
( context
ist der Name, der vom ember-routing
Paket für das verwendet wird, was ich oben model
nenne)
Der setupController
Hook kann für verschiedene Zwecke außer Kraft gesetzt werden, wie zum Beispiel das Zurücksetzen des Status des Controllers (wie in Häufiger Fehler Nr. 2 oben). Wenn man jedoch vergisst, die Elternimplementierung aufzurufen, die ich oben in Ember.Route kopiert habe, kann es zu einer langen Kopfzerbrechen-Session kommen, da der Controller seine model
nicht festgelegt hat. Rufen Sie also immer this._super(controller, model)
:
export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); // put the custom setup here } });
Embers Minderungsplan
Wie bereits erwähnt, werden Controller und mit ihnen der setupController
Hook bald verschwinden, sodass diese Falle keine Bedrohung mehr darstellen wird. Hier gibt es jedoch eine wichtigere Lektion zu lernen, nämlich die Implementierungen in Vorfahren zu beachten. Die in Ember.Object
definierte init
-Funktion, die Mutter aller Objekte in Ember, ist ein weiteres Beispiel, auf das Sie achten müssen.
Häufiger Fehler Nr. 4: Verwenden this.modelFor
mit nicht übergeordneten Routen
Der Ember-Router löst das Modell für jedes Routensegment auf, während er die URL verarbeitet. Nehmen wir an, wir haben die folgenden Routen in unserer Anwendung:
Router.map({ this.route('bands', function() { this.route('band', { path: ':id' }, function() { this.route('songs'); }); }); });
Bei einer URL von /bands/24/songs
wird der model
Hook von bands
, bands.band
und dann bands.band.songs
in dieser Reihenfolge aufgerufen. Die Routen-API verfügt über eine besonders praktische Methode, modelFor
, die in untergeordneten Routen verwendet werden kann, um das Modell von einer der übergeordneten Routen abzurufen, da dieses Modell zu diesem Zeitpunkt sicherlich aufgelöst wurde.
Der folgende Code ist beispielsweise eine gültige Methode zum Abrufen des Bandobjekts in der 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); } });
Ein häufiger Fehler ist jedoch die Verwendung eines Routennamens in modelFor, der kein Elternteil der Route ist. Wenn die Routen aus dem obigen Beispiel leicht geändert wurden:
Router.map({ this.route('bands'); this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
Unsere Methode zum Abrufen des in der URL bezeichneten Bands würde brechen, da die Route des bands
kein Elternteil mehr ist und daher ihr Modell nicht aufgelöst wurde.
// 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! } });
Die Lösung besteht darin, modelFor
nur für übergeordnete Routen zu verwenden und die erforderlichen Daten auf andere Weise abzurufen, wenn modelFor
nicht verwendet werden kann, z. B. durch Abrufen aus dem Speicher.
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); } });
Häufiger Fehler Nr. 5: Verwechseln des Kontexts, in dem eine Komponentenaktion ausgelöst wird
Verschachtelte Komponenten waren schon immer einer der am schwierigsten zu begründenden Teile von Ember. Mit der Einführung von Blockparametern in Ember 1.10 wurde ein Großteil dieser Komplexität erleichtert, aber in vielen Situationen ist es immer noch schwierig, auf einen Blick zu erkennen, auf welcher Komponente eine von einer untergeordneten Komponente ausgelöste Aktion ausgelöst wird.
Nehmen wir an, wir haben eine band-list
, die band-list-items
enthält, und wir können jede Band als Favorit in der Liste markieren.
// app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band faveAction="setAsFavorite"}} {{/each}}
Der Aktionsname, der aufgerufen werden soll, wenn der Benutzer auf die Schaltfläche klickt, wird an die band-list-item
Komponente übergeben und wird zum Wert ihrer faveAction
Eigenschaft.

Sehen wir uns nun die Vorlagen- und Komponentendefinition von band-list-item
an:
// 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')); } } });
Wenn der Benutzer auf die Schaltfläche „Fave this“ klickt, wird die Aktion faveBand
ausgelöst, die die faveAction
der Komponente ( im obigen Fall setAsFavorite
) auf der übergeordneten Komponente band-list
auslöst.
Das bringt viele Leute zum Stolpern, da sie erwarten, dass die Aktion auf dem Controller genauso ausgelöst wird wie Aktionen von routengesteuerten Vorlagen (und dann auf den aktiven Routen hochsprudelt). Erschwerend kommt hinzu, dass keine Fehlermeldung protokolliert wird; die übergeordnete Komponente schluckt den Fehler einfach.
Die allgemeine Regel ist, dass Aktionen im aktuellen Kontext ausgelöst werden. Im Fall von Nicht-Komponenten-Vorlagen ist dieser Kontext der aktuelle Controller, während es im Fall von Komponenten-Vorlagen die übergeordnete Komponente ist (falls vorhanden) oder wiederum der aktuelle Controller, wenn die Komponente nicht verschachtelt ist.
Im obigen Fall müsste die Bandlistenkomponente die von band- band-list
band-list-item
empfangene Aktion erneut auslösen, um sie an den Controller oder die Route hochzublasen.
// app/components/band-list.js export default Ember.Component.extend({ bands: [], favoriteAction: 'setFavoriteBand', actions: { setAsFavorite: function(band) { this.sendAction('favoriteAction', band); } } });
Wenn die Bandliste in der bands
band-list
Vorlage definiert wurde, müsste die setFavoriteBand
Aktion im bands
-Controller oder der bands
-Route (oder einer ihrer übergeordneten Routen) behandelt werden.
Embers Minderungsplan
Sie können sich vorstellen, dass dies komplexer wird, wenn es mehr Verschachtelungsebenen gibt (z. B. durch eine fav-button
Komponente innerhalb von band-list-item
). Sie müssen von innen ein Loch durch mehrere Ebenen bohren, um Ihre Botschaft herauszubringen, und auf jeder Ebene aussagekräftige Namen definieren ( setAsFavorite
, favoriteAction
, faveAction
usw.).
Vereinfacht wird dies durch den „Improved Actions RFC“, der bereits auf dem master-Branch verfügbar ist und voraussichtlich in 1.13 enthalten sein wird.
Das obige Beispiel würde dann vereinfacht zu:
// 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>
Häufiger Fehler Nr. 6: Verwenden von Array-Eigenschaften als abhängige Schlüssel
Die berechneten Eigenschaften von Ember hängen von anderen Eigenschaften ab, und diese Abhängigkeit muss vom Entwickler explizit definiert werden. Angenommen, wir haben eine isAdmin
, die genau dann wahr sein sollte, wenn eine der Rollen admin
ist. So könnte man es schreiben:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles')
Mit der obigen Definition wird der Wert von isAdmin
nur ungültig, wenn sich das roles
-Array-Objekt selbst ändert, aber nicht, wenn Elemente zu dem vorhandenen Array hinzugefügt oder daraus entfernt werden. Es gibt eine spezielle Syntax, um festzulegen, dass Hinzufügungen und Entfernungen ebenfalls eine Neuberechnung auslösen sollen:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]')
Häufiger Fehler Nr. 7: Keine beobachterfreundlichen Methoden anwenden
Lassen Sie uns das (jetzt behobene) Beispiel aus Common Mistake No. 6 erweitern und eine User-Klasse in unserer Anwendung erstellen.
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.[]') });
Wenn wir einem solchen User
die admin
Rolle hinzufügen, erleben wir eine Überraschung:
var user = User.create(); user.get('isAdmin'); // => false user.get('roles').push('admin'); user.get('isAdmin'); // => false ?
Das Problem besteht darin, dass Beobachter nicht ausgelöst werden (und daher berechnete Eigenschaften nicht aktualisiert werden), wenn die Standard-JavaScript-Methoden verwendet werden. Dies könnte sich ändern, wenn sich die globale Einführung von Object.observe
in Browsern verbessert, aber bis dahin müssen wir die von Ember bereitgestellten Methoden verwenden. Im aktuellen Fall ist pushObject
das beobachterfreundliche Äquivalent von push
:
user.get('roles').pushObject('admin'); user.get('isAdmin'); // => true, finally!
Häufiger Fehler Nr. 8: Mutation von Eigenschaften in Komponenten
Stellen Sie sich vor, wir haben eine star-rating
die die Bewertung eines Artikels anzeigt und die Einstellung der Bewertung des Artikels ermöglicht. Die Bewertung kann für ein Lied, ein Buch oder die Dribbling-Fähigkeit eines Fußballspielers erfolgen.
Sie würden es wie folgt in Ihrer Vorlage verwenden:
{{#each songs as |song|}} {{star-rating item=song rating=song.rating}} {{/each}}
Nehmen wir weiter an, dass die Komponente Sterne anzeigt, einen vollen Stern für jeden Punkt und danach leere Sterne bis zu einer maximalen Bewertung. Wenn auf einen Stern geklickt wird, wird eine set
Aktion auf dem Controller ausgelöst und sollte so interpretiert werden, dass der Benutzer die Bewertung aktualisieren möchte. Wir könnten den folgenden Code schreiben, um dies zu erreichen:
// 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(); } } });
Das würde die Arbeit erledigen, aber es gibt ein paar Probleme damit. Erstens wird davon ausgegangen, dass das weitergegebene Element eine rating
hat, und daher können wir diese Komponente nicht verwenden, um Leo Messis Dribbling-Fähigkeit zu verwalten (wobei diese Eigenschaft als score
bezeichnet werden könnte).
Zweitens mutiert es die Wertung des Gegenstands in der Komponente. Dies führt zu Szenarien, in denen schwer zu erkennen ist, warum sich eine bestimmte Eigenschaft ändert. Stellen Sie sich vor, wir haben eine andere Komponente in derselben Vorlage, in der diese Bewertung ebenfalls verwendet wird, beispielsweise um die durchschnittliche Punktzahl für den Fußballspieler zu berechnen.
Der Slogan zur Minderung der Komplexität dieses Szenarios lautet „Data down, actions up“ (DDAU). Daten sollten weitergegeben werden (von Route zu Controller zu Komponenten), während Komponenten Aktionen verwenden sollten, um ihren Kontext über Änderungen in diesen Daten zu benachrichtigen. Wie also sollte DDAU hier angewendet werden?
Lassen Sie uns einen Aktionsnamen hinzufügen, der zum Aktualisieren der Bewertung gesendet werden soll:
{{#each songs as |song|}} {{star-rating item=song rating=song.rating setAction="updateRating"}} {{/each}}
Und dann verwenden Sie diesen Namen, um die Aktion zu senden:
// 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 }); } } });
Schließlich wird die Aktion stromaufwärts vom Controller oder der Route behandelt, und hier wird die Bewertung des Elements aktualisiert:
// 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(); } } });
Wenn dies geschieht, wird diese Änderung durch die Bindung, die an die star-rating
mit Sternen übergeben wird, nach unten weitergegeben, und die Anzahl der angezeigten vollen Sterne ändert sich als Ergebnis.
Auf diese Weise findet keine Mutation in den Komponenten statt, und da der einzige App-spezifische Teil die Handhabung der Aktion in der Route ist, leidet die Wiederverwendbarkeit der Komponente nicht.
Wir könnten die gleiche Komponente genauso gut für Fußballfähigkeiten verwenden:
{{#each player.skills as |skill|}} {{star-rating item=skill rating=skill.score setAction="updateSkill"}} {{/each}}
Letzte Worte
Es ist wichtig anzumerken, dass einige (die meisten?) der Fehler, die ich gesehen habe (oder selbst begangen habe), einschließlich derjenigen, über die ich hier geschrieben habe, bald in der 2.x-Serie verschwinden oder stark gemildert werden von Ember.js.
Was bleibt, wird von meinen obigen Vorschlägen angesprochen. Wenn Sie also in Ember 2.x entwickeln, haben Sie keine Entschuldigung mehr, Fehler zu machen! Wenn Sie diesen Artikel als PDF haben möchten, gehen Sie zu meinem Blog und klicken Sie auf den Link am Ende des Beitrags.
Über mich
Ich bin vor zwei Jahren mit Ember.js in die Front-End-Welt gekommen, und ich bin hier, um zu bleiben. Ich war so begeistert von Ember, dass ich anfing, sowohl in Gastbeiträgen als auch auf meinem eigenen Blog intensiv zu bloggen und auf Konferenzen zu präsentieren. Ich habe sogar ein Buch geschrieben, Rock and Roll with Ember.js , für alle, die Ember lernen möchten. Hier können Sie ein Beispielkapitel herunterladen.