Retour au cours

frontend / vuejs

Vue Router — guards & routes dynamiques avancées

Leçon 131 exercice

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 guardPortéeOù le déclarer
beforeEachToutes les navigationsrouter.beforeEach(...) global
beforeEnterUne route préciseDans la définition de cette route
onBeforeRouteLeaveDans 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.

js
// 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'
})
js
// 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' }
  },
}
vue
<!-- 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>
js
// 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
js
// 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 false annule la navigation, retourner un objet route effectue une redirection.
  • router.addRoute()/removeRoute() permettent un routing modulaire chargé dynamiquement selon les droits.
  • afterEach s'exécute après coup : parfait pour l'analytics, inutile pour bloquer une navigation.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →