Modèles et Bonnes Pratiques Vue.js
Guide complet pour le développement Vue.js 3 avec la Composition API (<script setup>), couvrant la conception de composants, la réactivité, la gestion d'état, le routage, les tests et les modèles SSR. Des conseils spécifiques à Nuxt sont inclus lorsqu'ils diffèrent de Vue standard.
Quand activer
Activez ce skill quand :
- Le projet utilise Vue.js (toute version), Nuxt, Vite + Vue, ou Pinia.
- L'utilisateur pose des questions sur l'architecture des composants Vue, les composables, la réactivité ou la gestion d'état.
- Vous examinez des composants monofichiers Vue (fichiers
.vue). - Vous configurez Vue Router, les stores Pinia, ou la configuration Vite/Vitest.
- Vous discutez de performances, de sécurité ou de modèles SSR spécifiques à Vue.
1. Structure du Projet
Disposition recommandée (par fonctionnalité)
src/
├── api/ # Client API et définitions de points de terminaison
├── assets/ # Ressources statiques (images, polices, icônes)
├── components/ # Composants partagés/réutilisables
│ ├── base/ # Primitives d'interface de base (Bouton, Champ, Modale)
│ └── features/ # Composants partagés spécifiques à une fonctionnalité
├── composables/ # Logique réutilisable de la Composition API
├── layouts/ # Mises en page (optionnel)
├── pages/ # Composants de page au niveau des routes
├── router/ # Configuration Vue Router
├── stores/ # Stores Pinia
├── types/ # Définitions de types TypeScript
├── utils/ # Fonctions utilitaires pures
└── App.vue # Composant racine
Nommage des fichiers
| Convention | Quand l'utiliser |
|---|---|
PascalCase.vue | Tous les composants (imposé par vue/multi-word-component-names) |
useCamelCase.ts | Composables |
camelCase.ts | Utilitaires, clients API, types |
Répertoires kebab-case | Segments de route, dossiers de fonctionnalités |
2. Architecture des Composants
Ordre des composants monofichiers
<script setup lang="ts">
// 1. Imports (vue → écosystème → absolu → relatif)
// 2. Props & Événements & Slots
// 3. Composables
// 4. État local (ref/reactive)
// 5. Propriétés calculées
// 6. Méthodes
// 7. Observateurs
// 8. Hooks de cycle de vie
</script>
<template>
<!-- Contenu du template -->
</template>
<style scoped>
/* Styles encapsulés */
</style>
Composants de présentation vs conteneur
- Composants conteneur : Gèrent la récupération des données, l'état et les effets de bord. Affichent les composants de présentation.
- Composants de présentation : Reçoivent des props, émettent des événements. Pas d'appels API, pas d'accès au store. Rendu pur.
Bonnes pratiques pour les props
// Props typées avec valeurs par défaut
interface Props {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
items: Item[];
}
const props = withDefaults(defineProps<Props>(), {
variant: "primary",
disabled: false,
});
- Fournissez toujours
type, etrequired/defaultle cas échéant. - Props booléennes :
isXxx,hasXxx,canXxx. - Ne mutez jamais les props — émettez des événements à la place.
- Pour la liaison v-model, utilisez
defineModel()(Vue 3.4+) oumodelValue+update:modelValue.
Événements
const emit = defineEmits<{
submit: [];
"update:modelValue": [value: string];
select: [id: string, index: number];
}>();
- Utilisez le kebab-case dans les templates (
@update:model-value). - Utilisez le camelCase dans le script (
emit("update:modelValue", val)).
3. Composables (Logique réutilisable)
Structure
// composables/useDebounce.ts
export function useDebounce<T>(value: MaybeRef<T>, delay: number): Ref<T> {
const debounced = ref(toValue(value)) as Ref<T>;
let timer: ReturnType<typeof setTimeout>;
watch(
() => toValue(value),
(newVal) => {
clearTimeout(timer);
timer = setTimeout(() => { debounced.value = newVal; }, delay);
}
);
onUnmounted(() => clearTimeout(timer));
return readonly(debounced);
}
Règles
- Doit commencer par le préfixe
use. - Retournez des valeurs réactives (
ref,computed,reactive), jamais de primitives simples. - Acceptez des entrées réactives via
MaybeRef/toRef()/toValue(). - Nettoyez les effets de bord dans
onUnmountedouonCleanupd'observateur. - Pas d'effets de bord au niveau du module.
vs Mixins
Les composables remplacent entièrement les mixins Vue 2 :
- Mixins : Flux de données opaque, collisions de source de vérité, conflits de noms.
- Composables : Importations explicites, valeurs de retour claires, composables et arborescences optimisables.
4. Gestion d'État
Quand utiliser quoi
| Modèle | Cas d'utilisation |
|---|---|
ref() / reactive() | État local du composant |
| Props + Événements | Communication parent-enfant |
| Provide / Inject | Thème, configuration, API de plugin |
| Store Pinia | État global, partagé, complexe |
| Composable d'état serveur | Données API avec cache (envelopper fetch/TanStack Query) |
Store Pinia Setup (Recommandé)
// stores/useCartStore.ts
export const useCartStore = defineStore("cart", () => {
const items = ref<CartItem[]>([]);
const isLoading = ref(false);
const totalPrice = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
);
const itemCount = computed(() =>
items.value.reduce((sum, i) => sum + i.quantity, 0)
);
async function addItem(productId: string) {
isLoading.value = true;
try {
const item = await fetchProduct(productId);
const existing = items.value.find(i => i.id === item.id);
if (existing) existing.quantity++;
else items.value.push({ ...item, quantity: 1 });
} finally {
isLoading.value = false;
}
}
return { items, isLoading, totalPrice, itemCount, addItem };
});
- Utilisez la syntaxe Setup Store (pas Options Store).
- Privilégiez les actions pour les mutations métier et
$patch()pour les mises à jour groupées. - Chaque action asynchrone : gérez le chargement + succès + erreur.
5. Vue Router
Définitions de routes
const routes = [
{
path: "/users/:id",
name: "user-detail",
component: () => import("@/pages/UserDetail.vue"), // paresseux
props: true, // passer les paramètres comme props
meta: { requiresAuth: true },
},
];
Gardes de navigation
router.beforeEach((to, from) => {
const { isLoggedIn } = useAuthStore();
if (to.meta.requiresAuth && !isLoggedIn) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
Paramètres de route réactifs
Lorsqu'un composant reste monté mais que les paramètres de route changent :
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));
6. Modèles de Template
Syntaxe du template
<!-- v-if/v-else-if/v-else -->
<div v-if="isLoading">Chargement...</div>
<div v-else-if="error">Erreur : {{ error }}</div>
<div v-else>{{ content }}</div>
<!-- v-show pour les basculements fréquents -->
<div v-show="isOpen">Contenu basculé</div>
<!-- v-for avec clés stables -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
<!-- Liste filtrée par calcul (pas v-if + v-for sur le même élément) -->
<div v-for="item in activeItems" :key="item.id">{{ item.name }}</div>
<!-- Gestion d'événements -->
<form @submit.prevent="handleSubmit">
<button type="submit">Enregistrer</button>
</form>
<!-- v-model -->
<input v-model="name" />
<CustomInput v-model="value" v-model:title="title" />
7. Performances
| Technique | Quand l'utiliser |
|---|---|
v-memo | Éléments de liste qui changent rarement |
v-once | Contenu rendu une fois et statique pour toujours |
shallowRef() | Grandes structures de données remplacées en bloc |
shallowReactive() | Seules les propriétés de premier niveau sont réactives |
v-show plutôt que v-if | Basculements de visibilité fréquents |
<KeepAlive :max="10"> | Mettre en cache les vues basculées |
| Routes paresseuses | () => import(...) pour les routes non critiques |
Suspense | Chargement de composant asynchrone avec fallback |
8. Tests
Stack
- Vitest pour les tests unitaires et de composants
- Vue Test Utils pour le montage et l'interaction
- @pinia/testing pour le mock des stores
- Playwright pour les tests E2E
Modèle de test de composant
import { mount } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import UserCard from "./UserCard.vue";
beforeEach(() => { setActivePinia(createPinia()); });
it("rend et émet", async () => {
const wrapper = mount(UserCard, {
props: { user: { id: "1", name: "Alice" } },
});
expect(wrapper.text()).toContain("Alice");
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("select")![0]).toEqual(["1"]);
});
9. Modèles Spécifiques à Nuxt
Auto-importations
Nuxt auto-importe ref, computed, watch, useFetch, useAsyncData, etc. Utilisez-les directement sans importation. Pour les projets non Nuxt, importez toujours explicitement.
useAsyncData / useFetch
const { data: user, pending, error, refresh } = await useAsyncData(
"user", // clé unique pour le cache
() => $fetch(`/api/users/${id}`),
);
const { data: posts } = await useFetch("/api/posts", {
query: { page: 1 },
key: "posts-page-1", // déduplication des requêtes
});
Routes serveur
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, z.object({
id: z.string().uuid(),
}).parse);
// ... récupérer et retourner
});
Configuration runtime
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// serveur uniquement
apiSecret: "",
// public (exposé au client)
public: {
apiBase: "https://api.example.com",
},
},
});
10. Nouvelles API Vue 3.5+
Déstructuration réactive des props
Vue 3.5 a stabilisé la déstructuration réactive des props — les variables déstructurées de defineProps() sont automatiquement réactives :
// Vue 3.5+ : les props déstructurées sont réactives (pas besoin de toRefs)
const { count = 0, msg = "hello" } = defineProps<{
count?: number;
msg?: string;
}>();
// Limitation : impossible d'observer directement une prop déstructurée
watch(() => count, (newVal) => { ... }); // PASS : un getter est requis
useTemplateRef()
Remplacez les refs simples nommées par useTemplateRef() pour les références de template :
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef<HTMLInputElement>("input");
// "input" correspond à l'attribut ref="input" dans le template, pas au nom de la variable
Prend en charge les IDs ref dynamiques : useTemplateRef(dynamicRefId).
onWatcherCleanup()
API de nettoyage d'observateur importable globalement (Vue 3.5+). Elle doit être appelée de manière synchrone à l'intérieur du callback de l'observateur :
import { watch, onWatcherCleanup } from "vue";
watch(userId, async (newId) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort());
// ... récupérer avec signal
});
useId()
Génération d'ID unique stable pour le SSR pour les éléments de formulaire et l'accessibilité :
import { useId } from "vue";
const id = useId();
Téléportation defer
<Teleport defer> permet de téléporter vers des cibles rendues dans le même cycle :
<Teleport defer to="#container">Contenu</Teleport>
<div id="container"></div>
Hydratation paresseuse (SSR)
defineAsyncComponent() prend désormais en charge la stratégie hydrate :
import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
loader: () => import("./Comp.vue"),
hydrate: hydrateOnVisible(),
});
Anti-patrons
| Anti-patron | Pourquoi c'est faux | La correction |
|---|---|---|
Déstructuration de defineProps() (Vue < 3.5) | Capture un instantané, perd la réactivité | Accéder via props.xxx ou utiliser toRefs() |
watch() sur une prop déstructurée (Vue 3.5+) | Erreur à la compilation — les props déstructurées ne peuvent pas être observées directement | Utiliser un getter enveloppant : watch(() => count, ...) |
v-if + v-for sur le même élément | Ordre d'exécution ambigu | Utiliser un tableau filtré calculé |
Clé v-for = index | État cassé lors du réordonnancement | Utiliser des IDs de base de données stables |
| Muter les props | Viole le flux de données unidirectionnel | Émettre des événements ou utiliser v-model |
v-html avec contenu utilisateur | Vulnérabilité XSS | Nettoyer avec DOMPurify |
| Mixins dans Vue 3 | Opaque, sujet aux collisions | Remplacer par des composables |
| Effets de bord au niveau du module dans un composable | Partagés entre instances | Limiter la portée dans onMounted + onUnmounted |
reactive() pour un état remplaçable | Le remplacement casse la réactivité | Utiliser ref() à la place |
| Observateur sans nettoyage | Fuites mémoire, conditions de course | Utiliser onCleanup ou onWatcherCleanup() (Vue 3.5+) |
| Options API dans du nouveau code Vue 3 | L'écosystème évolue vers la Composition API | Utiliser <script setup> |
| Ref simple pour les références de template | Pas de support de ref dynamique, correspondance de nom fragile | Utiliser useTemplateRef() (Vue 3.5+) |
Skills associés
accessibility— ARIA, HTML sémantique, gestion du focusfrontend-patterns— Architecture frontend multi-cadrestypescript— Bonnes pratiques TypeScript appliquées aux projets Vuecoding-standards— Standards généraux de qualité de code