8 najczęstszych błędów popełnianych przez programistów Ember.js
Opublikowany: 2022-03-11Ember.js to kompleksowy framework do budowania złożonych aplikacji po stronie klienta. Jedną z jego zasad jest „konwencja nad konfiguracją” i przekonanie, że istnieje bardzo duża część rozwoju wspólnego dla większości aplikacji internetowych, a zatem jest to jedyny najlepszy sposób na rozwiązanie większości tych codziennych wyzwań. Jednak znalezienie właściwej abstrakcji i uwzględnienie wszystkich przypadków wymaga czasu i wkładu całej społeczności. Zgodnie z tokiem rozumowania, lepiej jest poświęcić czas na właściwe rozwiązanie głównego problemu, a następnie wtopić je w ramy, zamiast rozkładać ręce i pozwalać, aby wszyscy radzili sobie sami, gdy muszą znaleźć rozwiązanie.
Ember.js stale ewoluuje, aby programowanie było jeszcze łatwiejsze. Jednak, jak w przypadku każdego zaawansowanego frameworka, wciąż istnieją pułapki, w które mogą wpaść programiści Ember. Mam nadzieję, że w poniższym poście przedstawię mapę, jak to ominąć. Wskoczmy od razu!
Powszechny błąd nr 1: Oczekiwanie, że hak modelu zostanie uruchomiony, gdy wszystkie obiekty kontekstu zostaną przekazane
Załóżmy, że w naszej aplikacji mamy następujące trasy:
Router.map(function() { this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
Trasa band
ma dynamiczny segment, id
. Gdy aplikacja jest ładowana z adresem URL, takim jak /bands/24
, 24
jest przekazywane do haka model
odpowiedniej trasy, band
. Hak modelu pełni rolę deserializacji segmentu w celu utworzenia obiektu (lub tablicy obiektów), którego można następnie użyć w szablonie:
// app/routes/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); // params.id is '24' } });
Na razie w porządku. Istnieją jednak inne sposoby wprowadzania tras niż ładowanie aplikacji z paska nawigacyjnego przeglądarki. Jednym z nich jest użycie link-to
pomocnika z szablonów. Poniższy fragment zawiera listę pasm i tworzy link do ich tras band
:
{{#each bands as |band|}} {{link-to band.name "band" band}} {{/each}}
Ostatnim argumentem link-to, band
, jest obiekt, który wypełnia dynamiczny segment trasy, a zatem jego id
staje się segmentem id dla trasy. Pułapka, w którą wpada wiele osób, polega na tym, że hak modelu nie jest w tym przypadku wywoływany, ponieważ model jest już znany i został przekazany. Ma to sens i może zapisać żądanie do serwera, ale to prawda intuicyjny. Pomysłowym sposobem na obejście tego jest przekazanie nie samego obiektu, ale jego identyfikatora:
{{#each bands as |band|}} {{link-to band.name "band" band.id}} {{/each}}
Plan łagodzenia skutków Ember
Komponenty routowalne pojawią się wkrótce w Ember, prawdopodobnie w wersji 2.1 lub 2.2. Kiedy wylądują, hak modelu zawsze zostanie wywołany, bez względu na to, jak przejdziemy na trasę z dynamicznym segmentem. Przeczytaj odpowiedni RFC tutaj.
Powszechny błąd nr 2: Zapominanie, że kontrolery oparte na trasach to singletony
Trasy w Ember.js konfigurują właściwości na kontrolerach, które służą jako kontekst dla odpowiedniego szablonu. Te kontrolery są singletonami i w związku z tym każdy zdefiniowany na nich stan utrzymuje się nawet wtedy, gdy kontroler nie jest już aktywny.
To jest coś, co bardzo łatwo przeoczyć i ja też się na to natknąłem. W moim przypadku miałem aplikację z katalogiem muzyki z zespołami i piosenkami. Flaga songCreationStarted
na kontrolerze songs
wskazuje, że użytkownik rozpoczął tworzenie utworu dla konkretnego zespołu. Problem polegał na tym, że jeśli użytkownik przełączył się na inny zespół, wartość songCreationStarted
utrzymywała się i wydawało się, że w połowie ukończona piosenka była przeznaczona dla innego zespołu, co było mylące.
Rozwiązaniem jest ręczne zresetowanie właściwości kontrolera, których nie chcemy zwlekać. Jednym z możliwych miejsc, w którym można to zrobić, jest zaczep setupController
odpowiedniej trasy, który jest wywoływany we wszystkich przejściach po zaczepie afterModel
(który, jak sama nazwa wskazuje, występuje po zaczepie model
):
// app/routes/band.js export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); controller.set('songCreationStarted', false); } });
Plan łagodzenia skutków Ember
Ponownie, pojawienie się komponentów trasowalnych rozwiąże ten problem, całkowicie kładąc kres kontrolerom. Jedną z zalet komponentów, które można trasować, jest to, że mają bardziej spójny cykl życia i zawsze ulegają zniszczeniu podczas odchodzenia od swoich tras. Kiedy przybędą, powyższy problem zniknie.
Powszechny błąd nr 3: nie wywołanie domyślnej implementacji w setupController
Trasy w Ember mają kilka haków cyklu życia do definiowania zachowania specyficznego dla aplikacji. Widzieliśmy już model
, który służy do pobierania danych dla odpowiedniego szablonu i setupController
, do konfigurowania kontrolera, kontekstu szablonu.
Ten ostatni, setupController
, ma sensowne ustawienie domyślne, które polega na przypisaniu modelu z zaczepu model
jako właściwości model
kontrolera:
// ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }
( context
to nazwa używana przez pakiet ember-routing
dla tego, co nazywam model
powyżej)
setupController
można przesłonić w kilku celach, takich jak resetowanie stanu kontrolera (jak w Common Mistake nr 2 powyżej). Jeśli jednak zapomni się wywołać implementację nadrzędną, którą skopiowałem powyżej w Ember.Route, można mieć długą sesję head-scratchingu, ponieważ kontroler nie będzie miał ustawionej właściwości model
. Więc zawsze nazywaj this._super(controller, model)
:
export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); // put the custom setup here } });
Plan łagodzenia skutków Ember
Jak wspomniano wcześniej, kontrolery, a wraz z nimi hak setupController
, wkrótce znikną, więc ta pułapka nie będzie już zagrożeniem. Jednak jest tu większa lekcja, którą można wyciągnąć, a mianowicie uważać na implementacje u przodków. Funkcja init
, zdefiniowana w Ember.Object
, matce wszystkich obiektów w Ember, jest kolejnym przykładem, na który musisz uważać.
Powszechny błąd nr 4: Używanie this.modelFor
z trasami niebędącymi rodzicami
Router Ember rozwiązuje model dla każdego segmentu trasy podczas przetwarzania adresu URL. Załóżmy, że w naszej aplikacji mamy następujące trasy:
Router.map({ this.route('bands', function() { this.route('band', { path: ':id' }, function() { this.route('songs'); }); }); });
Biorąc pod uwagę adres URL /bands/24/songs
, wywoływany jest model
hook of bands
, bands.band
, a następnie bands.band.songs
w tej kolejności. Interfejs API tras ma szczególnie przydatną metodę, modelFor
, której można użyć w trasach podrzędnych do pobrania modelu z jednej z tras nadrzędnych, ponieważ model ten został z pewnością rozwiązany w tym momencie.
Na przykład poniższy kod jest prawidłowym sposobem pobrania obiektu band z trasy 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); } });
Częstym błędem jest jednak użycie nazwy trasy w modelFor, która nie jest elementem nadrzędnym trasy. Gdyby trasy z powyższego przykładu zostały nieznacznie zmienione:
Router.map({ this.route('bands'); this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
Nasza metoda pobierania pasma wyznaczonego w adresie URL uległaby awarii, ponieważ trasa bands
nie jest już elementem nadrzędnym, a zatem jej model nie został rozwiązany.
// 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! } });
Rozwiązaniem jest użycie modelFor
tylko dla tras nadrzędnych i użycie innych środków do pobierania niezbędnych danych, gdy nie można użyć modelFor
, takich jak pobieranie ze sklepu.
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); } });
Powszechny błąd nr 5: Pomylenie kontekstu, w którym uruchamiana jest akcja komponentu
Zagnieżdżone komponenty zawsze były jedną z najtrudniejszych części Ember do zrozumienia. Wraz z wprowadzeniem parametrów blokowych w Ember 1.10, wiele z tej złożoności zostało złagodzone, ale w wielu sytuacjach nadal trudno jest na pierwszy rzut oka zobaczyć, na jakim komponencie zostanie uruchomiona akcja, uruchomiona z komponentu podrzędnego.
Załóżmy, że mamy składnik band-list
, który zawiera elementy listy band-list-items
i możemy oznaczyć każdy zespół jako ulubiony na liście.
// app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band faveAction="setAsFavorite"}} {{/each}}
Nazwa akcji, która powinna zostać wywołana, gdy użytkownik kliknie przycisk, jest przekazywana do komponentu band-list-item
i staje się wartością jego właściwości faveAction
.

