Cele mai frecvente 8 greșeli pe care le fac dezvoltatorii Ember.js
Publicat: 2022-03-11Ember.js este un cadru cuprinzător pentru construirea de aplicații complexe pe partea clientului. Una dintre principiile sale este „convenția asupra configurației” și convingerea că există o parte foarte mare de dezvoltare comună pentru majoritatea aplicațiilor web și, prin urmare, o singură modalitate cea mai bună de a rezolva majoritatea acestor provocări de zi cu zi. Cu toate acestea, găsirea abstractizării potrivite și acoperirea tuturor cazurilor necesită timp și contribuții din partea întregii comunități. După cum arată raționamentul, este mai bine să ne facem timp pentru a obține soluția corectă pentru problema principală și apoi să o coacem în cadru, în loc să ne ridicăm mâinile și să lăsăm pe toți să se descurce singuri atunci când trebuie să găsească o soluție.
Ember.js evoluează constant pentru a face dezvoltarea și mai ușoară. Dar, ca în orice cadru avansat, există încă capcane în care pot cădea dezvoltatorii Ember. Cu următoarea postare, sper să ofer o hartă pentru a evita acestea. Să sărim direct înăuntru!
Greșeala obișnuită nr. 1: așteptarea ca cârligul modelului să se declanșeze atunci când toate obiectele contextului sunt transmise
Să presupunem că avem următoarele rute în aplicația noastră:
Router.map(function() { this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
Traseul band
are un segment dinamic, id
. Când aplicația este încărcată cu o adresă URL cum ar fi /bands/24
, 24
este trecută la cârligul model
al rutei corespunzătoare, band
. Cârligul model are rolul de a deserializa segmentul pentru a crea un obiect (sau o matrice de obiecte) care poate fi apoi folosit în șablon:
// app/routes/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); // params.id is '24' } });
Până acum, bine. Cu toate acestea, există și alte modalități de a introduce rute decât încărcarea aplicației din bara de navigare a browserului. Unul dintre ele folosește link-to
ajutor din șabloane. Următorul fragment trece printr-o listă de band
și creează o legătură către rutele respective ale benzilor:
{{#each bands as |band|}} {{link-to band.name "band" band}} {{/each}}
Ultimul argument pentru link-to, band
, este un obiect care completează segmentul dinamic pentru rută și astfel id
-ul său devine segmentul id pentru rută. Capcana în care cad mulți oameni este că modelul hook nu este numit în acest caz, deoarece modelul este deja cunoscut și a fost transmis. Are sens și ar putea salva o solicitare către server, dar, desigur, nu este. intuitiv. O modalitate ingenioasă de ocolire este să treci, nu obiectul în sine, ci id-ul său:
{{#each bands as |band|}} {{link-to band.name "band" band.id}} {{/each}}
Planul de atenuare al lui Ember
Componentele rutabile vor veni la Ember în curând, probabil în versiunea 2.1 sau 2.2. Când aterizează, cârligul model va fi apelat întotdeauna, indiferent de modul în care se trece la o rută cu un segment dinamic. Citiți RFC-ul corespunzător aici.
Greșeala comună nr. 2: uitând că controlerele conduse de rută sunt singleton
Rutele din Ember.js stabilesc proprietăți pe controlere care servesc drept context pentru șablonul corespunzător. Aceste controlere sunt singleton-uri și, în consecință, orice stare definită pe ele persistă chiar și atunci când controlerul nu mai este activ.
Acesta este ceva care este foarte ușor de trecut cu vederea și m-am împiedicat și eu de asta. În cazul meu, aveam o aplicație de catalog muzical cu trupe și melodii. Indicatorul songCreationStarted
de pe controlerul de songs
a indicat că utilizatorul a început să creeze o melodie pentru o anumită trupă. Problema era că, dacă utilizatorul trecea apoi la o altă trupă, valoarea songCreationStarted
a persistat și părea că melodia pe jumătate terminată era pentru cealaltă trupă, ceea ce era confuz.
Soluția este să resetați manual proprietățile controlerului pe care nu vrem să zăbovim. Un loc posibil pentru a face acest lucru este hook-ul setupController
al rutei corespunzătoare, care este apelat la toate tranzițiile după hook-ul afterModel
(care, după cum sugerează și numele, vine după cârligul model
):
// app/routes/band.js export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); controller.set('songCreationStarted', false); } });
Planul de atenuare al lui Ember
Din nou, zorii componentelor rutabile vor rezolva această problemă, punând capăt complet controlerelor. Unul dintre avantajele componentelor rutabile este că au un ciclu de viață mai consistent și sunt întotdeauna dărâmați atunci când se îndepărtează de rutele lor. Când vor ajunge, problema de mai sus va dispărea.
Greșeala comună nr. 3: Nu se apelează la implementarea implicită în setupController
Rutele din Ember au o mână de cârlige ciclului de viață pentru a defini comportamentul specific aplicației. Am văzut deja model
care este folosit pentru a prelua date pentru șablonul corespunzător și setupController
, pentru configurarea controlerului, contextul șablonului.
Acesta din urmă, setupController
, are o valoare implicită sensibilă, care este atribuirea modelului, din cârligul model
ca proprietate de model
a controlerului:
// ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }
( context
este numele folosit de pachetul ember-routing
pentru ceea ce eu numesc model
mai sus)
Cârligul setupController
poate fi suprascris în mai multe scopuri, cum ar fi resetarea stării controlerului (ca în greșeala comună nr. 2 de mai sus). Cu toate acestea, dacă cineva uită să apeleze implementarea părinte pe care am copiat-o mai sus în Ember.Route, se poate participa la o sesiune lungă de zgârieturi, deoarece controlerul nu va avea proprietatea model
setată. Deci, numiți întotdeauna this._super(controller, model)
:
export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); // put the custom setup here } });
Planul de atenuare al lui Ember
După cum sa spus mai devreme, controlerele și, odată cu ele, cârligul setupController
, vor dispărea în curând, așa că această capcană nu va mai fi o amenințare. Cu toate acestea, există o lecție mai mare de învățat aici, care este să fii atent la implementările în strămoși. Funcția init
, definită în Ember.Object
, mama tuturor obiectelor din Ember, este un alt exemplu la care trebuie să fii atent.
Greșeala comună nr. 4: Utilizarea this.modelFor
cu rute non-parent
Routerul Ember rezolvă modelul pentru fiecare segment de rută pe măsură ce procesează adresa URL. Să presupunem că avem următoarele rute în aplicația noastră:
Router.map({ this.route('bands', function() { this.route('band', { path: ':id' }, function() { this.route('songs'); }); }); });
Având în vedere o adresă URL a /bands/24/songs
, model
hook of bands
, bands.band
și apoi bands.band.songs
sunt numite, în această ordine. Route API are o metodă deosebit de utilă, modelFor
, care poate fi folosită în rutele secundare pentru a prelua modelul de la una dintre rutele părinte, deoarece modelul respectiv a fost cu siguranță rezolvat până în acel moment.
De exemplu, următorul cod este o modalitate validă de a prelua obiectul band în ruta 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); } });
O greșeală comună, totuși, este să folosiți un nume de rută în modelFor care nu este un părinte al rutei. Dacă rutele din exemplul de mai sus au fost ușor modificate:
Router.map({ this.route('bands'); this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
Metoda noastră de a prelua banda desemnată în URL s-ar rupe, deoarece ruta bands
nu mai este părinte și, prin urmare, modelul său nu a fost rezolvat.
// 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! } });
Soluția este să folosiți modelFor
numai pentru rutele părinte și să folosiți alte mijloace pentru a prelua datele necesare atunci când modelFor
nu poate fi utilizat, cum ar fi preluarea din magazin.
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); } });
Greșeala obișnuită nr. 5: greșelirea contextului în care se desfășoară o acțiune componente
Componentele imbricate au fost întotdeauna una dintre cele mai dificile părți ale lui Ember despre care să se raționeze. Odată cu introducerea parametrilor de bloc în Ember 1.10, o mare parte din această complexitate a fost ușurată, dar în multe situații, este încă dificil să vedem dintr-o privire pe ce componentă va fi declanșată o acțiune, declanșată dintr-o componentă copil.
Să presupunem că avem o componentă a band-list
band-list-items
și putem marca fiecare trupă ca favorită în listă.
// app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band faveAction="setAsFavorite"}} {{/each}}
Numele acțiunii care ar trebui să fie invocat atunci când utilizatorul face clic pe buton este trecut în componenta band-list-item
și devine valoarea proprietății sale faveAction
.

