Vue.js

Routing et Navigation Guards

Gérer la navigation dans une SPA Vue 3 avec Vue Router et les navigation guards

Dans ce chapitre, on ajoute la navigation multi-pages au Show Watchlist : une page d'accueil, une page de connexion et une page de détail par film.

Pourquoi un router ?

Dans une SPA, une seule page HTML est chargée. La navigation entre les "pages" est gérée côté client : Vue Router intercepte les changements d'URL et affiche le composant correspondant sans rechargement.

Sans Vue Router, la navigation entre vues s'effectuerait manuellement avec v-if — ingérable dès qu'on a plusieurs pages.

Installation

npm install vue-router

Configurer le router

// src/router.ts
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from './pages/HomePage.vue'
import ShowDetailPage from './pages/ShowDetailPage.vue'

export const ROUTES = {
  HOME: '/',
  SHOW_DETAIL: '/show/:id',
} as const

const routes = [
  { path: ROUTES.HOME, component: HomePage },
  { path: ROUTES.SHOW_DETAIL, component: ShowDetailPage },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router
// main.ts
import naive from 'naive-ui'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.use(naive)
app.mount('#app')

createWebHistory() utilise l'API History du navigateur (URLs propres sans #). Le serveur doit être configuré pour renvoyer index.html pour toutes les routes.

Afficher les pages avec RouterView

<RouterView /> est le composant qui affiche la page correspondant à l'URL courante. Il se place dans App.vue.

<!-- App.vue -->
<template>
  <NavMenu />
  <main>
    <RouterView />
  </main>
</template>

<script setup lang="ts">
import NavMenu from '@/components/NavMenu.component.vue'
</script>

<RouterLink> génère un <a> qui navigue sans recharger la page.

<!-- src/components/NavMenu.component.vue -->
<template>
  <NSpace align="center" :size="8">
    <RouterLink :to="ROUTES.HOME">
      <NButton quaternary :type="route.path === ROUTES.HOME ? 'primary' : 'default'">
        Accueil
      </NButton>
    </RouterLink>
  </NSpace>
</template>

<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ROUTES } from '@/router'

const route = useRoute()
</script>

Routes dynamiques avec paramètres

Une route comme /show/:id accepte n'importe quel identifiant. Le paramètre est accessible via useRoute().

<!-- src/pages/ShowDetailPage.vue -->
<template>
  <div>
    <p v-if="!show">Film introuvable.</p>
    <NCard v-else :title="show.title">
      <p>{{ show.genre }} — {{ show.year }}</p>
      <NTag :type="show.seen ? 'success' : 'default'">
        {{ show.seen ? '✓ Vu' : 'À voir' }}
      </NTag>
    </NCard>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import type { Show } from '@/types'

const route = useRoute()

const shows: Show[] = [
  { id: 1, title: 'Inception', genre: 'Sci-Fi', year: 2010, seen: true },
  { id: 2, title: 'The Dark Knight', genre: 'Action', year: 2008, seen: false },
  { id: 3, title: 'Interstellar', genre: 'Sci-Fi', year: 2014, seen: false },
]

const show = computed(() =>
  shows.find((m) => m.id === Number(route.params.id))
)
</script>

Et dans ShowCard, on ajoute un lien vers la page de détail :

<template>
  <NCard :title="show.title">
    <!-- ... -->
    <template #footer>
      <NSpace>
        <NButton type="primary" @click="emit('toggle-seen', show)">Basculer</NButton>
        <RouterLink :to="`/show/${show.id}`">
          <NButton>Détail</NButton>
        </RouterLink>
      </NSpace>
    </template>
  </NCard>
</template>

useRouter() permet de naviguer par code (après une action, une connexion, etc.).

import { useRouter } from 'vue-router'

const router = useRouter()

router.push(ROUTES.HOME)           // Naviguer vers une route
router.push(`/show/${show.id}`)  // Avec un paramètre dynamique
router.back()                      // Revenir en arrière
router.replace(ROUTES.HOME)        // Remplacer l'historique (pas de retour possible)

Les navigation guards permettent de contrôler l'accès aux routes : rediriger l'utilisateur non connecté vers la page de connexion, ou empêcher un utilisateur connecté d'accéder à la page de login.

Définir des métadonnées sur les routes

// src/router.ts
const routes = [
  {
    path: ROUTES.HOME,
    component: HomePage,
  },
  {
    path: ROUTES.SHOW_DETAIL,
    component: ShowDetailPage,
    meta: { requiresAuth: true }, // Nécessite d'être connecté
  },
]

Typer les métadonnées (TypeScript)

// src/router.ts (en haut du fichier)
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
  }
}

Implémenter le guard global

// src/router.ts
import { ref } from 'vue'

export const isAuthenticated = ref(false)

router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isAuthenticated.value) {
    return ROUTES.HOME
  }

  return true
})

Le bouton dans le NavMenu importe et bascule directement la ref exportée depuis le router :

<!-- src/components/NavMenu.component.vue -->
<template>
  <NSpace align="center" :size="8">
    <RouterLink :to="ROUTES.HOME">
      <NButton quaternary :type="route.path === ROUTES.HOME ? 'primary' : 'default'">
        Accueil
      </NButton>
    </RouterLink>
    <NButton v-if="isAuthenticated" type="default" @click="isAuthenticated = false">
      Déconnexion
    </NButton>
    <NButton v-else type="primary" @click="isAuthenticated = true">
      Se connecter
    </NButton>
  </NSpace>
</template>

<script setup lang="ts">
import { useRoute } from 'vue-router'
import { isAuthenticated, ROUTES } from '@/router'

const route = useRoute()
</script>