forked from DiscoverMeteor/DiscoverMeteor_Pl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
05-routing.md.erb
334 lines (221 loc) · 18.5 KB
/
05-routing.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
---
title: Routing
slug: routing
date: 0005/01/01
number: 5
contents: o routingu w Meteorze.|jak tworzyć stronę z dyskusją o postach, z unikalnymi URLami.|jak odpowiednio linkować się do powyższych URL.
paragraphs: 72
---
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](https://github.com/EventedMind/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:
~~~bash
$ mrt add iron-router
~~~
<%= caption "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).
<% note do %>
### 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](https://github.com/EventedMind/iron-router).
<% end %>
### 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”):
<%= diagram "router-diagram", "Layouts and templates.", "pull-center" %>
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:
~~~html
<head>
<title>Microscope</title>
</head>
~~~
<%= caption "client/main.html" %>
A nowo utworzony `layout.html` będzie zawierał zewnętrzny layout aplikacji:
~~~html
<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>
~~~
<%= caption "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.
~~~js
Router.configure({
layoutTemplate: 'layout'
});
Router.map(function() {
this.route('postsList', {path: '/'});
});
~~~
<%= caption "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ą `/`.
<% note do %>
### 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.
<% end %>
### 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.
~~~html
<header class="navbar">
<div class="navbar-inner">
<a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
</header>
//...
~~~
<%= caption "client/views/application/layout.html"%>
<%= highlight "3" %>
<%= commit "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ę:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.map(function() {
this.route('postsList', {path: '/'});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "3,4" %>
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:
~~~html
<template name="loading">
{{>spinner}}
</template>
~~~
<%= caption "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.
<%= commit "5-2", "Czekanie na subskrypcję post." %>
<% note do %>
### 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!
<% end %>
### 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.
~~~html
<template name="postPage">
{{> postItem}}
</template>
~~~
<%= caption "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`:
~~~js
Router.map(function() {
this.route('postsList', {path: '/'});
this.route('postPage', {
path: '/posts/:_id'
});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "4~6" %>
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:
<%= diagram "router-diagram-2", "Kontekst danych.", "pull-center" %>
W naszym przypadku otrzymamy prawidłowy kontekst danych przez szukanie posta na postawie `_id` otrzymanego z URL:
~~~js
Router.map(function() {
this.route('postsList', {path: '/'});
this.route('postPage', {
path: '/posts/:_id',
data: function() { return Posts.findOne(this.params._id); }
});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "4~7" %>
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`).
<% note do %>
### 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.
~~~html
{{#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ć:
~~~html
{{#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:
~~~js
{{> widgetPage myWidget}}
~~~
<% end %>
### 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'}}`:
~~~html
<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>
~~~
<%= caption "client/views/posts/post_item.html"%>
<%= highlight "6" %>
<%= commit "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:
<%= screenshot "5-2", "Strona z pojedyńczym postem." %>
<% note do %>
### HTML5 pushState
Należy zdać sobie sprawę, że powyższe zmiany URL zachodzą przy użyciu [HTML5 pushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history?redirectlocale=en-US&redirectslug=Web%2FGuide%2FDOM%2FManipulating_the_browser_history).
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ń.
<% end %>