Przewodnik po tworzeniu pierwszej aplikacji Ember.js

Opublikowany: 2022-03-11

Ponieważ nowoczesne aplikacje internetowe działają coraz częściej po stronie klienta (sam fakt, że teraz nazywamy je „aplikacjami internetowymi”, a nie „stronami internetowymi”, jest dość wymowny), wzrosło zainteresowanie frameworkami po stronie klienta . Na tym polu jest wielu graczy, ale w przypadku aplikacji o dużej funkcjonalności i wielu ruchomych elementach wyróżniają się dwa z nich: Angular.js i Ember.js.

Opublikowaliśmy już obszerny [samouczek Angular.js][https://www.toptal.com/angular-js/a-step-by-step-guide-to-your-first-angularjs-app], więc W tym poście skupimy się na Ember.js, w którym zbudujemy prostą aplikację Ember do skatalogowania Twojej kolekcji muzycznej. Zostaniesz wprowadzony do głównych elementów konstrukcyjnych frameworka i rzucisz okiem na jego zasady projektowania. Jeśli chcesz zobaczyć kod źródłowy podczas czytania, jest on dostępny jako rock and roll na Github.

Co zbudujemy?

Oto jak będzie wyglądać nasza aplikacja Rock & Roll w swojej ostatecznej wersji:

Finalna wersja aplikacji z ember.js

Po lewej stronie zobaczysz, że mamy listę wykonawców, a po prawej listę utworów wybranego wykonawcy (widać też, że mam dobry gust muzyczny, ale robię dygresję). Nowi artyści i utwory można dodawać po prostu wpisując w polu tekstowym i naciskając sąsiedni przycisk. Gwiazdy obok każdej piosenki służą do oceny, a la iTunes.

Możemy podzielić podstawową funkcjonalność aplikacji na następujące kroki:

  1. Kliknięcie przycisku „Dodaj” powoduje dodanie do listy nowego wykonawcy o nazwie określonej w polu „Nowy wykonawca” (to samo dotyczy utworów danego wykonawcy).
  2. Opróżnienie pola „Nowy wykonawca” powoduje wyłączenie przycisku „Dodaj” (to samo dotyczy utworów danego wykonawcy).
  3. Kliknięcie nazwy wykonawcy powoduje wyświetlenie jego utworów po prawej stronie.
  4. Klikanie w gwiazdki ocenia dany utwór.

Przed nami długa droga, żeby to zadziałało, więc zacznijmy.

Trasy: klucz do aplikacji Ember.js

Jedną z wyróżniających cech Ember jest duży nacisk, jaki kładzie na adresy URL. W wielu innych frameworkach, posiadanie oddzielnych adresów URL dla osobnych ekranów albo nie istnieje, albo jest wprowadzane po namyśle. W Ember router — komponent, który zarządza adresami URL i przejściami między nimi — jest centralnym elementem koordynującym pracę między blokami konstrukcyjnymi. W związku z tym jest to również klucz do zrozumienia wewnętrznego działania aplikacji Ember.

Oto trasy dla naszej aplikacji:

 App.Router.map(function() { this.resource('artists', function() { this.route('songs', { path: ':slug' }); }); });

Definiujemy ścieżkę zasobów, artists i zagnieżdżoną w niej ścieżkę songs . Ta definicja da nam następujące trasy:

Trasy

Użyłem świetnej wtyczki Ember Inspector (istnieje ona zarówno dla przeglądarki Chrome, jak i Firefox), aby pokazać wygenerowane trasy w czytelny sposób. Oto podstawowe zasady dotyczące tras Ember, które możesz zweryfikować dla naszego konkretnego przypadku za pomocą powyższej tabeli:

  1. Istnieje niejawna trasa application .

    To jest aktywowane dla wszystkich żądań (przejść).

  2. Istnieje niejawna trasa index .

    Jest on wprowadzany, gdy użytkownik przechodzi do katalogu głównego aplikacji.

  3. Każda trasa zasobów tworzy trasę o tej samej nazwie i niejawnie tworzy pod nią trasę indeksu.

    Ta trasa indeksu jest aktywowana, gdy użytkownik nawiguje do trasy. W naszym przypadku artists.index jest uruchamiany, gdy użytkownik przechodzi do /artists .

  4. Prosta (bez zasobów) trasa zagnieżdżona będzie miała swoją nazwę trasy nadrzędnej jako prefiks.

    Trasa, którą zdefiniowaliśmy jako this.route('songs', ...) będzie miała nazwę artists.songs . Uruchamia się, gdy użytkownik przechodzi do /artists/pearl-jam lub /artists/radiohead .

  5. Jeśli ścieżka nie zostanie podana, zakłada się, że jest ona równa nazwie trasy.

  6. Jeśli ścieżka zawiera : , jest uważana za segment dynamiczny .

    Przypisana do niego nazwa (w naszym przypadku slug ) będzie odpowiadać wartości w odpowiednim segmencie adresu url. Powyższy segment slug będzie miał wartość pearl-jam , radiohead lub dowolną inną wartość wyodrębnioną z adresu URL.

Wyświetl listę artystów

W pierwszym kroku zbudujemy ekran, na którym po lewej stronie będzie wyświetlana lista artystów. Ten ekran powinien zostać wyświetlony użytkownikom, którzy przejdą do /artists/ :

Artyści

Aby zrozumieć, jak ten ekran jest renderowany, nadszedł czas, aby wprowadzić inną nadrzędną zasadę projektowania Ember: konwencja nad konfiguracją . W powyższej sekcji widzieliśmy, że /artists aktywuje trasę artists . Zgodnie z konwencją nazwa tego obiektu trasy to ArtistsRoute . Obowiązkiem tego obiektu trasy jest pobranie danych do renderowania aplikacji. Tak dzieje się w haczyku modelu trasy:

 App.ArtistsRoute = Ember.Route.extend({ model: function() { var artistObjects = []; Ember.$.getJSON('http://localhost:9393/artists', function(artists) { artists.forEach(function(data) { artistObjects.pushObject(App.Artist.createRecord(data)); }); }); return artistObjects; } });

W tym fragmencie dane są pobierane za pomocą wywołania XHR z zaplecza i – po konwersji na obiekt modelowy – przekazywane do tablicy, którą możemy następnie wyświetlić. Jednak obowiązki trasy nie obejmują dostarczania logiki wyświetlania, która jest obsługiwana przez kontroler. Spójrzmy.

Hmmm — właściwie nie musimy w tym momencie definiować kontrolera! Ember jest wystarczająco sprytny, aby w razie potrzeby wygenerować kontroler i ustawić atrybut M.odel kontrolera na wartość zwracaną przez sam hook modelu, a mianowicie listę artystów. (Ponownie jest to wynik paradygmatu „konwencja nad konfiguracją”.) Możemy przejść o jedną warstwę w dół i utworzyć szablon do wyświetlania listy:

 <script type="text/x-handlebars" data-template-name="artists"> <div class="col-md-4"> <div class="list-group"> {{#each model}} {{#link-to "artists.songs" this class="list-group-item artist-link"}} {{name}} <span class="pointer glyphicon glyphicon-chevron-right"></span> {{/link-to}} {{/each}} </div> </div> <div class="col-md-8"> <div class="list-group"> {{outlet}} </div> </div> </script>

Jeśli wygląda to znajomo, to dlatego, że Ember.js używa szablonów Handlebars, które mają bardzo prostą składnię i pomocników, ale nie pozwalają na nietrywialną logikę (np. warunki ORing lub AND w trybie warunkowym).

W powyższym szablonie iterujemy przez model (ustawiony wcześniej przez trasę do tablicy zawierającej wszystkich artystów) i dla każdego elementu w nim renderujemy łącze, które prowadzi nas do trasy artists.songs dla tego artysty. Link zawiera nazwę wykonawcy. Pomocnik #each w Handlebars zmienia zakres wewnątrz niego na bieżący element, więc {{name}} zawsze będzie odnosić się do nazwy artysty, który jest aktualnie w iteracji.

Trasy zagnieżdżone dla widoków zagnieżdżonych

Kolejny interesujący punkt w powyższym fragmencie: {{outlet}} , który określa miejsca w szablonie, w których można renderować treść. Podczas zagnieżdżania tras szablon zewnętrznej trasy zasobów jest renderowany jako pierwszy, a następnie trasa wewnętrzna, która renderuje zawartość szablonu w {{outlet}} zdefiniowanym przez trasę zewnętrzną. Dokładnie tak się tutaj dzieje.

Zgodnie z konwencją wszystkie trasy renderują swoją zawartość do szablonu o tej samej nazwie. Powyżej atrybut data-template-name powyższego szablonu to artists co oznacza, że ​​zostanie on wyrenderowany dla zewnętrznej trasy, artists . Określa ujście dla zawartości prawego panelu, do którego wewnętrzna ścieżka, artists.index , renderuje swoją zawartość:

 <script type="text/x-handlebars" data-template-name="artists/index"> <div class="list-group-item empty-list"> <div class="empty-message"> Select an artist. </div> </div> </script>

Podsumowując, jedna trasa ( artists ) wyświetla swoją zawartość na lewym pasku bocznym, a jej modelem jest lista artystów. Inna droga, artists.index , renderuje własną zawartość w gnieździe udostępnionym przez szablon artists . Może pobrać pewne dane, które posłużą jako model, ale w tym przypadku wszystko, co chcemy wyświetlić, to tekst statyczny, więc nie musimy tego robić.

Powiązane: 8 podstawowych pytań do wywiadu Ember.js

Stwórz artystę

Część 1: Wiązanie danych

Następnie chcemy móc tworzyć artystów, a nie tylko patrzeć na nudną listę.

Kiedy pokazałem ten szablon artists , który renderuje listę artystów, trochę oszukałem. Wycinam górną część, aby skupić się na tym, co ważne. Teraz dodam to z powrotem:

 <script type="text/x-handlebars" data-template-name="artists"> <div class="col-md-4"> <div class="list-group"> <div class="list-group-item"> {{input type="text" class="new-artist" placeholder="New Artist" value=newName}} <button class="btn btn-primary btn-sm new-artist-button" {{action "createArtist"}} {{bind-attr disabled=disabled}}>Add</button> </div> < this is where the list of artists is rendered > ... </script>

Używamy pomocnika Ember, input , z typem text do renderowania prostego wprowadzania tekstu. W nim newName kontrolera , który tworzy kopię zapasową tego szablonu, ArtistsController . W konsekwencji, gdy zmieni się właściwość value wejścia (innymi słowy, gdy użytkownik wpisze do niego tekst), właściwość newName na kontrolerze będzie zsynchronizowana.

Informujemy również, że akcja createArtist powinna być uruchamiana po kliknięciu przycisku. Na koniec wiążemy wyłączoną właściwość przycisku z disabled właściwością kontrolera. Jak więc wygląda kontroler?

 App.ArtistsController = Ember.ArrayController.extend({ newName: '', disabled: function() { return Ember.isEmpty(this.get('newName')); }.property('newName') });

newName jest ustawione na puste na początku, co oznacza, że ​​wprowadzany tekst będzie pusty. (Pamiętasz, co powiedziałem o powiązaniach? Spróbuj zmienić newName i zobacz, jak zostanie odzwierciedlone jako tekst w polu wejściowym.)

disabled jest zaimplementowane w taki sposób, że gdy nie ma tekstu w polu wprowadzania, zwróci true a zatem przycisk zostanie wyłączony. Wywołanie .property na końcu sprawia, że ​​jest to „właściwość obliczona”, kolejny pyszny kawałek ciasta Ember.

Właściwości obliczane to właściwości zależne od innych właściwości, które same mogą być „normalne” lub obliczane. Ember buforuje ich wartość, dopóki nie zmieni się jedna z zależnych właściwości. Następnie ponownie oblicza wartość obliczonej właściwości i ponownie ją buforuje.

Oto wizualna reprezentacja powyższego procesu. Podsumowując: gdy użytkownik wprowadzi nazwisko artysty, właściwość newName aktualizuje się, a następnie właściwość disabled , a na końcu nazwisko artysty jest dodawane do listy.

Objazd: pojedyncze źródło prawdy

Pomyśl o tym przez chwilę. Za pomocą powiązań i obliczonych właściwości możemy ustalić (modelować) dane jako pojedyncze źródło prawdy . Powyżej zmiana nazwy nowego artysty powoduje zmianę właściwości kontrolera, co z kolei powoduje zmianę właściwości wyłączonej. Gdy użytkownik zaczyna wpisywać nazwę nowego artysty, przycisk staje się aktywny, jak za dotknięciem czarodziejskiej różdżki.

Im większy system, tym większą przewagę zyskujemy dzięki zasadzie „pojedynczego źródła prawdy”. Dzięki temu nasz kod jest czysty i niezawodny, a definicje właściwości bardziej deklaratywne.

Niektóre inne frameworki również kładą nacisk na to, aby dane modelowe były jedynym źródłem prawdy, ale albo nie idą tak daleko, jak Ember, albo nie wykonują tak dokładnej pracy. Na przykład Angular ma wiązania dwukierunkowe, ale nie ma obliczanych właściwości. Potrafi „emulować” obliczone właściwości za pomocą prostych funkcji; problem polega na tym, że nie ma możliwości sprawdzenia, kiedy należy odświeżyć „właściwość obliczoną”, a zatem ucieka się do brudnego sprawdzania, co z kolei prowadzi do utraty wydajności, szczególnie zauważalnej w większych aplikacjach.

Jeśli chcesz dowiedzieć się więcej na ten temat, polecam przeczytanie posta na blogu eviltrout, aby uzyskać krótszą wersję, lub to pytanie Quora, aby uzyskać dłuższą dyskusję, w której biorą udział główni programiści z obu stron.

Część 2: Programy obsługi akcji

Wróćmy, aby zobaczyć, jak akcja createArtist jest tworzona po jej uruchomieniu (po naciśnięciu przycisku):

 App.ArtistsRoute = Ember.Route.extend({ ... actions: { createArtist: function() { var name = this.get('controller').get('newName'); Ember.$.ajax('http://localhost:9393/artists', { type: 'POST', dataType: 'json', data: { name: name }, context: this, success: function(data) { var artist = App.Artist.createRecord(data); this.modelFor('artists').pushObject(artist); this.get('controller').set('newName', ''); this.transitionTo('artists.songs', artist); }, error: function() { alert('Failed to save artist'); } }); } } });

Programy obsługi akcji muszą być opakowane w obiekt actions i można je zdefiniować na trasie, kontrolerze lub widoku. Zdecydowałem się zdefiniować go tutaj na trasie, ponieważ wynik akcji nie ogranicza się do kontrolera, ale raczej „globalny”.

Nie dzieje się tu nic wymyślnego. Po tym, jak back-end poinformował nas, że operacja zapisywania zakończyła się sukcesem, robimy trzy rzeczy, w kolejności:

  1. Dodaj nowego artystę do modelu szablonu (wszystkich artystów), aby został ponownie wyrenderowany, a nowy artysta pojawił się jako ostatnia pozycja na liście.
  2. Wyczyść pole wejściowe za pomocą powiązania newName , oszczędzając nam konieczności bezpośredniej manipulacji DOM.
  3. Przejście na nowy szlak ( artists.songs ), przekazując świeżo stworzonego artystę jako wzór dla tego szlaku. transitionTo to sposób na wewnętrzne przemieszczanie się między trasami. (Pomocnik link-to służy do tego za pomocą akcji użytkownika.)

Wyświetl piosenki dla artysty

Utwory artysty możemy wyświetlić, klikając jego nazwę. Mijamy też artystę, który stanie się wzorem nowej trasy. Jeśli obiekt modelu zostanie w ten sposób przekazany, hak model trasy nie zostanie wywołany, ponieważ nie ma potrzeby rozwiązywania modelu.

Aktywną trasą jest tutaj artists.songs , a zatem kontrolerem i szablonem będą odpowiednio ArtistsSongsController i artists/songs . Widzieliśmy już, w jaki sposób szablon jest renderowany w miejscu udostępnionym przez szablon artists , dzięki czemu możemy skupić się tylko na szablonie pod ręką:

 <script type="text/x-handlebars" data-template-name="artists/songs"> (...) {{#each songs}} <div class="list-group-item"> {{title}} {{view App.StarRating maxRating=5}} </div> {{/each}} </script>

Zauważ, że usunąłem kod, aby stworzyć nową piosenkę, ponieważ byłby dokładnie taki sam, jak przy tworzeniu nowego artysty.

Właściwość songs jest konfigurowana we wszystkich obiektach wykonawcy na podstawie danych zwróconych przez serwer. Dokładny mechanizm, za pomocą którego to się dzieje, nie interesuje obecnej dyskusji. Na razie wystarczy nam wiedzieć, że każda piosenka ma tytuł i ocenę.

Tytuł jest wyświetlany bezpośrednio w szablonie, a ocena jest reprezentowana przez gwiazdki za pośrednictwem widoku StarRating . Zobaczmy to teraz.

Widżet oceny gwiazdek

Ocena utworu mieści się w zakresie od 1 do 5 i jest pokazywana użytkownikowi w widoku App.StarRating . Widoki mają dostęp do swojego kontekstu (w tym przypadku do utworu) i swojego kontrolera. Oznacza to, że mogą czytać i modyfikować jego właściwości. Jest to w przeciwieństwie do innego bloku konstrukcyjnego Ember, komponentów, które są izolowanymi, kontrolkami wielokrotnego użytku z dostępem tylko do tego, co zostało do nich przekazane. (W tym przykładzie moglibyśmy również użyć elementu oceny w postaci gwiazdek).

Zobaczmy, jak widok wyświetla liczbę gwiazdek i ustawia ocenę utworu, gdy użytkownik kliknie jedną z gwiazdek:

 App.StarRating = Ember.View.extend({ classNames: ['rating-panel'], templateName: 'star-rating', rating: Ember.computed.alias('context.rating'), fullStars: Ember.computed.alias('rating'), numStars: Ember.computed.alias('maxRating'), stars: function() { var ratings = []; var fullStars = this.starRange(1, this.get('fullStars'), 'full'); var emptyStars = this.starRange(this.get('fullStars') + 1, this.get('numStars'), 'empty'); Array.prototype.push.apply(ratings, fullStars); Array.prototype.push.apply(ratings, emptyStars); return ratings; }.property('fullStars', 'numStars'), starRange: function(start, end, type) { var starsData = []; for (i = start; i <= end; i++) { starsData.push({ rating: i, full: type === 'full' }); }; return starsData; }, (...) });

rating , fullStars i numStars to właściwości obliczane , które omówiliśmy wcześniej z disabled właściwością ArtistsController . Powyżej użyłem tak zwanego makra obliczanej właściwości, z których kilkanaście jest zdefiniowanych w Ember. Sprawiają, że typowe właściwości obliczeniowe są bardziej zwięzłe i mniej podatne na błędy (do pisania). Ustawiłem rating jako ocenę kontekstu (a tym samym utworu), podczas gdy zdefiniowałem zarówno właściwości fullStars , jak i numStars , aby lepiej czytały się w kontekście widżetu oceny gwiazdek.

Główną atrakcją jest metoda stars . Zwraca tablicę danych dla gwiazd, w której każdy element zawiera właściwość rating (od 1 do 5) i flagę ( full ), aby wskazać, czy gwiazda jest pełna. To sprawia, że ​​przeglądanie ich w szablonie jest niezwykle proste:

 <script type="text/x-handlebars" data-template-name="star-rating"> {{#each view.stars}} <span {{bind-attr data-rating=rating}} {{bind-attr class=":star-rating :glyphicon full:glyphicon-star:glyphicon-star-empty"}} {{action "setRating" target=view}}> </span> {{/each}} </script>

Ten fragment zawiera kilka ważnych informacji:

  1. Po pierwsze, each pomocnik określa, że ​​używa właściwości widoku (w przeciwieństwie do właściwości na kontrolerze), poprzedzając nazwę właściwości przedrostkiem view .
  2. Po drugie, atrybut class znacznika span ma przypisane mieszane klasy dynamiczne i statyczne. Wszystko poprzedzone przez a : staje się klasą statyczną, podczas gdy full:glyphicon-star:glyphicon-star-empty jest jak operator trójargumentowy w JavaScript: jeśli właściwość full jest prawdziwa, należy przypisać pierwszą klasę; jeśli nie, drugi.
  3. Wreszcie, gdy tag zostanie kliknięty, akcja setRating powinna zostać uruchomiona — ale Ember sprawdzi ją w widoku, a nie w trasie lub kontrolerze, jak w przypadku tworzenia nowego artysty.

Akcja jest więc zdefiniowana na widoku:

 App.StarRating = Ember.View.extend({ (...) actions: { setRating: function() { var newRating = $(event.target).data('rating'); this.set('rating', newRating); } } });

Otrzymujemy ocenę z atrybutu danych rating , który przypisaliśmy w szablonie, a następnie ustawiamy ją jako rating utworu. Zauważ, że nowy rating nie jest utrwalany na zapleczu. Nie byłoby trudno wdrożyć tę funkcjonalność w oparciu o to, jak stworzyliśmy artystę i jest to ćwiczenie dla zmotywowanego czytelnika.

Zawijanie tego wszystkiego

Spróbowaliśmy kilku składników wspomnianego wcześniej ciasta Ember:

  • Widzieliśmy, jak trasy są sednem aplikacji Ember i jak służą jako podstawa konwencji nazewnictwa.
  • Widzieliśmy, jak dwukierunkowe powiązania danych i obliczone właściwości sprawiają, że dane naszego modelu są pojedynczym źródłem prawdy i pozwalają nam uniknąć bezpośredniej manipulacji DOM.
  • Widzieliśmy, jak uruchamiać i obsługiwać akcje na kilka sposobów oraz budować niestandardowy widok, aby utworzyć kontrolkę, która nie jest częścią naszego kodu HTML.

Piękne, prawda?

Dalsze czytanie (i oglądanie)

W Ember jest znacznie więcej, niż mógłbym zmieścić w tym poście. Jeśli chcesz zobaczyć serię screencastów na temat tego, jak stworzyłem nieco bardziej rozwiniętą wersję powyższej aplikacji i/lub dowiedzieć się więcej o Ember, możesz zarejestrować się na mojej liście mailingowej, aby co tydzień otrzymywać artykuły lub wskazówki.

Mam nadzieję, że zaostrzyłem Twój apetyt, aby dowiedzieć się więcej o Ember.js i że wykraczasz daleko poza przykładową aplikację, której użyłem w tym poście. Gdy będziesz dalej poznawać Ember.js, zapoznaj się z naszym artykułem na temat danych Ember, aby dowiedzieć się, jak korzystać z biblioteki danych ember. Miłego budowania!

Powiązane: Ember.js i 8 najczęstszych błędów popełnianych przez programistów