Le routeur : plusieurs écrans, une seule page
Cours · Vue : construire une application web (école d'ingénieur), chapitre 9 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun
Au chapitre précédent, la Librairie du Campus est devenue une vraie application : son catalogue vient du serveur, son panier se souvient de son contenu. Mais elle n'a qu'un seul écran. Le site du livre JavaScript, lui, avait plusieurs pages : l'accueil, le catalogue, la fiche d'un livre, le panier. Chaque page avait son adresse, que l'on pouvait mettre en favori, envoyer à un ami, ou retrouver avec le bouton Retour du navigateur.
Une application Vue peut offrir la même chose sans jamais recharger la page : c'est le rôle du routeur. Il associe chaque adresse à un composant, affiche le bon composant quand l'adresse change, et change l'adresse quand on clique sur un lien, sans demander de nouvelle page au serveur. Ce chapitre présente Vue Router, la bibliothèque officielle : routes, liens, paramètres, requête, page introuvable, gardes de navigation et chargement différé. À la fin, la Librairie aura une adresse par écran, et la fiche de chaque livre sera accessible directement par son ISBN.
Objectifs du chapitre
- Expliquer ce qu'est une application à page unique et ce que fait un routeur côté client.
- Déclarer des routes, naviguer avec
<RouterLink>et afficher la page courante avec<RouterView>. - Lire les paramètres et la requête d'une adresse, et réagir quand ils changent sans que le composant soit recréé.
- Écrire une page introuvable, des redirections et des gardes de navigation.
- Découper l'application pour ne charger une page qu'à sa première visite, et comprendre ce que l'historique HTML5 exige du serveur.
- Annoncer le changement de page aux utilisateurs de lecteurs d'écran.
Le chapitre utilise Vue Router 5.3.1. Les rendus montrés ont été produits en construisant chaque exemple avec Vite, puis en le pilotant dans Chromium avec l'outil Playwright ; les sorties marquées « test » viennent de Vitest, l'outil de test présenté au chapitre 11.
9.1 Une application à page unique
Sur un site classique, chaque clic sur un lien est une requête au serveur : le navigateur jette la page courante, télécharge la nouvelle, la construit, exécute à nouveau ses scripts. Tout l'état de la page précédente est perdu, sauf ce qu'on a pris soin de ranger dans localStorage.
Une application à page unique (single-page application, SPA) est chargée une seule fois : un document HTML presque vide et un programme JavaScript. Ensuite, c'est le programme qui construit chaque écran. Quand l'utilisateur « change de page », le programme remplace une partie du DOM et met à jour l'adresse affichée par le navigateur, sans nouvelle requête de document.
Le livre JavaScript a déjà montré l'outil qui rend cela possible : l'API History du navigateur. La méthode history.pushState change l'adresse et ajoute une entrée à l'historique sans rien charger ; l'événement popstate signale que l'utilisateur a utilisé les boutons Précédent ou Suivant. Un routeur n'est qu'une couche au-dessus de cette API :
- il tient une table des routes : à tel motif d'adresse, tel composant ;
- au démarrage, il lit l'adresse courante et choisit la route qui lui correspond ;
- quand on clique sur un de ses liens, il empêche le navigateur de charger une nouvelle page, appelle
pushStateet affiche le nouveau composant ; - quand on revient en arrière, il réagit à
popstatede la même façon.
On garde ainsi ce que les utilisateurs attendent du Web (une adresse par écran, le bouton Retour, les favoris, le partage d'un lien) tout en conservant l'état de l'application d'un écran à l'autre : le panier n'a pas besoin d'être relu, le catalogue déjà chargé reste en mémoire.
Le prix d'une page unique
Tout ce que le navigateur faisait seul à chaque changement de page, c'est maintenant au programme de le faire : changer le titre de l'onglet, remonter en haut de la page, et surtout prévenir les utilisateurs de lecteurs d'écran que le contenu a changé. Sans cela, ils cliquent sur un lien et n'entendent rien. La section 9.10 y est consacrée.
9.2 Premières routes
Avant la Librairie, une démonstration réduite au minimum : trois pages (accueil, liste de livres, fiche d'un livre) et une page introuvable. Le projet est dans code/ch9/demo-routeur/. On ajoute Vue Router aux dépendances :
$ npm install vue-router@5.3.1
9.2.1 La table des routes
Le routeur se crée dans un module à part, src/router.js :
import { createRouter, createWebHistory } from "vue-router";
import PageAccueil from "./pages/PageAccueil.vue";
import PageLivres from "./pages/PageLivres.vue";
import PageLivre from "./pages/PageLivre.vue";
import PageIntrouvable from "./pages/PageIntrouvable.vue";
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/", name: "accueil", component: PageAccueil },
{ path: "/livres", name: "livres", component: PageLivres },
{ path: "/livres/:isbn", name: "livre", component: PageLivre, props: true },
{ path: "/:chemin(.*)*", name: "introuvable", component: PageIntrouvable },
],
});
Une route associe un motif d'adresse (path) à un composant. Elle peut porter un nom (name), utile pour construire des liens, et des réglages : passage des paramètres en props, redirection, gardes, données libres (meta).
Trois choix sont faits ici :
createWebHistory()demande des adresses « normales » (/livres/9782956300052), gérées par l'API History ; la section 9.9 présente l'autre mode et ce que celui-ci exige du serveur ;:isbnest un paramètre : ce morceau de l'adresse est variable, et le routeur le met de côté ;- la dernière route attrape toutes les adresses qu'aucune autre n'a reconnues ; elle doit rester la dernière, on y revient plus loin.
On installe le routeur dans l'application comme un plugin, avec use, avant le montage :
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router.js";
createApp(App).use(router).mount("#app");
9.2.2 Les liens et la vue du routeur
Le composant racine ne contient plus de page : il contient le menu, et un emplacement où le routeur affichera la page courante.
<script setup>
import { RouterLink, RouterView } from "vue-router";
</script>
<template>
<header>
<nav aria-label="Navigation principale">
<RouterLink to="/">Accueil</RouterLink>
<RouterLink to="/livres">Livres</RouterLink>
</nav>
</header>
<main>
<RouterView />
</main>
</template>
<style>
body { font-family: system-ui, sans-serif; margin: 1rem; }
nav { display: flex; gap: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #ccc; }
.router-link-active { font-weight: bold; }
.router-link-exact-active { text-decoration: none; }
</style>
<RouterView /> affiche le composant de la route courante, et le remplace à chaque navigation. <RouterLink to="…"> produit un vrai lien <a href="…"> ; un clic dessus est intercepté : le routeur change l'adresse avec l'API History et met à jour la vue, sans recharger la page.
La page de la liste construit ses liens à partir des données :
<script setup>
import { RouterLink } from "vue-router";
import { livres } from "../livres.js";
</script>
<template>
<h1>Nos livres</h1>
<ul>
<li v-for="livre in livres" :key="livre.isbn">
<RouterLink :to="`/livres/${livre.isbn}`">{{ livre.titre }}</RouterLink>
</li>
</ul>
</template>
Construite avec vite build et ouverte à l'adresse /livres, la démonstration affiche ceci (le lien du menu correspondant à la page courante est en gras) :

Voici le HTML que <RouterLink> a produit pour les deux liens du menu, relevé dans Chromium :
<a href="/" class="">Accueil</a>
<a href="/livres" class="router-link-active router-link-exact-active" aria-current="page">Livres</a>
Ce sont de vrais liens, avec un vrai href : on peut les ouvrir dans un nouvel onglet, les copier, les suivre au clavier. Le routeur ajoute deux classes au lien de la page courante, et surtout aria-current="page", qui fait dire « page actuelle » au lecteur d'écran. Pour vérifier que le clic ne recharge rien, on pose une marque dans la page avant de cliquer sur « Le Petit Prince », puis on la relit :
après le clic : /livres/9782956300052 | window.marque = toujours là
L'adresse a changé, mais la variable globale window.marque a survécu : c'est toujours le même document. Un rechargement l'aurait effacée.
Un lien HTML ordinaire recharge tout
Écrire <a href="/livres"> dans un gabarit fonctionne, mais c'est un lien ordinaire : le navigateur demande une nouvelle page au serveur, l'application redémarre, et tout état non sauvegardé est perdu. Dans l'application, un lien interne s'écrit avec <RouterLink>. Un lien vers un autre site reste un <a> ordinaire.
9.2.3 Le lien actif
Le routeur distingue deux classes :
- router-link-exact-active le lien mène exactement à la route courante ;
- router-link-active le lien mène à la route courante ou à l'un de ses parents dans la table des routes (routes imbriquées, plus loin).
Sur la fiche du Petit Prince, que devient le lien « Livres » ? Son adresse « commence par » /livres, et l'on pourrait croire que le lien reste actif. Relevé dans Chromium :
<a href="/" class="">Accueil</a>
<a href="/livres" class="">Livres</a>
Actif selon les routes, pas selon le texte de l'adresse
Le routeur ne compare pas des chaînes de caractères : il compare des routes. /livres et /livres/:isbn sont deux routes sœurs, l'une n'est pas le parent de l'autre ; le lien « Livres » n'est donc pas actif sur une fiche. Pour qu'il le soit, il faut soit déclarer la fiche comme route enfant de la liste (section 9.6), soit calculer soi-même la classe (exercice 9.2).
9.3 Paramètres de route
La fiche reçoit l'ISBN de l'adresse. Avec props: true dans la route, le routeur passe chaque paramètre au composant comme une prop de même nom :
<script setup>
import { computed } from "vue";
import { RouterLink } from "vue-router";
import { livres } from "../livres.js";
const props = defineProps({ isbn: { type: String, required: true } });
const livre = computed(() => livres.find((l) => l.isbn === props.isbn));
</script>
<template>
<template v-if="livre">
<h1>{{ livre.titre }}</h1>
<p>ISBN {{ livre.isbn }} — {{ livre.prix }} €</p>
</template>
<template v-else>
<h1>Livre introuvable</h1>
<p>Aucun livre n’a l’ISBN {{ isbn }}.</p>
</template>
<p><RouterLink to="/livres">Retour à la liste</RouterLink></p>
</template>
La suite du cours est sur Adloun
Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.
Lire la suite du coursEn accès libre, sans inscription.