Le Routage

5

Traduction complétée à

Au programme de ce chapitre :

  • Apprendre le routage dans Meteor.
  • Créer des pages pour discuter des posts.
  • Apprendre à faire des liens vers ces URLs.
  • 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 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 :

    meteor add iron:router
    
    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.

    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.

    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”) :

    Layouts et templates.
    Layouts et templates.

    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 :

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

    Le fichier layout.html nouvellement créé contiendra maintenant le layout extérieur de notre application :

    <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>
    
    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 :

    Router.configure({
      layoutTemplate: 'layout'
    });
    
    Router.route('/', {name: 'postsList'});
    
    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 /.

    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.

    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.

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

    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 :

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

    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 :

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

    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 :

    <template name="loading">
      {{>spinner}}
    </template>
    
    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.

    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 !

    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.

    <template name="postPage">
      <div class="post-page page">
        {{> postItem}}
      </div>
    </template>
    
    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 :

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

    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 :

    Le contexte de données.
    Le contexte de données.

    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 :

    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); }
    }); 
    
    lib/router.js

    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).

    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 :

    {{#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 :

    {{#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 :

    {{> 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) sur ce sujet.

    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'}} :

    <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>
    
    client/templates/posts/post_item.html

    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 :

    Page d'un article.
    Page d'un article.

    HTML5 pushState

    Une chose à savoir est que ces changements d'URLs utilisent HTML5 pushState.

    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.

    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 :

    <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>
    
    client/templates/application/not_found.html

    Ensuite, nous allons tout simplement lier Iron Route à ce template :

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    //...
    
    lib/router.js

    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’estpas 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 :

    //...
    
    Router.onBeforeAction('dataNotFound', {only: 'postPage'});
    
    lib/router.js

    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é».

    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).