Routing

5

procent przetłumaczone

W tym rozdziale dowiesz się:

  • o routingu w Meteorze.
  • jak tworzyć stronę z dyskusją o postach, z unikalnymi URLami.
  • jak odpowiednio linkować się do powyższych URL.
  • Teraz skoro mamy listę postów (która w końcu będzie wysłana) potrzebujemy stronę dla każdego posta, na której użytkownicy będą mogli prowadzić dyskuję.

    Chcielibyśmy, żeby te strony były dostępne za pomocą permanentnego linka, URLa w formie http://myapp.com/posts/xyz (gdzie xyz jest identyfikatorem MongoDB _id), który jest unikalny dla każdego posta.

    Oznacza to, że potrzebujemy pewnego rodzaju routera, który sprawdza, jaki URL został wpisany w przeglądarce i przekierowuje do właściwej podstrony i wyświetla pożądaną treść.

    Dodanie pakietu Iron Router

    Iron Router jest routerem przeznaczonym i zaprojektowanym specjalnie dla aplikacji Meteora.

    Nie tylko pomaga ze zorganizowaniem routingu (odpowiednich ścieżek), ale również dba o filtry (przypisaniem akcji do niektórych ścieżek) jak również zarządza subskrypcjami (kontroluje, która ścieżka ma dostęp do konretnych danych). (Uwaga: Iron Router został częściowo zaprojektowany przez autora książki Discover Meteor, którą czytasz , Toma Colemana.)

    Na początek zainstalujmy pakiet z Atmosphere:

    $ mrt add iron-router
    
    Terminal

    Powyższa komenda pobiera i instaluje pakiet iron-router w Twojej aplikacji, który jest od razu gotowy do użycia. Zauważ, że będziesz musiał czasem zrestartować aplikacji (za pomocą ctrl+c aby zabić proces i mrt aby go ponownie wystartować) aby pakiet mógł być użyty.

    Zauważ, że Iron Router jest pakietem niedostępnym w standardowych pakietach Meteora, pochodzi z trzeciego źródła i konieczna jest instalacja Meteorite (meteor add iron-router nie zadziała).

    Słowniczek Routera

    Poruszymy wiele różnych tematów routera w tym rozdziale. Jeżeli masz doświadczenie z frameworkiem takim jak Rails, będziesz już zaznajomiony z większością tych konceptów. Jeżeli nie jesteś, oto kilka pojęć, które przyspieszą pracę (zachowujemy oryginalne nazewnictwo, ponieważ jest stosowane w kodzie źródłowym, niektórych się z reguły nie tłumaczy. Podajemy tłumaczenie w nawiasie - przyp. tłum.):

    • Routes (trasy): Trasa jest fundamentalnym pojęciem routera. Jest to zbiór instrukcji, który wskaże aplikacji gdzie się kierować i co robić po napotkaniu danego URL.
    • Paths (ścieżki): Ścieżka jest dowolnym URLem dostępnym w Twojej aplikacji. Może być statyczna (/terms_of_service) lub dynamiczna (/posts/xyz) i może nawet zawierać parametry zapytań (/search?keyword=meteor).
    • Segments (segmenty): części ścieżki rozdzielone slashem (/).
    • Hooks (haki): Hak jest akcją, którą chciałbyć wykonać przed, po, lub podczas wykonywania procesu routowania. Typowym przykładem może być sprawdzenie, czy użytkownik ma wystarczające prawa, aby obejrzeć daną stronę.
    • Filters (filtry): Filtry to globalne haki, które definiuje się dla jednej lub więcej tras.
    • Route Templates (szablony trasy): Każda trasa musi wskazywać na szablon. Jeżeli nie określisz konkretnego szablonu, router będzie domyślnie szukał szablonu o tej samej nazwie, co trasa.
    • Layouts: (układy) Możesz pomyśleć o układach jak o ramce na zdjęcia. Zawierają cały kod HTML, który opakowuje bieżący szablon i pozostanie nienaruszony po zmianach szablonu.
    • Controllers (kontrolery): Czasami zdasz sobie sprawę, że wiele Twoich szablonów używa tych samych parametrów. Zamiast duplikować kod, możesz sprawic aby wszystkie takie trasy dziedziczyły z jednego kontrolera trasy, który będzie zawierał logikę routingu.

    Aby dowiedzieć się więcej o Iron Router, sprawdź pełną dokumentację na GitHub.

    Routing: Mapowanie URL na odpowiednie szablony

    Jak do tej pory budowaliśmy layout za pomocą zahardcode'owanych dołączeń szablonu (takich jak {{>postsList}}). Zatem mimo to, że zawartość strony może ulec zmianom, struktura strony pozostanie taka sama: nagłówek z listą postów poniżej.

    Iron Router pozwala na przerwanie tego schematu przez przejęcie kontroli nad zawartością renderowaną w tagu HTML <body>. Nie definiujemy zatem zawartości nagłówka sami, jak by to było w przypadku normalnej strony HTML. Zamiast tego wskazujemy routerowi specjalny szablon layoutu zawierający helper szablonu zawierający {{> yield}}.

    Powyższy helper {{> yield}} zdefiniuje specjalny dynamiczny obszar, który zostanie automatycznie przerenderowany za każdym razem, gdy szablon będzie odpowiadał bieżącej ścieżce (przyjmijmy, że ten specjalny szablon będzie się nazywał od tej pory “szablonem trasy” ang. “route templates”):

    Layouts and templates.
    Layouts and templates.

    Rozpoczniemy od utworzenia naszego szablonu i dodania helpera {{> yield}}. Najpierw usuniemy tag HTML <body> z main.html i przeniesiemy jego zawartość do odrębnego szablonu layout.html.

    Nasz odchudzony main.html wygląda teraz następująco:

    <head>
      <title>Microscope</title>
    </head>
    
    client/main.html

    A nowo utworzony layout.html będzie zawierał zewnętrzny layout aplikacji:

    <template name="layout">
      <div class="container">
      <header class="navbar">
        <div class="navbar-inner">
          <a class="brand" href="/">Microscope</a>
        </div>
      </header>
      <div id="main" class="row-fluid">
        {{> yield}}
      </div>
      </div>
    </template>
    
    client/views/application/layout.html

    Zauważyłeś zapewne, że zamieniliśmy włączenie szablonu postsList wywołaniem helpera yield. Po zastosowaniu tej zmiany nie zobaczysz nic na ekranie. Dzieje się tak, ponieważ nie wskazaliśmy routerowi jak stosować się do URL / i pokazuje nam domyślnie pusty szablon.

    Aby rozpocząć, możemy przywrócić poprzednią funkcjonalność przez przemapowanie głównego URL / do szablonu postList. Utworzymy folder /lib w głównym folderze projektu i plik router.js w tym folderze.

    Router.configure({
      layoutTemplate: 'layout'
    });
    
    Router.map(function() {
      this.route('postsList', {path: '/'});
    });
    
    lib/router.js

    Osiągneliśmy dwie ważne sprawy. Po pierwsze, wskazaliśmy routerowi, aby używał layout, który właśnie utworzyliśmy jako domyślny layout dla wszystkich tras. Po drugie, zdefiniowaliśmy nową trasę o nazwie postLists i powiązaliśmy ją ze ścieżką /.

    The /lib folder

    Cokolwiek wstawisz do folderu lib będzie załadowane przed załadowaniem pozostałych części aplikacji (z wyjątkiem smart packages). Jest to świetne miejsce do wstawiania jakiegokolwiek kodu pomocniczego, który ma być cały czas dostępny.

    Uwaga: zauważ, że skoro folder /lib nie jest umieszczony w /client ani /server, oznacza to, że jego zawartość będzie dostępna w obu środowiskach.

    Nazwane trasy (ang named routes)

    Rozwiejmy pewne wątpliwości. Nazwaliśmy naszą trasę postsList, a również mamy szablon o nazwie postsList. Jak można to w prosty sposób wyjaśnić?

    Domyślnie Iron Router szuka szablonu o tej samej nazwie, co trasa. Tak naprawdę również szuka ścieżki o tej samej nazwie, co nazwa trasy. Oznacza to, że jeżeli nie zdefiniowaliśmy odrębnej ścieżki (a zrobiliśmy to przez dodanie opcji path w definicji trasy), nasz szablon nie byłby dostępny domyślnie przez URL postsList.

    Możesz się zastanawiać, dlaczego w ogóle potrzebujemy nadawać nazwy trasom. Otóż pozwala to na użycie kilku funkcji Iron Routera, które ułatwiają na tworzenie linków w aplikacji. Najbardziej przydatną jest helper Handlebars {{pathFor}}, który zwraca ścieżkę URL dla każdej trasy.

    Chcemy, aby nasz link do strony domowej wskazywał na listę postów, zatem zamiast precyzować statyczny URL /, możemy również użyć helpera Handlebars. Końcowy wynik będzie identyczny, ale da to nam więcej elastyczności, ponieważ helper zwróci zawsze prawidłowy URL nawet jeżeli zmienimy ścieżką trasy w routerze.

    <header class="navbar">
      <div class="navbar-inner">
        <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      </div>
    </header>
    
    //...
    
    client/views/application/layout.html

    Zatwierdź 5-1

    Podstawowy routing.

    Oczekiwanie na dane

    Jeżeli uruchomisz bieżącą wersję aplikacji w środowisku produkcyjnym przez zawołanie meteor deploy (lub uruchomisz instancję dostępną w linku powyżej) zauważysz, że lista przez moment jest pusta, zanim posty pojawią się na ekranie. Dzieje się tak, ponieważ podczas ładowania strony nie ma żadnych postów do wyświetlenia, aż do momentu gdy subskrypcja posts zakończy pobieranie danych z serwera.

    O wiele lepszym rozwiązaniem byłoby przedstawienie w sposób wizualny użytkownikowi, że coś się w tym czasie dzieje i że powinien przez chwilę poczekać.

    Na szczęście Iron Router umożliwia osiągnięcie tego w łatwy sposób – używając waitOn na subskrypcję:

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.map(function() {
      this.route('postsList', {path: '/'});
    });
    
    lib/router.js

    Opiszmy po kolei. Najpierw zmieniliśmy blok Router.configure() aby dostarczyć routerowi nazwę szablonu wyświetlanego podczas ładowania danych (który zaimplementujemy wkrótce).

    Następnie dodaliśmy funkcję waitOn, która zwraca subskrypcję posts. Ostatecznie podłączyliśmy się pod wbudowaną funkcję loading. Oznacza to, że router upewni się, że subskrypcja posts zostanie załadowana zanim użytkownik zostanie przekierowany na ścieżkę, którą wybrał.

    Zauważ, że skoro definiujemy globalnie funkcję waitOn() na poziomie routera, taka kolej rzeczy będzie miała miejsce tylko przy pierwszym dostępie użytkownika do aplikacji. Kolejnym razem dane będą już załadowane w pamięci przeglądarki i router nie będzie zmuszony na ponowne oczekiwanie.

    Ponieważ pozwalamy routerowi na zarządzanie subskrypcjami, możemy ją bezpiecznie usunąć z pliku main.js (który powinien być teraz pusty).

    Zwykle dobrym pomysłem jest czekanie na subskrypcje nie tylko ze względu na poprawienie obsługi użytkownika, ale również warto upewnić się, że dane będą zawsze dostępne z poziomu szablonu. Eliminuje to konieczność obsługi sytuacji wyjątkowych, gdy szablon jest renderowany zanim dane są dostępne.

    Ostatnim elementem układanki jest ładowanie szablonu. Użyjemy pakietu spin aby utworzyć ładnie wyglądający animowany spinner. Dodaj go za pomocą mrt add spin i utwórz szablon jak poniżej:

    <template name="loading">
      {{>spinner}}
    </template>
    
    client/views/includes/loading.html

    Zauważ, że {{>spinner}}, jest funkcją typu “partial” zawartą w pakiecie spin. Mimo to, że funkcja ta pochodzi z “zewnątrz” aplikacji, możemy ją stosować jak każdy inny dowolny szablon.

    Zatwierdź 5-2

    Czekanie na subskrypcję post.

    Pierwszy rzut oka na reaktywność

    Reaktywność jest jednym z głównych fundamentów Meteora. Pomimo tego, że jeszcze nie zajęliśmy się tym tematem, ładowanie naszego szablonu daje pierwsze wrażenie tego konceptu.

    Przekierowanie do szablonu ładowania danych, jeżeli dane nie zostały jeszcze załadowane jest bardzo dobrym rozwiązaniem, ale skąd router wie kiedy przekierować użytkownika z powrotem na żądaną stronę po załadowaniu danych?

    Na razie nie zastanawiajmy się nad tym. Ale nie martw się, dowiesz się o tym bardzo szybko!

    Przekierowanie do konkretnego posta

    Teraz po tym, jak dowiedzieliśmy się jak przekierowywać na szablon postsList, zaimplementujmy ścieżkę, która umożliwi wyświetlenie szczegółów pojedynczego posta.

    Jest tutaj jeden haczyk: nie możemy kontynuować i definiować osobnych tras dla każdego nowego posta, ponieważ może być ich setki. Potrzebujemy zatem ustawić jedną dynamiczną trasę i sprawić, aby ta trasa wyświetlała żądany post.

    Aby rozpocząć, utworzymy nowy szablon, który po prostu renderuje ten sam szablon posta, którego użyliśmy wcześniej w liście postów.

    <template name="postPage">
      {{> postItem}}
    </template>
    
    client/views/posts/post_page.html

    Dodamy później więcej elementów do tego szablonu (np. komentarze), ale na teraz będzie on służył jako szkielet dla naszego {{> postItem}}.

    Zamierzamy utworzyć nową nazwaną trasę, tym razem kojarząc ścieżkę URL o formie /posts/<ID> z szablonem postPage:

    Router.map(function() {
      this.route('postsList', {path: '/'});
    
      this.route('postPage', {
        path: '/posts/:_id'
      });
    });
    
    
    lib/router.js

    Specjalna składnia :_id informuje router o dwóch sprawach: po pierwsze aby skojarzyć jakąkolwiek trasę o formacie /posts/xyz/, gdzie “xyz” może być dowolne. Po drugie, aby wstawił cokolwiek znajdzie w “xyz” do własności _id tablicy parametrów routera (params).

    Zwróć uwagę, że używamy _id tylko ze względu na łatwość i wygodę implementacji. Router nie ma możliwości, aby dowiedzieć się że przekazujemy właściwy _id, czy dowolny losowy ciąg znaków.

    Przekierowujemy teraz na właściwy szablon, ale ciągle czegoś brakuje: router zna _id posta, który chcemy wyświetlić, ale szablon nie ma o tym żadnego pojęcia. Jak więc rozwiązać ten problem?

    Na szczęście router ma wbudowane sprytne rozwiązanie tego problemu: pozwala na określenie kontekstu danych dla szablonu. Możesz skojarzyć kontekst danych z marmoladą w abstrakcyjnym pączku, pączku stworzonym z szablonów i layoutów. Aby uprościć tok rozumowania, są to dane, z których korzysta szablon:

    Kontekst danych.
    Kontekst danych.

    W naszym przypadku otrzymamy prawidłowy kontekst danych przez szukanie posta na postawie _id otrzymanego z URL:

    Router.map(function() {
      this.route('postsList', {path: '/'});
    
      this.route('postPage', {
        path: '/posts/:_id',
        data: function() { return Posts.findOne(this.params._id); }
      });
    });
    
    
    lib/router.js

    Zatem za każdym razem, gdy użytkownik uzyska dostęp do tej trasy, odpowiedni post zostanie znaleziony i przekazany do szablonu. Pamiętaj, że findOne zwraca pojedynczy post, który jest wynikiem zapytania i że przekazanie id jako parametru funkcji jest skrótem dla {_id: id}.

    W ciele funkcji data dla danej trasy, this odpowiada bieżącej trasie i możemy użyć this.params aby uzyskać dostęp do nazwanych części trasy (które oznaczyliśmy za pomocą przedrostka : w naszej ścieżce).

    Więcej o kontekstach danych

    Możesz kontrolować wartość this w helperach szablonów przez ustawienie kontekstu danych szablonu.

    Dzieje się to zwykle pośrednio przy pomocy iteratora {{#each}}, który automatycznie ustawia kontekst danych każdej iteracji do bieżącego elementu będącego wynikiem iteracji.

    {{#each widgets}}
      {{> widgetItem}}
    {{/each}}
    

    Możemy również jawnie użyć {{#with}}, który oznacza: “użyj powyższy obiekt dla danego szablonu”. Przykładowo możemy napisać:

    {{#with myWidget}}
      {{> widgetPage}}
    {{/with}}
    

    Okazuje się, że możemy to również osiągnąć jawnie przez przesłanie kontekstu jako parametru do wywołania szablonu. W takim przypadku poprzedni fragment kodu może być przepisany następująco:

    {{> widgetPage myWidget}}
    

    Używanie dynamicznego helpera nazwanej trasy

    Na końcu musimy się upewnić, że wskazujemy na właściwe miejsce, gdy chcemy linkować pojedynczy post. Moglibyśmy to osiągnąć również przez <a href="/posts/{{_id}}">, ale użycie helpera trasy jest po prostu bardziej solidne.

    Nazwaliśmy ścieżkę posta postPage, zatem możemy użyć helpera {{pathFor 'postPage'}}:

    <template name="postItem">
      <div class="post">
        <div class="post-content">
          <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
        </div>
        <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
      </div>
    </template>
    
    client/views/posts/post_item.html

    Zatwierdź 5-3

    Przekierowanie na pojedyńczą stronę posta.

    Ale chwila, skąd dokładnie router wie, skąd wziąć część xyz z /posts/xyz? W końcu nie przekazujemy tu żadnego _id.

    Okazuje się, że Iron Router jest na tyle sprytny, że uzyskuje tą informację bez naszej pomocy. Gdy prosimy o użycie trasy postPage router wie, że ta trasa potrzebuje _id (ponieważ zdefiniowaliśmy to w naszej ścieżce).

    Zatem router będzie szukał tego _id w najbardziej logicznym dla tego miejscu: w kontekście danych helpera {{pathFor 'postPage'}}, innymi słowy this. I tak się składa, że this odpowiada postowi, który (o dziwo!) posiada własność _id.

    Ewentualnie, możesz również jawnie nakazać routerowi miejsce szukania własności _id, przez przekazanie drugiego parametru helperowi (np. {{pathFor 'postPage' someOtherPost}}). Praktycznym tego przykładem byłoby uzyskanie linku do poprzedniego lub następnego posta z listy.

    Aby sprawdzić, czy działa to poprawnie, przejdź do listy postów i kliknij na jeden z linków Discuss. Powinieneś zobaczyć coś podobnego do:

    Strona z pojedyńczym postem.
    Strona z pojedyńczym postem.

    HTML5 pushState

    Należy zdać sobie sprawę, że powyższe zmiany URL zachodzą przy użyciu HTML5 pushState.

    Router przechwytuje kliknięcie na URL, które są wewnętrzne dla serwisu i zapobiega przeglądarce na przejście do zewnętrznego linku dla aplikacji. Zamiast tego uaktualnia tylko potrzebne zmiany stanu aplikacji.

    Jeżeli wszystko działa prawidłowo, strona powinna zmieniać się natychmiastowo. Tak naprawdę, czasami dzieje się to zbyt szybko i może być potrzebny sposób na łagodne przejście z jednej strony na drugą. Jest to poza zakresem tego rozdziału, ale również jest to interesujący temat do przemyśleń.