frontend / vuejs
Vue Router — guards & routes dynamiques avancées
Explication
Ce que vous allez apprendre
- Protéger des routes entières avec un guard global (
beforeEach) - Restreindre l'accès à une route précise avec un guard local (
beforeEnter) - Avertir l'utilisateur avant de quitter un formulaire non sauvegardé avec un guard "in-component"
- Ajouter ou retirer des routes dynamiquement selon les droits de l'utilisateur
- Comprendre la différence entre annuler une navigation et la rediriger
Dans quel contexte ?
Une application doit interdire l'accès à /tableau-de-bord à tout visiteur non connecté, restreindre /admin aux seuls utilisateurs ayant le rôle admin, et avertir un utilisateur qui tente de quitter un formulaire d'édition avec des modifications non sauvegardées. Ces trois besoins de contrôle d'accès et de protection utilisateur correspondent chacun à un niveau différent de navigation guard.
D'abord, un guard qui s'exécute avant chaque navigation
router.beforeEach((to, from) => { ... }) s'exécute avant TOUTE navigation dans l'application, quelle que soit la route de destination. C'est l'endroit central pour vérifier des règles globales comme l'authentification, en s'appuyant sur les métadonnées (meta) déclarées sur chaque route.
Retourner un objet de destination ({ name: 'connexion', query: { redirect: to.fullPath } }) redirige la navigation ailleurs ; retourner false (implicitement, en ne retournant rien de spécial n'annule PAS la navigation — c'est un piège fréquent, voir plus bas) ; ne rien retourner autorise la navigation à continuer normalement.
Prérequis
Il faut avoir compris les bases du routing (leçon précédente) et idéalement Pinia (vu juste après dans ce cours, mais référencé ici via useAuthStore) pour représenter l'état de connexion.
Ensuite, une granularité différente selon le besoin
| Type de guard | Portée | Où le déclarer |
|---|---|---|
beforeEach | Toutes les navigations | router.beforeEach(...) global |
beforeEnter | Une route précise | Dans la définition de cette route |
onBeforeRouteLeave | Dans le composant lui-même | À l'intérieur du composant concerné |
beforeEnter cible une seule route sans affecter les autres — utile pour une validation très spécifique, comme vérifier que le paramètre :id d'une route d'édition est bien numérique avant même d'essayer de charger la page.
Il reste un cas où le composant lui-même doit décider
onBeforeRouteLeave s'exécute quand l'utilisateur tente de QUITTER la route du composant courant. C'est le seul endroit qui a un accès direct à l'état interne du composant (comme formulaireModifie), permettant d'afficher une confirmation avant de perdre des modifications non sauvegardées.
Piège fréquent
Dans beforeEach, afterEach s'exécute APRÈS que la navigation a déjà eu lieu — il est donc totalement inutile pour tenter d'annuler ou de rediriger une navigation à ce stade. afterEach convient uniquement à des actions qui ne peuvent plus influencer la navigation elle-même, comme mettre à jour le titre de la page ou envoyer un événement analytics.
Maintenant, un routing qui s'adapte aux droits de l'utilisateur
router.addRoute({...}) ajoute une route à l'exécution, utile pour des modules chargés dynamiquement selon les droits (un module de facturation visible seulement pour certains comptes). router.removeRoute() et router.hasRoute() complètent ce contrôle dynamique du routing.
Astuce
beforeEnter peut être asynchrone et effectuer un appel réseau AVANT même le rendu du composant, en stockant le résultat dans to.meta pour que le composant le récupère ensuite. Ça évite un affichage transitoire de page vide suivi d'un chargement, au prix d'un léger délai avant la navigation elle-même.
Maintenant que le routing est robuste et sécurisé, la prochaine leçon aborde la gestion de l'état applicatif partagé entre plusieurs composants et plusieurs pages : Pinia, le store officiel de Vue 3.
Commandes & code
Vue Router — guards & routes dynamiques avancées
Les navigation guards permettent de contrôler, annuler ou rediriger une navigation.
// router/index.js — guard global : exécuté avant CHAQUE navigation
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth'
const routes = [
{ path: '/', name: 'accueil', component: () => import('../pages/Accueil.vue') },
{
path: '/tableau-de-bord',
name: 'dashboard',
component: () => import('../pages/Dashboard.vue'),
meta: { necessiteAuth: true },
},
{
path: '/admin',
name: 'admin',
component: () => import('../pages/Admin.vue'),
meta: { necessiteAuth: true, roles: ['admin'] },
},
{ path: '/connexion', name: 'connexion', component: () => import('../pages/Connexion.vue') },
]
export const router = createRouter({ history: createWebHistory(), routes })
router.beforeEach((to, from) => {
const auth = useAuthStore()
if (to.meta.necessiteAuth && !auth.estConnecte) {
// redirection : on renvoie un objet de destination au lieu de laisser passer
return { name: 'connexion', query: { redirect: to.fullPath } }
}
if (to.meta.roles && !to.meta.roles.includes(auth.utilisateur?.role)) {
return { name: 'accueil' } // accès refusé, retour à l'accueil
}
// ne rien retourner (ou `return true`) = navigation autorisée
})
router.afterEach((to) => {
// idéal pour de l'analytics, PAS pour annuler une navigation (trop tard ici)
document.title = to.meta.titre ?? 'Mon Application'
})// Guard local à une route précise
const routeAvecConfirmation = {
path: '/edition/:id',
component: () => import('../pages/Edition.vue'),
beforeEnter: (to, from) => {
const idValide = /^\d+$/.test(to.params.id)
if (!idValide) return { name: 'not-found' }
},
}<!-- Guard "in-component" : utile pour avertir avant de quitter un formulaire non sauvegardé -->
<script setup>
import { ref } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
const formulaireModifie = ref(false)
onBeforeRouteLeave((to, from) => {
if (formulaireModifie.value) {
const confirme = window.confirm('Des modifications non sauvegardées seront perdues. Continuer ?')
if (!confirme) return false // annule la navigation
}
})
</script>// Routes dynamiques ajoutées à l'exécution (ex: modules chargés selon les droits utilisateur)
function ajouterRoutesModule(router, moduleAutorise) {
if (moduleAutorise === 'facturation') {
router.addRoute({
path: '/facturation',
name: 'facturation',
component: () => import('../modules/facturation/PageFacturation.vue'),
})
}
}
// router.removeRoute('facturation') pour retirer dynamiquement une route
// router.hasRoute('facturation') pour vérifier son existence// Résolution des données AVANT le rendu (data fetching couplé à la navigation)
const routeDetailUtilisateur = {
path: '/utilisateurs/:id',
component: () => import('../pages/UtilisateurDetail.vue'),
beforeEnter: async (to) => {
try {
const reponse = await fetch(`/api/utilisateurs/${to.params.id}`)
if (!reponse.ok) return { name: 'not-found' }
to.meta.utilisateur = await reponse.json() // transmis via meta au composant
} catch {
return { name: 'erreur-reseau' }
}
},
}Résumé
beforeEach(global),beforeEnter(par route),onBeforeRouteLeave(in-component) : trois granularités de guard.- Retourner
falseannule la navigation, retourner un objet route effectue une redirection. router.addRoute()/removeRoute()permettent un routing modulaire chargé dynamiquement selon les droits.afterEachs'exécute après coup : parfait pour l'analytics, inutile pour bloquer une navigation.
Exercices pratiques
Mission : une route admin accessible à n'importe qui
Objectif : Diagnostiquer un guard mal placé qui ne bloque jamais une navigation, puis restaurer une protection d'accès correcte à plusieurs niveaux.
Contexte
Un développeur a tenté de protéger /admin en ajoutant la logique de vérification des droits dans router.afterEach((to) => { const auth = useAuthStore(); if (to.meta.roles && !to.meta.roles.includes(auth.utilisateur?.role)) { return { name: 'accueil' } } }). En test, n'importe quel utilisateur non-admin arrive quand même sur la page /admin pendant une fraction de seconde avant d'être éventuellement redirigé, et parfois la redirection ne se produit même pas du tout.