Să vedem acum șablonul și definiția componentei 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')); } } });
Când utilizatorul face clic pe butonul „Fave this”, acțiunea faveBand
este declanșată, care declanșează faveAction
a componentei care a fost transmisă ( setAsFavorite
, în cazul de mai sus), pe componenta sa părinte , band-list
.
Acest lucru declanșează o mulțime de oameni, deoarece se așteaptă ca acțiunea să fie declanșată în același mod în care sunt acțiunile din șabloanele bazate pe rute, pe controler (și apoi să apară pe rutele active). Ceea ce face acest lucru mai rău este că nu este înregistrat niciun mesaj de eroare; componenta părinte doar înghite eroarea.
Regula generală este că acțiunile sunt declanșate în contextul actual. În cazul șabloanelor necomponente, acel context este controlerul curent, în timp ce în cazul șabloanelor componente, este componenta părinte (dacă există), sau din nou controlerul curent dacă componenta nu este imbricată.
Deci, în cazul de mai sus, componenta band-list
ar trebui să declanșeze din nou acțiunea primită de la band-list-item
pentru a o trimite la controler sau rută.
// app/components/band-list.js export default Ember.Component.extend({ bands: [], favoriteAction: 'setFavoriteBand', actions: { setAsFavorite: function(band) { this.sendAction('favoriteAction', band); } } });
Dacă band-list
a fost definită în șablonul de bands
, atunci acțiunea setFavoriteBand
ar trebui să fie gestionată în controlerul de bands
sau în ruta de bands
(sau una dintre rutele sale părinte).
Planul de atenuare al lui Ember
Vă puteți imagina că acest lucru devine mai complex dacă există mai multe niveluri de imbricare (de exemplu, având o componentă a fav-button
interiorul band-list-item
). Trebuie să găuriți o gaură prin mai multe straturi din interior pentru a vă transmite mesajul, definind nume semnificative la fiecare nivel ( setAsFavorite
, favoriteAction
, faveAction
, etc.)
Acest lucru este simplificat de „Improved Actions RFC”, care este deja disponibil pe ramura principală și probabil va fi inclus în 1.13.
Exemplul de mai sus ar fi apoi simplificat la:
// 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>
Greșeala comună nr. 6: Utilizarea proprietăților matricei ca chei dependente
Proprietățile calculate ale lui Ember depind de alte proprietăți, iar această dependență trebuie să fie definită în mod explicit de către dezvoltator. Să presupunem că avem o proprietate isAdmin
care ar trebui să fie adevărată dacă și numai dacă unul dintre roluri este admin
. Asa s-ar putea scrie:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles')
Cu definiția de mai sus, valoarea isAdmin
este invalidată numai dacă obiectul matrice de roles
în sine se modifică, dar nu și dacă elementele sunt adăugate sau eliminate în matricea existentă. Există o sintaxă specială pentru a defini că adăugările și eliminările ar trebui, de asemenea, să declanșeze o recalculare:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]')
Greșeala comună nr. 7: Nu folosiți metode prietenoase cu observatorii
Să extindem exemplul (acum remediat) de la Common Mistake No. 6 și să creăm o clasă User în aplicația noastră.
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.[]') });
Când adăugăm rolul de admin
unui astfel de User
, avem o surpriză:
var user = User.create(); user.get('isAdmin'); // => false user.get('roles').push('admin'); user.get('isAdmin'); // => false ?
Problema este că observatorii nu se vor declanșa (și astfel proprietățile calculate nu vor fi actualizate) dacă sunt utilizate metodele Javascript de stoc. Acest lucru s-ar putea schimba dacă adoptarea globală a Object.observe
în browsere se îmbunătățește, dar până atunci, trebuie să folosim setul de metode pe care Ember le oferă. În cazul actual, pushObject
este echivalentul prietenos pentru observatori al lui push
:
user.get('roles').pushObject('admin'); user.get('isAdmin'); // => true, finally!
Greșeala comună nr. 8: mutarea transmisă în proprietăți în componente
Imaginați-vă că avem o componentă star-rating
care afișează evaluarea unui articol și permite setarea evaluării articolului. Evaluarea poate fi pentru o melodie, o carte sau abilitatea de dribling a unui jucător de fotbal.
L-ați folosi astfel în șablonul dvs.:
{{#each songs as |song|}} {{star-rating item=song rating=song.rating}} {{/each}}
Să presupunem în continuare că componenta afișează stele, o stea plină pentru fiecare punct și stele goale după aceea, până la un rating maxim. Când se face clic pe o stea, se declanșează o acțiune set
asupra controlerului și ar trebui interpretată ca utilizatorul care dorește să actualizeze ratingul. Am putea scrie următorul cod pentru a realiza acest lucru:
// 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(); } } });
Asta ar duce treaba la bun sfârșit, dar există câteva probleme cu ea. În primul rând, se presupune că elementul trecut are o proprietate de rating
și, prin urmare, nu putem folosi această componentă pentru a gestiona abilitățile de dribling a lui Leo Messi (unde această proprietate ar putea fi numită score
).
În al doilea rând, modifică evaluarea articolului în componentă. Acest lucru duce la scenarii în care este greu de înțeles de ce se schimbă o anumită proprietate. Imaginați-vă că avem o altă componentă în același șablon în care acel rating este folosit și, de exemplu, pentru calcularea scorului mediu pentru jucătorul de fotbal.
Sloganul pentru atenuarea complexității acestui scenariu este „Date în jos, acțiuni în sus” (DDAU). Datele ar trebui să fie transmise (de la rută la controlor la componente), în timp ce componentele ar trebui să utilizeze acțiuni pentru a-și notifica contextul despre modificările acestor date. Deci, cum ar trebui aplicat DDAU aici?
Să adăugăm un nume de acțiune care ar trebui trimis pentru actualizarea evaluării:
{{#each songs as |song|}} {{star-rating item=song rating=song.rating setAction="updateRating"}} {{/each}}
Și apoi folosiți acest nume pentru a trimite acțiunea în sus:
// 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 }); } } });
În cele din urmă, acțiunea este gestionată în amonte, de către controlor sau rută, și aici este actualizat ratingul articolului:
// 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(); } } });
Când se întâmplă acest lucru, această modificare este propagată în jos prin legarea transmisă componentului star-rating
și, ca urmare, numărul de stele complete afișate se modifică.
În acest fel, mutația nu are loc în componente și, deoarece singura parte specifică aplicației este gestionarea acțiunii în traseu, reutilizarea componentei nu are de suferit.
Am putea folosi la fel de bine aceeași componentă pentru abilitățile de fotbal:
{{#each player.skills as |skill|}} {{star-rating item=skill rating=skill.score setAction="updateSkill"}} {{/each}}
Cuvinte finale
Este important de remarcat că unele (majoritatea?) dintre greșelile pe care le-am văzut pe oameni săvârșind (sau săvârșind eu însumi), inclusiv cele despre care am scris aici, vor dispărea sau vor fi atenuate foarte mult la începutul seriei 2.x. de Ember.js.
Ceea ce rămâne este abordat de sugestiile mele de mai sus, așa că odată ce dezvoltați în Ember 2.x, nu veți mai avea nicio scuză pentru a mai face erori! Dacă doriți acest articol în format pdf, accesați blogul meu și faceți clic pe linkul din partea de jos a postării.
Despre mine
Am venit în lumea front-end cu Ember.js acum doi ani și sunt aici pentru a rămâne. Am devenit atât de entuziasmat de Ember încât am început să scriu intens pe blog atât în postările pentru invitați, cât și pe propriul meu blog, precum și să prezint la conferințe. Am scris chiar și o carte, Rock and Roll cu Ember.js , pentru oricine vrea să învețe Ember. Puteți descărca un exemplu de capitol aici.