Table of Contents
Comment fonctionne l’accès aux URL dynamiques dans Vue Router ?
Introduction au routing dans les SPA
Les Single Page Applications (SPA) ont révolutionné la façon dont nous construisons des applications web. Contrairement aux applications traditionnelles où chaque navigation déclenche un rechargement complet de la page, les SPA chargent une seule page HTML et mettent à jour dynamiquement le contenu en fonction des interactions utilisateur.
Vue Router est la bibliothèque officielle de routing pour Vue.js. Elle permet de :
- Mapper des URLs à des composants Vue
- Gérer l’historique de navigation du navigateur
- Passer des paramètres entre les pages
- Protéger des routes avec des guards de navigation
- Créer des architectures d’URL complexes avec des routes imbriquées
Dans ce guide complet, nous allons explorer en profondeur les routes dynamiques avec Vue Router, en utilisant un exemple concret d’application e-commerce.
Pourquoi les routes dynamiques sont essentielles ?
Imaginez une boutique en ligne avec des milliers de produits. Créer une route statique pour chaque produit serait impossible et non maintenable :
// Mauvaise approche : routes statiques
routes: [
{ path: '/products/1', component: Product1 },
{ path: '/products/2', component: Product2 },
{ path: '/products/3', component: Product3 },
// ... des milliers de routes
]
Les routes dynamiques résolvent ce problème en utilisant des segments de paramètres :
// Bonne approche : route dynamique
routes: [
{ path: '/products/:id', component: ProductDetail }
]
Configuration des routes dynamiques
Installation et configuration de base
Avant de commencer, assurez-vous d’avoir Vue Router installé dans votre projet :
npm install vue-router@4
Créez ensuite votre fichier de configuration de routes :
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '@/views/Home.vue'
import ProductList from '@/views/ProductList.vue'
import ProductDetail from '@/views/ProductDetail.vue'
import Cart from '@/views/Cart.vue'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/products',
name: 'products',
component: ProductList
},
{
path: '/products/:id',
name: 'product-detail',
component: ProductDetail,
props: true
},
{
path: '/cart',
name: 'cart',
component: Cart
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
Syntaxe des paramètres dynamiques
Vue Router utilise la syntaxe :paramName pour définir des segments dynamiques :
// Paramètre simple
{ path: '/users/:userId', component: UserProfile }
// Plusieurs paramètres
{ path: '/users/:userId/posts/:postId', component: UserPost }
// Paramètre avec expression régulière personnalisée
{ path: '/products/:id(\\d+)', component: ProductDetail } // Seulement des chiffres
// Paramètre répétable (zero ou plus)
{ path: '/files/:pathMatch(.*)*', component: FileExplorer }
// Paramètre répétable (un ou plus)
{ path: '/categories/:categories+', component: CategoryView }
Paramètres multiples et optionnels
Routes avec plusieurs paramètres
Dans une application e-commerce, vous pourriez avoir besoin de routes avec plusieurs paramètres :
// src/router/index.ts
const routes: RouteRecordRaw[] = [
// Produit dans une catégorie
{
path: '/category/:categorySlug/product/:productId',
name: 'category-product',
component: ProductDetail,
props: true
},
// Avis d'un utilisateur sur un produit
{
path: '/products/:productId/reviews/:reviewId',
name: 'product-review',
component: ReviewDetail,
props: true
},
// Commande d'un utilisateur
{
path: '/users/:userId/orders/:orderId',
name: 'user-order',
component: OrderDetail,
props: true
}
]
Paramètres optionnels
Pour rendre un paramètre optionnel, ajoutez un ? après le nom du paramètre :
const routes: RouteRecordRaw[] = [
// La page affiche soit tous les produits, soit ceux d'une catégorie
{
path: '/products/:category?',
name: 'products',
component: ProductList,
props: true
},
// Pagination optionnelle
{
path: '/blog/:page?',
name: 'blog',
component: BlogList,
props: route => ({
page: parseInt(route.params.page as string) || 1
})
}
]
Exemple complet avec composant
<!-- src/views/ProductList.vue -->
<template>
<div class="product-list">
<h1 v-if="category">
Produits dans la catégorie : {{ categoryName }}
</h1>
<h1 v-else>Tous les produits</h1>
<div class="filters">
<select v-model="selectedCategory" @change="filterByCategory">
<option value="">Toutes les catégories</option>
<option
v-for="cat in categories"
:key="cat.slug"
:value="cat.slug"
>
{{ cat.name }}
</option>
</select>
</div>
<div class="products-grid">
<ProductCard
v-for="product in filteredProducts"
:key="product.id"
:product="product"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useProductStore } from '@/stores/products'
import ProductCard from '@/components/ProductCard.vue'
// Props reçues via la route
const props = defineProps<{
category?: string
}>()
const router = useRouter()
const route = useRoute()
const productStore = useProductStore()
const selectedCategory = ref(props.category || '')
const categories = computed(() => productStore.categories)
const categoryName = computed(() => {
const cat = categories.value.find(c => c.slug === props.category)
return cat?.name || props.category
})
const filteredProducts = computed(() => {
if (props.category) {
return productStore.getProductsByCategory(props.category)
}
return productStore.allProducts
})
function filterByCategory() {
if (selectedCategory.value) {
router.push({ name: 'products', params: { category: selectedCategory.value } })
} else {
router.push({ name: 'products' })
}
}
// Synchroniser le select avec l'URL
watch(() => props.category, (newCategory) => {
selectedCategory.value = newCategory || ''
})
</script>
Query params vs Route params
Différences fondamentales
| Aspect | Route Params | Query Params |
|---|---|---|
| Syntaxe URL | /products/123 | /products?id=123 |
| Définition | Dans la config des routes | N’importe où |
| Obligatoire | Oui (sauf avec ?) | Non |
| Usage typique | Identifier une ressource | Filtrer, trier, paginer |
| SEO | Meilleur pour les ressources | Moins important |
| Accès | route.params.id | route.query.id |
Quand utiliser quoi ?
Route params - Pour identifier une ressource unique :
// Bon usage des route params
/products/123 // Un produit spécifique
/users/john-doe // Un utilisateur spécifique
/categories/electronics // Une catégorie spécifique
Query params - Pour filtrer, trier ou paginer :
// Bon usage des query params
/products?category=electronics&sort=price&order=asc
/products?page=2&limit=20
/search?q=laptop&brand=apple&minPrice=500
Exemple combiné
// Configuration de la route
{
path: '/products/:category?',
name: 'products',
component: ProductList,
props: route => ({
category: route.params.category,
page: parseInt(route.query.page as string) || 1,
sort: route.query.sort || 'name',
order: route.query.order || 'asc',
search: route.query.search || ''
})
}
<!-- Composant utilisant les deux -->
<template>
<div>
<h1>{{ category ? `Catégorie: ${category}` : 'Tous les produits' }}</h1>
<input
v-model="searchQuery"
@input="updateFilters"
placeholder="Rechercher..."
/>
<select v-model="sortBy" @change="updateFilters">
<option value="name">Nom</option>
<option value="price">Prix</option>
<option value="date">Date</option>
</select>
<Pagination
:current-page="page"
:total-pages="totalPages"
@page-change="changePage"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const props = defineProps<{
category?: string
page: number
sort: string
order: string
search: string
}>()
const router = useRouter()
const route = useRoute()
const searchQuery = ref(props.search)
const sortBy = ref(props.sort)
function updateFilters() {
router.push({
name: 'products',
params: { category: props.category },
query: {
...route.query,
search: searchQuery.value || undefined,
sort: sortBy.value,
page: 1 // Revenir à la page 1 quand on filtre
}
})
}
function changePage(newPage: number) {
router.push({
name: 'products',
params: { category: props.category },
query: {
...route.query,
page: newPage
}
})
}
</script>
Props: true pour découpler les composants
Le problème du couplage
Sans props: true, vos composants sont fortement couplés à Vue Router :
<!-- Couplé à Vue Router - difficile à tester -->
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
const productId = route.params.id // Dépendance directe
</script>
La solution avec props: true
Avec props: true, le composant reçoit les paramètres comme des props normales :
// Configuration de la route
{
path: '/products/:id',
name: 'product-detail',
component: ProductDetail,
props: true // Les params deviennent des props
}
<!-- Découplé - facilement testable -->
<script setup lang="ts">
// Le composant ne sait même pas qu'il est utilisé avec Vue Router
const props = defineProps<{
id: string
}>()
// Utilisation simple
console.log(props.id)
</script>
Modes de props avancés
Vue Router offre trois modes pour les props :
const routes: RouteRecordRaw[] = [
// Mode booléen : route.params devient props
{
path: '/products/:id',
component: ProductDetail,
props: true
},
// Mode objet : props statiques
{
path: '/promotions',
component: ProductList,
props: { featured: true, limit: 10 }
},
// Mode fonction : transformation personnalisée
{
path: '/products/:id',
component: ProductDetail,
props: route => ({
id: parseInt(route.params.id as string),
preview: route.query.preview === 'true',
referrer: route.query.ref || 'direct'
})
}
]
Exemple complet avec typage TypeScript
// types/product.ts
export interface ProductDetailProps {
id: number
preview: boolean
referrer: string
}
// router/index.ts
{
path: '/products/:id',
name: 'product-detail',
component: () => import('@/views/ProductDetail.vue'),
props: (route): ProductDetailProps => ({
id: parseInt(route.params.id as string),
preview: route.query.preview === 'true',
referrer: (route.query.ref as string) || 'direct'
})
}
<!-- views/ProductDetail.vue -->
<template>
<div class="product-detail" :class="{ 'preview-mode': preview }">
<div v-if="preview" class="preview-banner">
Mode aperçu - Ce produit n'est pas encore publié
</div>
<div v-if="loading" class="loading">Chargement...</div>
<template v-else-if="product">
<img :src="product.image" :alt="product.name" />
<h1>{{ product.name }}</h1>
<p class="price">{{ formatPrice(product.price) }}</p>
<p class="description">{{ product.description }}</p>
<button @click="addToCart" :disabled="preview">
{{ preview ? 'Indisponible en aperçu' : 'Ajouter au panier' }}
</button>
</template>
<div v-else class="not-found">
Produit non trouvé
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useProductStore } from '@/stores/products'
import { useCartStore } from '@/stores/cart'
import type { Product } from '@/types/product'
const props = defineProps<{
id: number
preview: boolean
referrer: string
}>()
const productStore = useProductStore()
const cartStore = useCartStore()
const product = ref<Product | null>(null)
const loading = ref(true)
async function loadProduct() {
loading.value = true
try {
product.value = await productStore.fetchProduct(props.id)
// Tracking analytics
if (props.referrer !== 'direct') {
trackReferral(props.referrer, props.id)
}
} catch (error) {
console.error('Erreur lors du chargement du produit:', error)
product.value = null
} finally {
loading.value = false
}
}
function addToCart() {
if (product.value && !props.preview) {
cartStore.addItem(product.value)
}
}
function formatPrice(price: number): string {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(price)
}
function trackReferral(source: string, productId: number) {
// Envoi des données analytics
console.log(`Visite depuis ${source} pour le produit ${productId}`)
}
onMounted(loadProduct)
// Recharger si l'ID change (navigation entre produits)
watch(() => props.id, loadProduct)
</script>
Navigation programmatique
Vue Router offre plusieurs méthodes pour naviguer programmatiquement dans votre application.
Méthodes de navigation
router.push()
Ajoute une nouvelle entrée dans l’historique :
import { useRouter } from 'vue-router'
const router = useRouter()
// Navigation simple par chemin
router.push('/products')
// Navigation par nom de route
router.push({ name: 'products' })
// Avec paramètres
router.push({ name: 'product-detail', params: { id: '123' } })
// Avec query params
router.push({
name: 'products',
query: { category: 'electronics', sort: 'price' }
})
// Combinaison complète
router.push({
name: 'product-detail',
params: { id: '123' },
query: { preview: 'true' },
hash: '#reviews'
})
router.replace()
Remplace l’entrée actuelle sans ajouter à l’historique :
// Utile après une action qui ne doit pas être "retourable"
async function submitOrder() {
const order = await orderStore.createOrder()
// L'utilisateur ne peut pas revenir au formulaire avec "retour"
router.replace({
name: 'order-confirmation',
params: { orderId: order.id }
})
}
// Utile pour les redirections
function handleLogin() {
const redirectTo = route.query.redirect as string || '/'
router.replace(redirectTo)
}
router.go()
Navigation dans l’historique :
// Équivalent à cliquer sur le bouton "Retour" du navigateur
router.go(-1)
// Avancer d'une page
router.go(1)
// Reculer de 3 pages
router.go(-3)
// Rafraîchir la page actuelle (rarement utilisé)
router.go(0)
Tableau comparatif des méthodes de navigation
| Méthode | Historique | Usage typique | Exemple |
|---|---|---|---|
push() | Ajoute | Navigation standard | Clic sur un lien |
replace() | Remplace | Redirection après action | Après login |
go(n) | Navigate | Navigation historique | Boutons retour/avancer |
back() | Recule | Raccourci pour go(-1) | Bouton retour |
forward() | Avance | Raccourci pour go(1) | Bouton avancer |
Exemple complet : Flux d’achat e-commerce
<!-- views/Checkout.vue -->
<template>
<div class="checkout">
<div class="steps">
<span :class="{ active: step === 1 }">1. Panier</span>
<span :class="{ active: step === 2 }">2. Livraison</span>
<span :class="{ active: step === 3 }">3. Paiement</span>
<span :class="{ active: step === 4 }">4. Confirmation</span>
</div>
<component :is="currentStepComponent" @next="nextStep" @back="previousStep" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import CartReview from '@/components/checkout/CartReview.vue'
import ShippingForm from '@/components/checkout/ShippingForm.vue'
import PaymentForm from '@/components/checkout/PaymentForm.vue'
import Confirmation from '@/components/checkout/Confirmation.vue'
const router = useRouter()
const route = useRoute()
const step = computed(() => parseInt(route.query.step as string) || 1)
const steps = {
1: CartReview,
2: ShippingForm,
3: PaymentForm,
4: Confirmation
}
const currentStepComponent = computed(() => steps[step.value] || CartReview)
function nextStep() {
if (step.value < 4) {
router.push({
name: 'checkout',
query: { step: step.value + 1 }
})
} else {
// Commande terminée, remplacer pour éviter de revenir au checkout
router.replace({ name: 'order-success' })
}
}
function previousStep() {
if (step.value > 1) {
router.go(-1) // Utilise l'historique naturel
} else {
router.push({ name: 'cart' })
}
}
</script>
Guards de navigation
Les guards permettent de contrôler l’accès aux routes et d’exécuter du code avant/après la navigation.
Types de guards
Guard global : beforeEach
// router/index.ts
import { useAuthStore } from '@/stores/auth'
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
// Routes qui nécessitent une authentification
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next({
name: 'login',
query: { redirect: to.fullPath }
})
return
}
// Routes admin
if (to.meta.requiresAdmin && !authStore.isAdmin) {
next({ name: 'forbidden' })
return
}
next()
})
Guard global : afterEach
router.afterEach((to, from) => {
// Mise à jour du titre de la page
document.title = to.meta.title as string || 'Mon E-commerce'
// Tracking analytics
trackPageView(to.fullPath)
// Scroll to top
window.scrollTo(0, 0)
})
Guard par route : beforeEnter
const routes: RouteRecordRaw[] = [
{
path: '/checkout',
name: 'checkout',
component: Checkout,
beforeEnter: (to, from, next) => {
const cartStore = useCartStore()
if (cartStore.isEmpty) {
next({ name: 'cart', query: { error: 'empty' } })
return
}
next()
}
},
{
path: '/products/:id',
name: 'product-detail',
component: ProductDetail,
beforeEnter: async (to, from, next) => {
const productStore = useProductStore()
const id = parseInt(to.params.id as string)
// Vérifier si le produit existe
const exists = await productStore.productExists(id)
if (!exists) {
next({ name: 'not-found' })
return
}
next()
}
}
]
Guard dans le composant
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
const hasUnsavedChanges = ref(false)
// Appelé quand on quitte ce composant
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
const answer = window.confirm(
'Vous avez des modifications non sauvegardées. Voulez-vous vraiment quitter ?'
)
if (!answer) return false
}
return true
})
// Appelé quand la route change mais le composant est réutilisé
onBeforeRouteUpdate((to, from) => {
// Utile quand on navigue de /products/1 vers /products/2
loadProduct(to.params.id)
})
</script>
Routes imbriquées (Nested Routes)
Les routes imbriquées permettent de créer des layouts complexes avec des sous-vues.
Configuration
const routes: RouteRecordRaw[] = [
{
path: '/account',
name: 'account',
component: AccountLayout,
meta: { requiresAuth: true },
children: [
{
path: '', // /account
name: 'account-dashboard',
component: AccountDashboard
},
{
path: 'profile', // /account/profile
name: 'account-profile',
component: AccountProfile
},
{
path: 'orders', // /account/orders
name: 'account-orders',
component: AccountOrders
},
{
path: 'orders/:orderId', // /account/orders/123
name: 'account-order-detail',
component: OrderDetail,
props: true
},
{
path: 'settings', // /account/settings
name: 'account-settings',
component: AccountSettings,
children: [
{
path: 'security', // /account/settings/security
name: 'account-security',
component: SecuritySettings
},
{
path: 'notifications', // /account/settings/notifications
name: 'account-notifications',
component: NotificationSettings
}
]
}
]
}
]
Composant Layout avec RouterView
<!-- layouts/AccountLayout.vue -->
<template>
<div class="account-layout">
<aside class="sidebar">
<nav>
<RouterLink :to="{ name: 'account-dashboard' }">
Tableau de bord
</RouterLink>
<RouterLink :to="{ name: 'account-profile' }">
Mon profil
</RouterLink>
<RouterLink :to="{ name: 'account-orders' }">
Mes commandes
</RouterLink>
<RouterLink :to="{ name: 'account-settings' }">
Paramètres
</RouterLink>
</nav>
</aside>
<main class="content">
<!-- Les composants enfants sont rendus ici -->
<RouterView />
</main>
</div>
</template>
<style scoped>
.account-layout {
display: grid;
grid-template-columns: 250px 1fr;
min-height: 100vh;
}
.sidebar nav {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 2rem;
}
.sidebar a.router-link-active {
color: var(--primary-color);
font-weight: bold;
}
</style>
Bonnes pratiques
1. Nommez toujours vos routes
// Mauvais - difficile à maintenir
router.push('/products/123')
// Bon - découplé du chemin
router.push({ name: 'product-detail', params: { id: '123' } })
2. Utilisez toujours props: true
Cela rend vos composants testables et réutilisables en dehors du contexte de routing.
3. Typez vos routes avec TypeScript
// types/router.ts
import type { RouteRecordRaw } from 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
requiresAdmin?: boolean
title?: string
layout?: 'default' | 'admin' | 'blank'
}
}
4. Centralisez la logique de navigation
// composables/useNavigation.ts
export function useNavigation() {
const router = useRouter()
return {
goToProduct(id: number) {
router.push({ name: 'product-detail', params: { id: String(id) } })
},
goToCategory(slug: string) {
router.push({ name: 'category', params: { slug } })
},
goToCheckout() {
router.push({ name: 'checkout' })
},
goBack() {
router.go(-1)
}
}
}
5. Gérez les erreurs de navigation
router.push({ name: 'product-detail', params: { id: '123' } })
.catch(error => {
if (error.name !== 'NavigationDuplicated') {
console.error('Erreur de navigation:', error)
}
})
6. Utilisez le lazy loading pour les routes
const routes: RouteRecordRaw[] = [
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
children: [
{
path: 'dashboard',
component: () => import('@/views/admin/Dashboard.vue')
}
]
}
]
Pièges courants
1. Oublier de convertir les paramètres
Les paramètres d’URL sont toujours des chaînes de caractères :
// Piège : comparaison incorrecte
const id = route.params.id // "123" (string)
products.find(p => p.id === id) // Ne trouve rien si p.id est un number
// Solution
const id = parseInt(route.params.id as string)
products.find(p => p.id === id) // Fonctionne correctement
2. Ne pas réagir aux changements de paramètres
Quand on navigue entre /products/1 et /products/2, le composant n’est pas recréé :
<script setup>
// Piège : ne se met pas à jour
onMounted(() => {
loadProduct(props.id) // Appelé une seule fois
})
// Solution : watcher
watch(() => props.id, (newId) => {
loadProduct(newId)
}, { immediate: true })
</script>
3. Utiliser $route dans les templates avec Composition API
<!-- Piège : casse la réactivité -->
<template>
<div>{{ $route.params.id }}</div>
</template>
<!-- Solution : utiliser useRoute() -->
<script setup>
const route = useRoute()
</script>
<template>
<div>{{ route.params.id }}</div>
</template>
4. Navigation vers la même route
// Piège : déclenche une erreur NavigationDuplicated
router.push({ name: 'current-route' })
// Solution : vérifier avant de naviguer
if (route.name !== 'current-route') {
router.push({ name: 'current-route' })
}
// Ou ignorer l'erreur
router.push({ name: 'current-route' }).catch(() => {})
5. Oublier le trailing slash
// Ces routes sont différentes !
{ path: '/products' } // /products
{ path: '/products/' } // /products/
// Solution : configuration du router
const router = createRouter({
strict: true, // Différencie /products et /products/
// ou
strict: false // Les traite comme identiques (défaut)
})
Conclusion
Vue Router est un outil puissant qui va bien au-delà du simple mapping URL vers composant. Dans cet article, nous avons couvert :
- Les routes dynamiques avec paramètres simples, multiples et optionnels
- La différence entre route params et query params et quand utiliser chacun
- L’option props: true pour découpler vos composants du router
- La navigation programmatique avec push, replace et go
- Les guards de navigation pour protéger vos routes
- Les routes imbriquées pour des layouts complexes
Ces concepts sont essentiels pour construire des applications Vue.js professionnelles. Je vous encourage à expérimenter avec ces fonctionnalités dans vos propres projets, en commençant par un cas d’utilisation simple et en ajoutant progressivement de la complexité.
N’oubliez pas que la documentation officielle de Vue Router est une excellente ressource pour approfondir ces sujets. Bonne programmation !
In-Article Ad
Dev Mode
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
Creer un systeme de routage personnalise avec Vue.js et les composants dynamiques
Apprenez a construire un routeur Vue.js de zero avec les composants dynamiques. Gerez la navigation et les routes 404 sans dependances externes.
Securiser votre application Vue.js avec Vue Router et les jetons JWT
Implementez une authentification securisee avec Vue Router et Vuex. Gerez les jetons JWT et protegez vos routes sensibles efficacement.
Vue.js et TypeScript : Guide Complet pour la Composition API avec Typage Statique
Combinez Vue 3, TypeScript et la Composition API pour un code plus sur. Configuration, typage et bonnes pratiques pour vos projets Vue.