-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy path05-routing.md.erb
337 lines (223 loc) · 18.1 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
335
336
337
---
title: Routing
slug: routing
date: 0005/01/01
number: 5
contents: Il routing in Meteor.|Creazione di pagine di discussione dei post, con URL unici.|Come linkare correttamente questi URL.
paragraphs: 72
---
Ora che abbiamo una lista di post (che nella versione finale saranno inseriti dagli utenti), abbiamo bisogno di una pagina specifica per ogni post dove gli utenti possono discutere del singolo post.
Sarebbe bene rendere queste pagine accessibili tramite *permalink*, un URL nella forma `http://myapp.com/posts/xyz` (dove `xyz` è un identificatore `_id` di MongoDB).
Dunque abbiamo bisogno di un meccanismo di indirizzamento detto *routing* per leggere la barra dell'URL del browser e mostrare il contenuto corrispondente.
### Aggiungere il pacchetto Iron Router
[Iron Router](https://github.com/EventedMind/iron-router) è un pacchetto di gestione routing progettato appositamente per le applicazioni Meteor.
Non è utile solo per il routing (impostando il percorso), ma può gestire i filtri (assegnando azioni ad alcuni di questi percorsi) ed anche gestire le sottoscrizioni (controllando quale percorso ha accesso a quali dati). (Nota: Iron Router è sviluppato in parte da Tom Coleman, co-autore di *Discover Meteor*.)
Per prima cosa installiamo il package da Atmosphere:
~~~bash
$ mrt add iron-router
~~~
<%= caption "Terminal" %>
Questo comando scarica ed installa il pacchetto iron-router nell'applicazione, pronto all'uso. Potrebbe essere necessario rilanciare l'applicazione Meteor (con `ctrl+c` per terminare il processo e poi `mrt` per riavviarlo) prima che un pacchetto sia utilizzabile.
Si noti che Iron Router è un pacchetto di terze parti, quindi serve Meteorite per installarlo (`meteor add iron-router` non funziona).
<% note do %>
### Vocabolario del Router
In questo capitolo vedremo molte caratteristiche del router. Se hai già esperienza con un framework tipo Rails, hai già familiarità con la maggior parte di questi concetti. Se no, ecco un veloce glossario per farti imparare rapidamente:
- **Route**: Una route è il mattone base del routing. È il set di istruzioni che dice all'applicazione dove andare e cosa fare quando incontra un URL.
- **Path**: Un path è il percorso indicato da un URL all'interno dell'applicazione. Può essere statico (`/terms_of_service`) o dinamico (`/posts/xyz`), e può anche includere parametri di ricerca (`/search?keyword=meteor`).
- **Segment**: Le diverse parti di un path, delimitato da slashes (`/`).
- **Hook**: Gli hook sono azioni che desideri fare prima, dopo, o anche durante il processo di routing. Un esempio tipico è il controllo dei diritti di accesso dell'utente prima di mostrare una determinata pagina.
- **Filter**: I filtri sono semplicemente hook definiti globalmente per una o più route.
- **Route Template**: Ogni route deve puntare ad un template. Se non ne specifichi uno, il router cercherà un template con lo stesso nome della route.
- **Layout**: Puoi immaginare i layout come una di quelle cornici digitali. Contengono tutto il codice HTML che avvolge l'attuale template, e rimarrà invariato anche se il template cambia.
- **Controller**: A volte potresti renderti conto che molti dei tuoi template riutilizzano gli stessi parametri. Piuttosto che duplicare il codice, puoi ereditare queste route da un unico *routing controller* che contiene tutta la logica di routing.
Per altre informazioni su Iron Router, consulta [la documentazione completa su GitHub](https://github.com/EventedMind/iron-router).
<% end %>
### Routing: Associare gli URL ai Template
Fino a questo momento abbiamo costruito l'interfaccia usando elementi statici nei template (come `{{> postsList}}`). In questo modo sebbene il contenuto dell'applicazione possa cambiare, la struttura di base della pagina rimane sempre la stessa: una intestazione con una lista di post sotto.
Iron Router permette di superare questa staticità prendendo il controllo di ciò che viene mostrato nel tag HTML <body>. Non definiremo noi il contenuto di questo tag, come faremmo con una normale pagina HTML. Faremo invece puntare il router ad uno speciale template di struttura che contiene un helper `{{> yield}}`.
Questo helper `{{> yield}}` definirà un'area dinamica speciale che mostrerà automaticamente il template corrispondente all'attuale route (per convenzione chiameremo questo template speciale "route template" d'ora in poi):
<%= diagram "router-diagram", "Layouts and templates.", "pull-center" %>
Iniziamo creando lo schema di pagina e aggiungendo l'helper `{{> yield}}`. Per prima cosa eliminiamo il tag <body> da `main.html` e ne spostiamo il contenuto in un template `layout.html`.
Quindi il nostro `main.html` modificato appare così:
~~~html
<head>
<title>Microscope</title>
</head>
~~~
<%= caption "client/main.html" %>
Il nuovo file `layout.html` conterrà invece lo schema generale di pagina:
~~~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" %>
Come puoi notare abbiamo sostituito l'inclusione del template `postsList` con una chiamata all'helper `yield`. Dopo questa modifica a video non apparirà più nulla. Succede perché non abbiamo detto al router cosa mostrare con l'URL `/` e quindi mostra semplicemente un template vuoto.
Per iniziare possiamo tornare alla situazione di prima mappando l'URL radice `/` sul template `postsList`. Creiamo una cartella `/lib` nella radice del nostro progetto ed all'interno il file `router.js` :
~~~js
Router.configure({
layoutTemplate: 'layout'
});
Router.map(function() {
this.route('postsList', {path: '/'});
});
~~~
<%= caption "lib/router.js"%>
Abbiamo appena fatto due cose importanti. Primo, abbiamo detto al router di usare l'impaginazione appena creata come default per tutte le route. Secondo, abbiamo definito una nuova route chiamata `postsList` e mappata sul path `/`.
<% note do %>
### La cartella `/lib`
Tutto ciò che metti dentro la cartella `/lib` è garantito che sarà caricato prima di qualsiasi altra cosa nell'applicazione (con la sola potenziale eccezione dei pacchetti smart). È quindi il posto migliore dove mettere il codice degli helper che deve essere disponibile sempre.
Una piccola avvertenza: poiché la cartella `/lib` non si trova né dentro `/client` né dentro `/server` il suo contenuto sarà visibile in entrambi gli ambienti.
<% end %>
### Nomi delle Route
Chiariamo un aspetto potenzialmente ambiguo. Abbiamo chiamato la nostra route `postsList`, ma abbiamo anche un *template* chiamato `postsList`. Dunque che significa?
L'impostazione predefinita di Iron Router è quella di cercare un template con lo stesso nome della route. A dire il vero cercherà anche un *path* basandosi sul nome della route, e quindi se non avessimo definito un path personalizzato (cosa che abbiamo fatto indicando un `path` nella definizione della route), il nostro template sarebbe comunque stato accessibile all'URL `/postsList`.
Potresti chiederti perché abbiamo bisogno di dare un nome alle route come prima cosa. Dar loro un nome ci permette di usare alcune funzionalità di Iron Router che rendono più semplice costruire i link all'interno dell'applicazione. La più utile è l'helper di Spacebars `{{pathFor}}, che contiene la parte di path dell'URL di ogni route.
Noi vogliamo che il link alla home principale punti alla lista di post, e quindi invece di indicare una URL statica `/`, possiamo usare l'helper di Spacebars. Il risultato finale sarà lo stesso, ma avremo maggiore flessibilità visto che l'helper fornirà sempre l'URL corretta anche se cambiassimo il path della route nel router.
~~~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", "Very basic routing." %>
### In Attesa dei Dati
Se installi ed esegui l'attuale versione dell'applicazione (o la lanci dal link qui sopra), potrai notare come la lista appaia vuota per alcuni istanti prima che appaiano i post. Succede perché quando la pagina viene caricata per la prima volta non ci sono post da mostrare finché la sottoscrizione `posts` non ha completato il recupero dei dati dal server.
Sarebbe molto meglio dal punto di vista dell'esperienza d'uso dare un feedback visuale che indica che qualcosa sta succedendo, e quindi l'utente deve aspettare un attimo.
Fortunatamente Iron Router fornisce un modo semplice per farlo -- basta mettersi in attesa della sottoscrizione con `waitOn`:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.map(function() {
this.route('postsList', {path: '/'});
});
Router.onBeforeAction('loading');
~~~
<%= caption "lib/router.js" %>
<%= highlight "3,4, 11" %>
Esaminiamo pezzo per pezzo. Per prima cosa abbiamo modificato il blocco `Router.configure()` per indicare il nome di un template (che creeremo tra un attimo) da mostrare durante il caricamento e l'attesa dei dati.
Secondo abbiamo aggiunto una funzione `waitOn` che ritorna la sottoscrizione ai nostri `posts`. Infine abbiamo abilitato l'hook `loading` integrato. Ciò significa che il router si assicurerà che la sottoscrizione `posts` sia completamente caricata prima di portare l'utente alla route richiesta.
Osserva che poiché stiamo definendo la nostra funzione `waitOn` globalmente a livello del router, questa sequenza avverrà solo una volta quando l'utente accede per la prima volta all'applicazione. Dopo i dati saranno già presenti nella memoria del browser ed il router non dovrà aspettarli di nuovo.
Inoltre poiché lasciamo che sia il router a gestire la sottoscrizione, la puoi tranquillamente rimuovere da `main.js` (che ora dovrebbe essere vuota).
Normalmente è una buona idea attendere il caricamento della sottoscrizione, non solo per l'esperienza d'uso, ma anche perché ti permette di contare sulla disponibilità dei dati all'interno del template. Così facendo infatti non dobbiamo più gestire la situazione in cui un template viene mostrato prima che i dati contenuti siano disponibili, con tutti i problemi che ne conseguono.
Aggiungeremo anche un filtro `onBeforeAction` per triggherare la funzionalità 'loading', predefinita in Iron Router, ed assicurarci di mostrare il template di caricamento mentre attendiamo.
L'ultimo pezzo del puzzle è il template di caricamento vero e proprio. Useremo il pacchetto `spin` per creare una simpatica animazione di attesa caricamento. Aggiungilo con `mrt add spin` e poi crea il template di caricamento così:
~~~html
<template name="loading">
{{> spinner}}
</template>
~~~
<%= caption "client/views/includes/loading.html" %>
Nota che `{{> spinner}}` è un parziale contenuto nel pacchetto `spin`. Sebbene questo parziale arrivi "dall'esterno" dell'applicazione, possiamo includerlo come qualsiasi altro template.
<%= commit "5-2", "Wait on the post subscription." %>
<% note do %>
### Un Primo Sguardo alla Reattività
La reattività è una delle caratteristiche principali di Meteor, e sebbene non l'abbiamo ancora veramente affrontata, il template di caricamento ci dà una prima idea di questo concetto.
Mostrare all'utente un template di caricamento se i dati non sono ancora disponibili è cosa buona e giusta, ma come fa il router a sapere quando mostrare all'utente la pagina *completa* quando i dati sono arrivati?
Per ora diciamo che è esattamente ciò che la reattività ci permette di fare, e fermiamoci qui. Ma non preoccuparti, ne saprai di più molto presto!
<% end %>
### Routing ad un Post Specifico
Adesso che abbiamo visto come mostrare il template `postsList`, creiamo una route per mostrare i dettagli di un singolo post.
C'è solo una difficoltà: non possiamo pensare di creare una route per ogni post visto che potrebbero essercene centinaia. Quindi creiamo una sola route ma di tipo *dinamico*, e facciamo in modo che mostri qualsiasi post desideriamo.
Per iniziare creiamo un nuovo template che semplicemente mostra lo stesso template di singolo post che abbiamo usato prima nella lista dei post.
~~~html
<template name="postPage">
{{> postItem}}
</template>
~~~
<%= caption "client/views/posts/post_page.html" %>
Aggiungeremo altri elementi a questo template più avanti (come ad esempio dei commenti), ma per ora ci servirà semplicemente come contenitore per il nostro `{{> postItem}}`.
Creiamo poi un'altra route a cui assegniamo un nome, questa volta mappando il path dell'URL nella forma `/posts/<ID>` sul template `postPage`:
~~~js
Router.map(function() {
this.route('postsList', {path: '/'});
this.route('postPage', {
path: '/posts/:_id'
});
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "4~6" %>
La sintassi speciale `:_id` dice al router due cose: primo di riconoscere ogni route nella forma `/posts/xyz`, dove "xyz" può essere qualsiasi cosa. Secondo di mettere qualunque cosa trovi al posto di "xyz" dentro una proprietà `_id` dell'array `params` del router.
Nota che stiamo usando `_id` solo per chiarezza. Il router non ha modo di sapere se stai passando un vero `_id` o solo una sequenza casuale di caratteri.
A questo punto stiamo instradando al template corretto, ma ci manca ancora qualcosa: il router conosce l'`_id` del post che vogliamo mostrare, ma il template no. Come risolviamo?
Fortunatamente il router ha una soluzione intelligente già integrata: permette di specificare il **contesto dati** (data context) del template. Puoi immaginare il contesto dati come la farcitura di una torta fatta di template e layout della pagina. Messa in maniera semplice è ciò con cui riempiamo il template:
<%= diagram "router-diagram-2", "The data context.", "pull-center" %>
Nel nostro caso possiamo ottenere il giusto contesto dati cercando il nostro post in base all'`_id` che abbiamo ricavato dall'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" %>
Così ogni volta che l'utente accede a questa route, noi troveremo il post corrispondente e lo passeremo al template. Ricorda che `findOne` ritorna un solo post che soddisfa la ricerca, e che fornire solo un `_id` come argomento è una abbreviazione di `{_id: id}`.
All'interno della funzione `data` di una route, `this` corrisponde alla route attuale e possiamo usare `this.params` per accedere alle proprietà della route (che abbiamo indicato usando il prefisso `:` all'interno del nostro path).
<% note do %>
### Approfondiamo i Contesti Dati
Impostando il *contesto dati* di un template puoi controllare il valore assunto da `this` negli helpers all'interno del template.
Normalmente lo si imposta implicitamente con l'iteratore `{{#each}}`, che imposta automaticamente il contesto dati all'elemento su cui stiamo iterando:
~~~html
{{#each widgets}}
{{> widgetItem}}
{{/each}}
~~~
Ma possiamo anche impostarlo esplicitamente usando `{{#with}}`, che dice semplicemente: "considera questo oggetto e applicalo al seguente template". Ad esempio possiamo scrivere:
~~~html
{{#with myWidget}}
{{> widgetPage}}
{{/with}}
~~~
Si può ottenere lo stesso risultato passando il contesto come *argomento* nell'invocazione del template. In questo caso il precedente codice può essere riscritto così:
~~~js
{{> widgetPage myWidget}}
~~~
<% end %>
### Utilizzare un Helper nella Route Dinamica
Per concludere dobbiamo essere certi di puntare al giusto percorso ogni volta che desideriamo linkare un singolo post. Potremmo scrivere qualcosa del tipo `<a href="/posts/{{_id}}">`, ma usare un route helper è più affidabile.
Abbiamo chiamato la route del singolo post `postPage`, quindi possiamo utilizzare un helper `{{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", "Routing to a single post page." %>
Ma, un attimo! Come fa esattamente il router a sapere dove prendere la parte `xyz` in `/posts/xyz`? In fondo non stiamo passando nessun `_id`.
Scopriamo che Iron Router è sufficientemente intelligente da capirlo da solo. Infatti stiamo dicendo al router di usare la route `postPage`, ed il router sa già che questa route richiede un `_id` di qualche tipo (in quanto è così che abbiamo definito il `path`).
Quindi il router cercherà questo `_id` nel posto più logico: il contesto dati dell'helper {{pathFor 'postPage'}}, cioè `this`. Succede proprio che il nostro `this` corrisponde ad un post che (sorpresa!) possiede una proprietà `_id`.
In alternativa puoi anche dire esplicitamente al router dove vuoi che cerchi la proprietà `_id` passando un secondo argomento all'helper (es. `{{pathFor 'postPage' unAltroPost}}`). Nell'uso reale possiamo usare questa possibilità per avere un link al post precedente o successivo della lista, ad esempio.
Per vedere se funziona tutto correttamente, naviga alla lista di post e clicca sui link `Discuss`. Dovresti vedere qualcosa del genere:
<%= screenshot "5-2", "A single post page." %>
<% note do %>
### HTML5 pushState
Una cosa da tener presente è che questa gestione degli URL avviene attraverso [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).
Il router intercetta i click sugli URL che sono interni al sito e impedisce al browser di gestirli come fossero link esterni, preoccupandosi invece di modificare opportunamente solo lo stato dell'applicazione.
Se tutto funziona a dovere la pagina dovrebbe modificarsi istantaneamente. Addirittura a volte le cose cambiano così rapidamente che potrebbe essere opportuno inserire delle transizioni di pagina. Pur essendo fuori dall'ambito di questo capitolo è un argomento molto interessante.
<% end %>