Zobaczmy teraz szablon i definicję komponentu 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')); } } });
Gdy użytkownik kliknie przycisk „Fave this”, zostanie wyzwolona akcja faveBand
, która uruchomi przekazane działanie faveAction
składnika (w powyższym przypadku setAsFavorite
) na jego komponencie nadrzędnym , band-list
.
To wyskakuje wiele osób, ponieważ spodziewają się, że akcja zostanie uruchomiona w taki sam sposób, jak akcje z szablonów opartych na trasach, na kontrolerze (a następnie bąbelkowanie na aktywnych trasach). Co gorsza, nie jest rejestrowany żaden komunikat o błędzie; komponent nadrzędny po prostu połyka błąd.
Ogólna zasada jest taka, że akcje są uruchamiane w bieżącym kontekście. W przypadku szablonów niekomponentowych kontekst ten jest aktualnym kontrolerem, natomiast w przypadku szablonów komponentowych jest to komponent nadrzędny (jeśli taki istnieje) lub ponownie bieżący kontroler, jeśli komponent nie jest zagnieżdżony.
Tak więc w powyższym przypadku składnik band-list
musiałby ponownie uruchomić akcję otrzymaną z band-list-item
, aby przesłać ją do kontrolera lub trasy.
// app/components/band-list.js export default Ember.Component.extend({ bands: [], favoriteAction: 'setFavoriteBand', actions: { setAsFavorite: function(band) { this.sendAction('favoriteAction', band); } } });
Jeśli band-list
pasm została zdefiniowana w szablonie bands
, akcja setFavoriteBand
musiałaby być obsługiwana w kontrolerze bands
lub w trasie bands
(lub jednej z jej tras nadrzędnych).
Plan łagodzenia skutków Ember
Możesz sobie wyobrazić, że staje się to bardziej skomplikowane, jeśli jest więcej poziomów zagnieżdżenia (na przykład przez umieszczenie komponentu fav-button
wewnątrz band-list-item
). Musisz wywiercić dziurę w kilku warstwach od środka, aby przekazać swoją wiadomość, definiując znaczące nazwy na każdym poziomie ( setAsFavorite
, faveAction
favoriteAction
itp.)
Jest to prostsze dzięki „Poprawione działania RFC”, które jest już dostępne w gałęzi master i prawdopodobnie zostanie uwzględnione w wersji 1.13.
Powyższy przykład zostałby wówczas uproszczony do:
// 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>
Częsty błąd nr 6: Używanie właściwości tablicy jako kluczy zależnych
Obliczone właściwości Ember zależą od innych właściwości, a ta zależność musi być wyraźnie zdefiniowana przez programistę. Załóżmy, że mamy właściwość isAdmin
, która powinna być prawdziwa wtedy i tylko wtedy, gdy jedną z ról jest admin
. Tak można by to napisać:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles')
Z powyższą definicją, wartość isAdmin
zostaje unieważniona tylko wtedy, gdy zmieni się sam obiekt tablicy roles
, ale nie wtedy, gdy elementy zostaną dodane lub usunięte z istniejącej tablicy. Istnieje specjalna składnia, która definiuje, że dodanie i usunięcie powinno również wywołać ponowne przeliczenie:
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]')
Powszechny błąd nr 7: nieużywanie metod przyjaznych dla obserwatora
Rozszerzmy (teraz naprawiony) przykład z Common Mistake nr 6 i stwórzmy klasę User w naszej aplikacji.
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.[]') });
Gdy dodamy takiemu User
rolę admin
, czeka nas niespodzianka:
var user = User.create(); user.get('isAdmin'); // => false user.get('roles').push('admin'); user.get('isAdmin'); // => false ?
Problem polega na tym, że obserwatorzy nie odpalą (a tym samym obliczone właściwości nie zostaną zaktualizowane), jeśli zostaną użyte standardowe metody Javascript. Może się to zmienić, jeśli globalne przyjęcie Object.observe
w przeglądarkach ulegnie poprawie, ale do tego czasu musimy korzystać z zestawu metod, które zapewnia Ember. W obecnym przypadku pushObject
jest przyjaznym dla obserwatora odpowiednikiem push
:
user.get('roles').pushObject('admin'); user.get('isAdmin'); // => true, finally!
Powszechny błąd nr 8: mutacje przekazywane we właściwościach w komponentach
Wyobraź sobie, że mamy komponent star-rating
, który wyświetla ocenę przedmiotu i umożliwia ustawienie oceny przedmiotu. Ocena może dotyczyć piosenki, książki lub umiejętności dryblingu piłkarza.
Używałbyś go tak w swoim szablonie:
{{#each songs as |song|}} {{star-rating item=song rating=song.rating}} {{/each}}
Załóżmy dalej, że komponent wyświetla gwiazdki, jedną pełną gwiazdę na każdy punkt, a następnie puste gwiazdki, aż do maksymalnej oceny. Kliknięcie gwiazdki powoduje uruchomienie akcji set
na kontrolerze, która powinna zostać zinterpretowana jako użytkownik chcący zaktualizować ocenę. Aby to osiągnąć, moglibyśmy napisać następujący kod:
// 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(); } } });
To załatwiłoby sprawę, ale jest z tym kilka problemów. Po pierwsze, zakłada, że przedmiot, który przeszedł, ma właściwość rating
, więc nie możemy użyć tego komponentu do zarządzania umiejętnością dryblingu Leo Messiego (gdzie ta właściwość może być nazwana score
).
Po drugie, zmienia ocenę elementu w komponencie. Prowadzi to do scenariuszy, w których trudno jest zrozumieć, dlaczego dana właściwość się zmienia. Wyobraź sobie, że mamy inny składnik w tym samym szablonie, w którym ta ocena jest również używana, na przykład do obliczania średniego wyniku dla piłkarza.
Hasło łagodzące złożoność tego scenariusza brzmi „Dane w dół, działania w górę” (DDAU). Dane powinny być przekazywane (z trasy do kontrolera do komponentów), natomiast komponenty powinny używać akcji do powiadamiania ich kontekstu o zmianach tych danych. Jak więc zastosować tutaj DDAU?
Dodajmy nazwę akcji, którą należy wysłać w celu aktualizacji oceny:
{{#each songs as |song|}} {{star-rating item=song rating=song.rating setAction="updateRating"}} {{/each}}
A następnie użyj tej nazwy do wysyłania akcji w górę:
// 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 }); } } });
Wreszcie akcja jest obsługiwana z góry, przez kontroler lub trasę, i tutaj aktualizowana jest ocena elementu:
// 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(); } } });
Kiedy tak się dzieje, zmiana ta jest propagowana w dół przez wiązanie przekazywane do składnika star-rating
gwiazd, w wyniku czego zmienia się liczba wyświetlanych pełnych gwiazd.
W ten sposób mutacja nie zachodzi w komponentach, a ponieważ jedyną częścią specyficzną dla aplikacji jest obsługa akcji na trasie, ponowne użycie komponentu nie ucierpi.
Równie dobrze moglibyśmy użyć tego samego komponentu do umiejętności piłkarskich:
{{#each player.skills as |skill|}} {{star-rating item=skill rating=skill.score setAction="updateSkill"}} {{/each}}
Ostatnie słowa
Ważne jest, aby pamiętać, że niektóre (większość?) błędów, które widziałem, jak ludzie popełniali (lub sam popełniłem), w tym te, o których pisałem tutaj, znikną lub zostaną znacznie złagodzone na początku serii 2.x z Ember.js.
To, co pozostaje, zostało uwzględnione w moich sugestiach powyżej, więc gdy będziesz programować w Ember 2.x, nie będziesz miał wymówki, aby popełnić więcej błędów! Jeśli chcesz ten artykuł w formacie PDF, przejdź do mojego bloga i kliknij link na dole posta.
O mnie
Przyszedłem do świata front-endu z Ember.js dwa lata temu i tu zostanę. Stałem się tak entuzjastycznie nastawiony do Ember, że zacząłem intensywnie blogować zarówno w postach gościnnych, jak i na własnym blogu, a także prezentować na konferencjach. Napisałem nawet książkę „ Rock and Roll with Ember.js ” dla każdego, kto chce nauczyć się Ember. Tutaj możesz pobrać przykładowy rozdział.