-
Notifications
You must be signed in to change notification settings - Fork 32
/
Copy path05-routing.md.erb
417 lines (282 loc) · 22.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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
---
title: Le Routage
slug: routing
date: 0005/01/01
number: 5
points: 5
photoUrl: http://www.flickr.com/photos/ikewinski/9517814403/
photoAuthor: Mike Lewinski
contents: Apprendre le routage dans Meteor.|Créer des pages pour discuter des posts.|Apprendre à faire des liens vers ces URLs.
paragraphs: 72
---
Maintenant que nous avons une liste de posts (qui peuvent avoir été éventuellement envoyés par des utilisateurs), nous avons besoin d'une page pour chaque post où les utilisateurs auront la possibilité de laisser des commentaires.
Nous aimerions rendre ces pages accessible par un *permalien*, une URL de la forme `http://myapp.com/posts/xyz` (où `xyz` est un identifiant MongoDB `_id`) qui est unique pour chaque post.
Cela signifie que nous allons avoir besoin d'un *routage* pour voir ce qu'il y a dans la barre URL du navigateur et afficher le contenu correspondant.
### Ajout du package Iron Router
[Iron Router](https://github.com/EventedMind/iron-router) est un package de routage qui a été créé spécialement pour les applications Meteor.
Non seulement c'est une aide pour le routage (la mise en place des chemins), mais le package s'occupe aussi des filtres (l'assignation de ces chemins à des actions) et il s'occupe même des abonnements (savoir quel chemin permet d'accéder à quelle donnée). (Note : Iron Router a été développé par un des co-auteurs de *Discover Meteor*, Tom Coleman.)
Commençons par installer le package depuis Atmosphere :
~~~bash
meteor add iron:router
~~~
<%= caption "Terminal" %>
Cette commande va télécharger et installer le package Iron Router dans votre application. Notez que vous devrez probablement redémarrer votre application Meteor (avec `ctrl+c` pour terminer le processus, puis `meteor` pour le redémarrer) avant que le package ne soit utilisable.
<% note do %>
### Vocabulaire sur le routage
Nous allons aborder plusieurs fonctionnalités du routage dans ce chapitre. Si vous avez déjà utilisé un Framework comme Rails vous connaissez probablement la plupart de ces concepts. Si ce n'est pas le cas, voici un glossaire pour vous aider :
- **Routes** : la route est le bloc de base du routage. C'est un jeu d'instruction qui dit à l'application où aller et quoi faire pour chaque URL.
- **Chemins** : un chemin (ou Path) est une URL de l'application. Elle peut être statique (`/information_legales`) ou dynamique (`/posts/xyz`). Il peut même y avoir des paramètres (`/search?keyword=meteor`).
- **Segments** : ce sont les différentes parties qui composent un chemin, séparées par un slash (`/`).
- **Hooks** : Les Hooks sont les actions qui seront effectuées avant, après ou même pendant le processus de routage. Un exemple typique serait de vérifier si l'utilisateur a les droits nécessaire pour afficher une page.
- **Filtres** : Les filtres sont des hooks qui sont définis globalement pour une ou plusieurs routes.
- **Template de routes** : Chaque route doit pointer vers un template. Si vous n'en précisez pas un, le routeur cherchera le template avec le même nom que la route.
- **Layouts** : Vous pouvez voir les layouts comme des cadres pour vos données. Ils contiennent tout le code HTML qui entoure les templates et qui ne bougera pas même si le template lui-même est modifié.
- **Contrôleurs** : Quelques fois, vous vous rendrez compte que beaucoup de templates réutilisent les mêmes paramètres. Plutôt que de dupliquer votre code, vous pouvez faire hériter toutes ces routes d'un même *contrôleur de routage* qui contient toute la logique de routage ordinaire.
Pour plus d'information sur Iron Router, consultez [la documentation complète sur GitHub](https://github.com/EventedMind/iron-router).
<% end %>
### Routage : Relier des URLS à des templates
Jusqu'à présent nous avons construit notre layout en utilisant des inclusions codées en dur (comme `{{>postsList}}`). Bien que le contenu de notre application puisse changer, la structure de la page est toujours la même : un titre avec une liste de posts en dessous.
Iron Router nous laisse sortir du cadre en nous laissant changer ce qui est affiché dans la balise HTML `<body>`. Donc nous n'allons pas définir le contenu de cette balise nous-même comme dans une page HTML classique. A la place, nous allons pointer le routeur vers un template spécial qui contient un helper de template `{{> yield}}`.
Ce helper `{{> yield}}` va définir une zone dynamique qui va automatiquement afficher le template correspondant à la route courante (par convention, nous désignerons à partir de maintenant ce template spécial le "template de routage") :
<%= diagram "router-diagram", "Layouts et templates.", "pull-center" %>
Nous allons commencer par créer notre layout et ajouter le helper `{{> yield}}`. Premièrement, nous allons supprimer l'élément HTML `<body>` de `main.html`, et déplacer son contenu vers son propre template, `layout.html` (que nous placerons dans un nouveau dossier `client/templates/application`).
Iron Router s'occupera d'intégrer notre layout dans le template minimaliste `main.html`, qui ressemble maintenant à ça :
~~~html
<head>
<title>Microscope</title>
</head>
~~~
<%= caption "client/main.html" %>
Le fichier `layout.html` nouvellement créé contiendra maintenant le layout extérieur de notre application :
~~~html
<template name="layout">
<div class="container">
<header class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<a class="navbar-brand" href="/">Microscope</a>
</div>
</header>
<div id="main">
{{> yield}}
</div>
</div>
</template>
~~~
<%= caption "client/templates/application/layout.html" %>
Vous noterez que nous avons remplacé l'inclusion du template `postsList` avec un appel du helper `yield`.
Après ce changement, l'onglet de notre navigateur affiche la page d'aide par défaut d'Iron Router. C'est parce que nous n'avons pas encore dit au routeur que faire avec l'URL `/`, donc il renvoie un template vide.
Pour démarrer, nous pouvons retrouver notre ancien comportement en assignant l'URL racine `/` au template `postList`. Nous allons créer un nouveau fichier `router.js` à l'intérieur du répertoire `/lib` dans la racine du projet :
~~~js
Router.configure({
layoutTemplate: 'layout'
});
Router.route('/', {name: 'postsList'});
~~~
<%= caption "lib/router.js"%>
Nous avons effectué deux choses importantes. Premièrement, nous avons dit au routeur d'utiliser le layout que nous venons tout juste de créer comme layout par défaut pour toutes les routes.
Deuxièmement, nous avons défini une nouvelle route appelée `postList` et nous l'avons assignée à la racine `/`.
<% note do %>
### Le répertoire `/lib`
Quoi que vous mettiez dans le répertoire `/lib`, cela sera assurément chargé en premier avant tous les autres fichiers de votre application (avec comme exception possible les paquets intelligents). Ceci en fait une place de choix pour y mettre un helper qui a besoin d'être disponible en permanence.
Une petite mise en garde : notez que le répertoire `/lib` n'est ni dans `/client` ni dans `/server`, cela signifie que le contenu sera disponible dans les deux environnements.
<% end %>
### Routes nommées
Éclaircissons un peu l'ambiguïté ici. Nous avons nommé notre route `postList`, mais nous avons également un *template* appelé `postList`. Donc qu'est-ce qu'il va se passer ici ?
Par défaut, Iron Router va chercher un template avec le même nom que celui de la route. En fait, il va même déduire le nom du *chemin* que vous spécifiez. Bien que cela ne marcherait pas dans ce cas particulier (puisque notre chemin est `/`), Iron Router aurait trouvé le bon template si nous avions utilisé `http://localhost:3000/postsList` comme chemin.
Vous pouvez vous demander pourquoi nous avons quand même besoin de nommer nos routes dans un premier temps. Nommer les routes nous laisse utiliser quelques fonctionnalités de Iron Router qui nous rend plus facile la création de liens dans notre application. La plus utile est le helper Spacebars `{{pathFor}}`, qui retourne l'URL du composant chemin de la route.
Nous voulons que notre lien d'accueil principal pointe vers la liste d'articles, donc au lieu de spécifier une URL statique `/`, nous allons pouvoir utiliser le helper Spacebars. Le résultat final sera le même, mais cela nous donne plus de flexibilité puisque le helper nous renverra toujours la bonne URL même si nous changeons après-coup le chemin de la route dans le routeur.
~~~html
<header class="navbar navbar-default" role="navigation">
<div class="navbar-header">
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
</header>
//...
~~~
<%= caption "client/templates/application/layout.html"%>
<%= highlight "3" %>
<%= commit "5-1", "Routage très basique." %>
### Attente De Données
Si vous déployez la version courante de l'application (ou lancez l'instance web en utilisant le lien au-dessus), vous noterez que la liste apparaît vide un petit moment avant que les articles apparaissent. C'est parce que quand la page se charge la première fois, il n'y a pas d'articles à afficher jusqu'à que la souscription aux `articles` soit terminée, récupérant les données des articles du serveur.
Ce serait une bien meilleure expérience de fournir un indicateur visuel que quelque chose est en train de se passer, et que l'utilisateur doit attendre un moment.
Par chance, Iron Router nous donne un moyen facile de faire ça : nous pouvons lui demander *d'attendre* (to wait on) la souscription.
On commence par déplacer notre souscription `posts` depuis `main.js` vers le routeur :
~~~js
Router.configure({
layoutTemplate: 'layout',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "3" %>
Ce que nous voulons faire ici, c'est que pour *chaque* route du site (nous n'en avons qu'une pour le moment, mais nous en aurons bientôt plus !), nous voulons souscrire à `posts`.
La principale différence entre ceci et ce que nous avions précédemment (lorsque la souscription était dans `main.js`, qui devrait être dorénavant vide et que vous pouvez supprimer), est que maintenant, Iron Router "sait" quand la route est prête -- c'est-à-dire lorsqu'elle a les données dont il a besoin pour le rendu.
### Visez un peu ça
Savoir quand la route `postsList` est prête ne nous est pas grandement utile si de toute façon nous n'allons afficher qu'un template vide. Heureusement, Iron Router inclut une procédé pour retarder l'affichage d'un template jusqu'à ce que la route qui l'appelle soit prête, et affiche un template de chargement (`loading`) à la place :
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "3,4" %>
Notez que puisque nous définissons notre fonction `waitOn` globalement au niveau du routeur, cette séquence ne se produira qu'une fois, lorsqu'un utilisateur accédera à l'application pour la première fois. Après cela, les données seront chargées dans la mémoire du navigateur et le routeur n'aura plus besoin de les attendre.
La pièce finale du puzzle est le template de chargement. Nous allons utiliser le paquet `spin` pour créer un joli indicateur de chargement animé. Ajoutez le avec `meteor add sacha:spin`, et créez le template de `chargement` comme suit, dans le dossier `client/templates/includes` :
~~~html
<template name="loading">
{{>spinner}}
</template>
~~~
<%= caption "client/templates/includes/loading.html" %>
Notez que `{{>spinner}}` est un partial contenu dans le paquet `spin`. Quand bien même ce partial "ne provient pas" de notre application, nous pouvons l'inclure comme n'importe quel autre template.
C'est normalement une bonne idée d'attendre les souscriptions, non seulement pour l'expérience utilisateur, mais aussi parce que cela signifie que vous pouvez, avec certitude, partir du principe que les données seront toujours disponibles depuis un template. Cela supprime le besoin de gérer les cas où les templates sont interprétés avant que leur données sous-jacentes soient disponibles, ce qui nécessite souvent des astuces laborieuses.
<%= commit "5-2", "Attendre la souscription aux articles." %>
<% note do %>
### Un premier aperçu sur la réactivité
La réativité est une partie essentielle de Meteor, et bien que nous n'y avons pas encore vraiment touché, notre template de chargement nous donne un premier aperçu de ce concept.
Rediriger vers un template de chargement si les données ne sont pas encore chargées est vraiment bien, mais comment le routeur sait quand rediriger l'utilisateur vers la bonne page une fois que les données arrivent ?
Pour l'instant, disons juste que c'est exactement où la réactivité intervient, et restons-en là. Mais ne vous inquiétez pas, vous en apprendrez plus bientôt !
<% end %>
### Router vers un article spécifique
Maintenant que nous avons vu comment router vers le template `postsList`, ajoutons une route pour afficher le détail d'un seul article.
Il n'y a pas qu'un seul article : nous ne pouvons continuer et définir une route par article, sinon il y en aurait des milliers. Donc nous allons avoir besoin de mettre une seule route *dynamique*, et permettre à la route d'afficher n'importe quel article que l'on souhaite.
Pour commencer, nous allons créer un template qui renvoie simplement le même template d'article que nous avons utilisé dans la liste d'articles.
~~~html
<template name="postPage">
<div class="post-page page">
{{> postItem}}
</div>
</template>
~~~
<%= caption "client/templates/posts/post_page.html" %>
Nous allons ajouter plus d'éléments dans le template plus tard (tels que les commentaires), mais pour l'instant il va simplement servir de coquille pour notre inclusion `postItem`.
Nous allons créer une autre route nommée, cette fois en associant les chemins d'URL de la forme `/posts/<ID>` au template `postPage` :
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return
Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage'
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "8~10" %>
La syntaxe spéciale `:_id` dit au routeur deux choses : premièrement, faire correspondre n'importe quelle route de la forme `/posts/xyz/`, où "xyz" peut être n'importe quoi. Deuxièmement, mettre ce qu'il trouve à la place de xyz dans une propriété `_id` dans le tableau des `params` du routeur.
Notez que nous utilisons seulement `_id` par convention ici. Le routeur n'a pas de moyen de connaitre si ce que vous lui passez est un `_id`, ou juste une chaîne aléatoire de caractères.
Nous routons maintenant vers le template correct, mais il nous manque encore quelque chose : le routeur connaît l'`_id` de l'article que nous voulons afficher, mais le template n'a toujours pas d'indice. Donc comment peut-on combler ce fossé ?
Heureusement, le routeur a une solution intégrée intelligente : il vous laisse spécifier un **contexte de données** (data context) de template. Vous pouvez imaginer le contexte de données comme l'intérieur d'un délicieux gateau fait de templates et de layouts. Tout simplement, c'est ce avec quoi vous remplissez votre template :
<%= diagram "router-diagram-2", "Le contexte de données.", "pull-center" %>
Dans notre cas, nous pouvons récupérer le bon contexte de données en regardant notre article basé sur l'`_id` récupéré dans l'URL :
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "10" %>
A chaque fois qu'un utilisateur accède à cette route, nous trouverons l'article approprié et le passerons au template. Souvenez-vous que `findOne` retourne un seul article qui correspond à la requête, et que fournir juste un `_id` comme argument est un raccourci pour `{_id: id}`.
A l'intérieur de la fonction `data` d'une route, `this` correspond à la route courante correspondante, et nous pouvons utiliser `this.params` pour accéder aux parties nommées de la route (que nous avons indiqué en les préfixant avec `:` dans notre chemin).
<% note do %>
### En savoir plus à propos des contextes de données
En initialisant un *contexte de données* de template, nous pouvons contrôler la valeur de `this` dans les helpers de template.
C'est habituellement fait implicitement avec l'itérateur `{{#each}}`, qui renvoie automatiquement le contexte de données de chaque itération à l'item en cours d'itération :
~~~html
{{#each widgets}}
{{> widgetItem}}
{{/each}}
~~~
Mais nous pouvons également le faire explicitement en utilisant `{{#with}}`, qui dit simplement "prends cet objet, et applique lui le template suivant". Par exemple, nous pouvons écrire :
~~~html
{{#with myWidget}}
{{> widgetPage}}
{{/with}}
~~~
Il s'avère que vous pouvez obtenir le même résultat en passant le contexte comme *argument* dans l'appel de template. Et donc le code précédent peut être réécrit comme suit :
~~~js
{{> widgetPage myWidget}}
~~~
Pour une exploration plus poussée des contextes de données nous vous suggérons de [lire notre article de blog (en anglais)](https://www.discovermeteor.com/blog/a-guide-to-meteor-templates-data-contexts/) sur ce sujet.
<% end %>
### En utilisant un Route Helper Dynamique Nommé
Enfin, nous allons créer un nouveau bouton "Discuter" qui redirigera vers notre page personnelle de posts. De même, nous pourrions faire quelque chose comme `<a href="/posts/{{_id}}">`, mais c'est plus fiable en utilisant un route helper.
Nous avons nommé la route article `postPage`, donc nous pouvons utiliser le 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 btn-default">Discuss</a>
</div>
</template>
~~~
<%= caption "client/templates/posts/post_item.html"%>
<%= highlight "6" %>
<%= commit "5-3", "Route vers la page d'un article." %>
Attendez, comment le routeur sait comment récupérer la partie `xyz` dans `/posts/xyz` ? Après tout, nous ne lui passons aucun `_id`.
Il s'avère que Iron Router est assez intelligent pour le trouver par lui-même. Nous disons au routeur d'utiliser la route `postPage`, et le routeur sait que cette route requiert un `_id` de ce type (vu que c'est comment nous avons défini notre `path`).
Donc le routeur cherchera cet `_id` dans l'endroit disponible le plus logique : le data context du helper `{{pathFor postPage}}`, en d'autre mots `this`. Et il se trouve que notre `this` va correspondre à l'article, lequel (surprise !) possède une propriété `_id`.
Alternativement, vous pouvez également explicitement dire au routeur où vous aimeriez qu'il cherche la propriété `_id`, en passant un second argument au helper (i.e. `{{pathFor 'postPage' someOtherPost}}`). Un usage pratique de ce modèle serait de récupérer le lien des articles précédents et suivants dans une liste, par exemple.
Pour voir si ça fonctionne correctement, naviguez dans la liste d'articles et cliquez sur un des liens 'Discuss'. Vous devriez voir quelque chose comme ça :
<%= screenshot "5-2", "Page d'un article." %>
<% note do %>
### HTML5 pushState
Une chose à savoir est que ces changements d'URLs utilisent [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).
Le routeur récupère les clics sur les URLs internes au site, et empêche le navigateur de naviguer à l'extérieur de l'application, en plus de faire les changements nécessaires à l'état de l'application.
Si tout fonctionne correctement la page devrait changer instantanément. En fait, parfois les choses changent si vite qu'une sorte de transition pourrait être nécessaire. C'est hors du champ de ce chapitre, mais un sujet tout de même intéressant.
<% end %>
### Article non trouvé
N'oublions pas que le routing fonctionne dans les deux sens : il permet de changer l'url lorsqu'on visite une page, mais il peut aussi afficher une nouvelle page lorsqu'on change *l'url*. Ainsi, nous devons nous assurer de ce qui se passe si quelqu'un entre une mauvaise url.
Heureusement, Iron Router s'occupe de cela pour nous grâce à l'option `notFoundTemplate`.
En premier lieu, nous allons mettre au point un nouveau template qui affiche un simple message d'erreur 404 :
~~~html
<template name="notFound">
<div class="not-found page jumbotron">
<h2>404</h2>
<p>Désolé, nous ne pouvons pas trouver une page à cette adresse.</p>
</div>
</template>
~~~
<%= caption "client/templates/application/not_found.html"%>
Ensuite, nous allons tout simplement lier Iron Route à ce template :
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
//...
~~~
<%= caption "lib/router.js"%>
<%= highlight "4" %>
Pour tester notre nouvelle page d'erreur, vous pouvez essayer d'accéder à une url quelconque comme `http://localhost:3000/rien-par-ici`.
Un instant ; que se passe-t-il si quelqu'un entre une url de la forme `http://localhost:3000/posts/xyz`, où `xyz` *n'*est*pas* un `_id` valide d'article ? C'est toujours une route valide, mais elle ne mène à aucune donnée.
Heureusement, Iron Router est assez intelligent pour gérer cela, il suffit d'ajouter un hook spécial `dataNotFound` à la fin de `router.js` :
~~~js
//...
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
~~~
<%= caption "lib/router.js"%>
<%= highlight "4" %>
Cela indique à Iron Router d'afficher la page "non trouvé" non seulement pour les routes invalides mais aussi à chaque fois que la fonction `data` renvoie un objet non désiré (i.e. `null`, `false`, `undefined` ou un objet vide).
<%= commit "5-4", "Avec le template «non trouvé»." %>
<% note do %>
### Pourquoi "Iron" ?
Au cas où vous vous demanderiez qu'elle est l'histoire derrière le nom "Iron Router" : d'après Chris Mather, créateur de Iron Router, cela s'explique par le fait que les météores sont composées principalement de fer (iron en anglais).
<% end %>