Architecture de composants¶
Objectifs d'apprentissage¶
Objectifs:
- ✅ Identifier quand créer un composant
- ✅ Structurer une application Vue en composants réutilisables
- ✅ Organiser vos fichiers et dossiers efficacement
- ✅ Comprendre les relations entre composants (parent-enfant)
- ✅ Appliquer le principe de responsabilité unique
📚 1: Pourquoi des composants?¶
Le problème sans composants¶
Imaginez une application dans un seul fichier de 2000 lignes:
<!-- App.vue - MAUVAIS EXEMPLE ❌ -->
<template>
<div>
<!-- Header -->
<header>...</header>
<!-- Navigation -->
<nav>...</nav>
<!-- Liste des salles -->
<div class="rooms">...</div>
<!-- Formulaire d'ajout -->
<form>...</form>
<!-- Modal -->
<div class="modal">...</div>
<!-- Footer -->
<footer>...</footer>
</div>
</template>
<script>
export default {
data() {
return {
// 50 variables ici...
}
},
methods: {
// 30 méthodes ici...
}
}
</script>
<style>
/* 500 lignes de CSS... */
</style>
Problèmes:
- 🚫 Difficile à maintenir
- 🚫 Code non réutilisable
- 🚫 Impossible de travailler en équipe efficacement
- 🚫 Bugs difficiles à isoler
- 🚫 Lent à charger
La solution: Les composants¶
<!-- App.vue - BON EXEMPLE ✅ -->
<template>
<div>
<AppHeader />
<AppNavigation />
<RoomsList />
<AddRoomModal v-if="showModal" />
<AppFooter />
</div>
</template>
<script>
import AppHeader from './components/AppHeader.vue';
import AppNavigation from './components/AppNavigation.vue';
import RoomsList from './components/RoomsList.vue';
import AddRoomModal from './components/AddRoomModal.vue';
import AppFooter from './components/AppFooter.vue';
export default {
components: {
AppHeader,
AppNavigation,
RoomsList,
AddRoomModal,
AppFooter
}
}
</script>
Avantages:
- ✅ Code organisé et lisible
- ✅ Composants réutilisables
- ✅ Travail d'équipe facilité
- ✅ Bugs isolés
- ✅ Performance optimisée
🏗️ 2: Principe de responsabilité unique¶
La règle d'or¶
Un composant = Une responsabilité
❌ Mauvais exemple: Composant qui fait trop¶
<!-- UserDashboard.vue - TROP DE RESPONSABILITÉS -->
<template>
<div>
<!-- Affiche le profil -->
<div class="profile">
<img :src="user.avatar" />
<h2>{{ user.name }}</h2>
<button @click="editProfile">Modifier</button>
</div>
<!-- Affiche les statistiques -->
<div class="stats">
<div>Posts: {{ user.posts }}</div>
<div>Followers: {{ user.followers }}</div>
</div>
<!-- Affiche la liste des posts -->
<div class="posts">
<div v-for="post in posts" :key="post.id">
<h3>{{ post.title }}</h3>
<p>{{ post.content }}</p>
<button @click="likePost(post.id)">Like</button>
<button @click="deletePost(post.id)">Delete</button>
</div>
</div>
<!-- Formulaire d'ajout de post -->
<form @submit.prevent="addPost">
<input v-model="newPost.title" />
<textarea v-model="newPost.content"></textarea>
<button>Publier</button>
</form>
</div>
</template>
<script>
export default {
data() {
return {
user: {},
posts: [],
newPost: {},
// ... beaucoup trop de données
}
},
methods: {
editProfile() { /* ... */ },
likePost() { /* ... */ },
deletePost() { /* ... */ },
addPost() { /* ... */ },
// ... beaucoup trop de méthodes
}
}
</script>
Problèmes:
- Composant fait 4 choses différentes
- Difficile à tester
- Difficile à maintenir
✅ Bon exemple: Découpage logique¶
<!-- UserDashboard.vue - BIEN DÉCOUPÉ -->
<template>
<div class="dashboard">
<UserProfile :user="user" @edit="editProfile" />
<UserStats :stats="userStats" />
<PostList :posts="posts" @like="likePost" @delete="deletePost" />
<PostForm @submit="addPost" />
</div>
</template>
<script>
import UserProfile from './UserProfile.vue';
import UserStats from './UserStats.vue';
import PostList from './PostList.vue';
import PostForm from './PostForm.vue';
export default {
components: {
UserProfile,
UserStats,
PostList,
PostForm
},
// Logique simplifiée car déléguée aux composants enfants
}
</script>
Chaque composant a UNE seule responsabilité:
UserProfile→ Afficher et éditer le profilUserStats→ Afficher les statistiquesPostList→ Afficher la liste des postsPostForm→ Formulaire d'ajout
📁 3: Organisation des fichiers¶
Structure recommandée pour vos projets¶
src/
├── components/
│ ├── common/ ← Composants réutilisables partout
│ │ ├── BaseButton.vue
│ │ ├── BaseInput.vue
│ │ ├── BaseModal.vue
│ │ └── LoadingSpinner.vue
│ │
│ ├── layout/ ← Composants de structure de mise en page
│ │ ├── AppHeader.vue
│ │ ├── AppFooter.vue
│ │ ├── AppSidebar.vue
│ │ └── AppNavigation.vue
│ │
│ └── [specific]/ ← Composants spécifiques au domaine
│ ├── RoomCard.vue
│ ├── RoomList.vue
│ ├── MemoryCard.vue
│ └── MemoryForm.vue
│
├── views/ ← Pages (routes)
│ ├── HomeView.vue
│ ├── MuseumView.vue
│ ├── RoomView.vue
│ └── SearchView.vue
│
├── stores/ ← Stores Pinia
│ ├── museumStore.js
│ └── memoryStore.js
│
├── router/
│ └── index.js
│
├── assets/
│ ├── styles/
│ │ ├── main.css
│ │ ├── variables.css
│ │ └── animations.css
│ └── images/
│
├── composables/ ← Logique réutilisable
│ └── useLocalStorage.js
│
├── utils/ ← Fonctions utilitaires
│ └── helpers.js
│
├── App.vue
└── main.js
Conventions de nommage¶
Components:
- PascalCase:
UserProfile.vue,MemoryCard.vue - Préfixe pour composants de base:
Base,AppBaseButton.vueAppHeader.vue
Views (pages):
- PascalCase avec suffixe
View:HomeView.vue,RoomView.vue
Stores:
- camelCase avec suffixe
Store:museumStore.js,memoryStore.js
🎨 4: Types de composants¶
1. Composants de présentation (Presentational)¶
Rôle: Afficher des données, pas de logique métier*
<!-- MemoryCard.vue - PRÉSENTATIONNEL | Dans cet exemple: carte d'un contenu -->
<template>
<div class="memory-card">
<img :src="memory.image" :alt="memory.title" />
<h3>{{ memory.title }}</h3>
<p>{{ memory.description }}</p>
<div class="tags">
<span v-for="tag in memory.tags" :key="tag">{{ tag }}</span>
</div>
<button @click="$emit('edit', memory.id)">Éditer</button>
</div>
</template>
<script>
export default {
props: {
memory: {
type: Object,
required: true
}
},
emits: ['edit']
}
</script>
Caractéristiques:
- ✅ Reçoit des données via
props - ✅ Émet des événements avec
$emit - ✅ Pas d'accès aux stores
- ✅ Réutilisable facilement
2. Composants conteneurs (Container)¶
Rôle: Gérer la logique, récupérer les données
<!-- MemoryList.vue - CONTENEUR | Dans cet exemple: liste de cartes -->
<template>
<div class="memory-list">
<MemoryCard
v-for="memory in memories"
:key="memory.id"
:memory="memory"
@edit="handleEdit"
/>
</div>
</template>
<script>
import { useMemoryStore } from '@/stores/memoryStore';
import MemoryCard from './MemoryCard.vue';
export default {
components: { MemoryCard },
data() {
return {
memoryStore: useMemoryStore()
}
},
computed: {
memories() {
return this.memoryStore.memories;
}
},
methods: {
handleEdit(memoryId) {
// Logique d'édition
this.memoryStore.editMemory(memoryId);
}
}
}
</script>
Caractéristiques:
- ✅ Accède aux stores (Pinia)
- ✅ Contient la logique métier*
- ✅ Contient des composants de présentation
3. Composants de base (Base Components)¶
Rôle: Composants UI réutilisables
<!-- BaseButton.vue | Dans cet exemple: un bouton -->
<template>
<button
:class="['btn', `btn-${variant}`, { 'btn-loading': loading }]"
:disabled="disabled || loading"
@click="$emit('click')"
>
<span v-if="loading" class="spinner"></span>
<slot v-else></slot>
</button>
</template>
<script>
export default {
props: {
variant: {
type: String,
default: 'primary',
validator: (value) => ['primary', 'secondary', 'danger'].includes(value)
},
loading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
emits: ['click']
}
</script>
Utilisation:
<BaseButton variant="primary" @click="save">
Enregistrer
</BaseButton>
<BaseButton variant="danger" :loading="isDeleting" @click="deleteItem">
Supprimer
</BaseButton>
🔗 5: Communication entre composants¶
5.1. Parent → Enfant: Props¶
<!-- Parent.vue -->
<template>
<MemoryCard
:memory="selectedMemory"
:show-actions="true"
/>
</template>
<script>
export default {
data() {
return {
selectedMemory: {
id: 1,
title: 'Mon souvenir',
description: 'Description...'
}
}
}
}
</script>
<!-- MemoryCard.vue (Enfant) -->
<script>
export default {
props: {
memory: {
type: Object,
required: true
},
showActions: {
type: Boolean,
default: false
}
}
}
</script>
5.2. Enfant → Parent: Events ($emit)¶
<!-- Enfant: MemoryForm.vue -->
<template>
<form @submit.prevent="handleSubmit">
<input v-model="title" />
<button type="submit">Créer</button>
</form>
</template>
<script>
export default {
data() {
return {
title: ''
}
},
methods: {
handleSubmit() {
// Émet un événement vers le parent
this.$emit('create', { title: this.title });
this.title = ''; // Reset
}
},
emits: ['create'] // Déclarer les events (bonne pratique)
}
</script>
<!-- Parent: RoomView.vue -->
<template>
<MemoryForm @create="addMemory" />
</template>
<script>
export default {
methods: {
addMemory(memoryData) {
console.log('Nouvelle mémoire:', memoryData);
// Logique d'ajout...
}
}
}
</script>
5.3. Communication complexe: Store (Pinia)¶
Quand plusieurs composants non liés ont besoin d'accéder aux mêmes données:
<!-- N'importe quel composant -->
<script>
import { useMemoryStore } from '@/stores/memoryStore';
export default {
data() {
return {
memoryStore: useMemoryStore()
}
},
computed: {
memories() {
return this.memoryStore.memories;
}
},
methods: {
addMemory(data) {
this.memoryStore.addMemory(data);
}
}
}
</script>
🎯 6: Application aux projets¶
Pour "Mémoires interactives" (ou musée de voyages ou de créations)¶
Démo d'un projet en exemple¶
Hiérarchie de composants recommandée:¶
Pour l'équipe qui fait un musée de créations: vous pouvez changer le mot Memory pour Creation dans vos noms de composantes.
App.vue
├── AppHeader.vue
├── AppNavigation.vue
└── Router View
├── HomeView.vue
├── MuseumView.vue
│ └── RoomGrid.vue
│ └── RoomCard.vue
│ ├── RoomHeader.vue
│ └── RoomActions.vue
│
└── RoomView.vue
├── RoomHeader.vue
├── SearchBar.vue
├── TagFilters.vue
└── MemoryGrid.vue (ou MemoryList.vue)
└── MemoryCard.vue
├── MemoryImage.vue
├── MemoryContent.vue
└── MemoryActions.vue
Suggestion de découpage par composant (dépendemment de votre intention)¶
Mise en page (layout) (2):
AppHeader.vue- En-tête avec navigationAppFooter.vue- Pied de page
Salles (4):
RoomCard.vue- Carte d'une salleRoomGrid.vueouRoomList- Grille ou liste de sallesRoomForm.vue- Formulaire ajout/édition salleRoomHeader.vue- En-tête détail d'une salle
Mémoires: ou Créations: changer le mot Memory pour Creation dans vos noms de composantes
MemoryCard.vue- Carte d'une mémoireMemoryGrid.vueouMemoryList.vue- Grille ou liste des cartes des mémoiresMemoryForm.vue- Formulaire ajout/édition mémoireMemoryDetail.vue- Vue détaillée d'une mémoireMemoryImage.vue- Gestion de l'imageMemoryTags.vue- Affichage des tags
UI Communs:
BaseButton.vue- Bouton réutilisableBaseModal.vue- Modal réutilisableBaseInput.vue- Input réutilisableLoadingSpinner.vue- Indicateur de chargement
Fonctionnalités:
SearchBar.vue- Barre de recherche (optionnel, au delà du MVP)TagFilter.vue- Filtre par tags (optionnel, au delà du MVP)ExportButton.vue- Bouton d'export (optionnel, au delà du MVP)
Total: ~20 composants
Pour "Trace ton chemin"¶
Démo de 2 projets de ce type en exemple¶
Hiérarchie de composants recommandée:¶
App.vue
├── AppHeader.vue
│ └── StatsBar.vue
│ └── StatIndicator.vue
└── Router View
├── MenuView.vue
│ └── MenuButton.vue
│
└── StoryView.vue
├── ChapterHeader.vue
├── NarrativeText.vue
│ └── TextParagraph.vue
├── ChoicePanel.vue
│ └── ChoiceButton.vue
│ ├── ChoiceText.vue
│ └── ChoiceHint.vue
├── ContinueButton.vue
└── EndingScreen.vue
├── EndingBadge.vue
├── StatsSummary.vue
└── ChoiceHistory.vue
Suggestion de découpage par composant (dépendemment de votre intention)¶
Layout:
AppHeader.vue- En-tête avec titreStatsBar.vue- Barre de statistiques
Story:
ChapterView.vue- Vue d'un chapitreChapterHeader.vue- Titre du chapitreNarrativeText.vue- Texte de narrationChoicePanel.vue- Panel de choixChoiceButton.vue- Bouton de choixContinueButton.vue- Bouton continuerProgressBar.vue- Barre de progressionSaveSlotManager.vue- Gestion des sauvegardes
Ending:
EndingScreen.vue- Écran de finEndingBadge.vue- Badge de fin (optionnel)ChoiceHistory.vue- Historique des choix
UI Communs:
BaseButton.vueBaseModal.vueStatIndicator.vue- Indicateur de statLoadingSpinner.vue
Total: ~17 composants
📝 Exercice Pratique (En classe)¶
Exercice 1: Identifier les composants¶
Regardez cette maquette et identifiez les composants à créer:
┌─────────────────────────────────────┐
│ [Logo] Musée │
├─────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Tokyo │ │ Paris │ │
│ │ │ │ │ │
│ │ 5 photos │ │ 3 photos │ │
│ └──────────┘ └──────────┘ │
│ ┌──────────┐ │
│ │ New York │ │
│ │ │ │
│ │ 0 photos │ │
│ └──────────┘ │
│ │
│ [+ Ajouter une destination] │
└─────────────────────────────────────┘
Exercice 2: Props ou Emit?¶
Pour chaque scenario, indiquez si vous utiliseriez Props ou Emit:
-
Afficher le titre d'une mémoire dans
MemoryCard -
Notifier le parent qu'un bouton "Supprimer" a été cliqué
-
Passer l'URL d'une image à afficher
-
Informer qu'un formulaire a été soumis
-
Afficher ou cacher un modal
const app = Vue.createApp({});
✅ Checklist: Bon composant vs Mauvais composant¶
Un BON composant:¶
- ✅ Un composant fait UNE seule chose et la fait bien
- ✅ Moins de 200 lignes de code
- ✅ Nom clair et descriptif
- ✅ Props bien documentées avec types (ex:
props: { title: String, inStock: Boolean}) - ✅ Émissions d'événements déclarées (
emits) - ✅ Réutilisable dans différents contextes
- ✅ Styles scopés (
<style scoped>) - ✅ Pas de logique métier* complexe (sauf les composants de type conteneurs dont le rôle est de gérer la logique et récupérer les données)
Un MAUVAIS composant:¶
- ❌ Fait trop de choses différentes
- ❌ Plus de 300 lignes
- ❌ Nom vague (
Component1.vue,Thing.vue) - ❌ Props non typées
- ❌ Dépendances cachées
- ❌ Code dupliqué
- ❌ Styles globaux non nécessaires
- ❌ Logique métier* mélangée à la présentation
🎓 Récapitulatif¶
Les 5 principes clés:¶
-
Un composant = Une responsabilité
- Ne pas mélanger présentation et logique
-
Hiérarchie claire
- Parent → Enfant avec Props
- Enfant → Parent avec Emit
- Store Pinia pour données partagées entre plusieurs composants
-
Réutilisabilité
- Composants de base génériques
- Props configurables
-
Organisation des fichiers
common/,layout/, entités spécifiques- Nommage cohérent
-
Communication explicite
- Props typées
- Émissions d'événements déclarées
📚 Ressources supplémentaires¶
Documentation officielle
- Vue.js - Principes fondamentaux des composants
- Vue.js - Enregistrement des composants
- Vue.js - Props
- Vue.js - Les événements de composant ($emit)
Lectures recommandées:
- "Thinking in Components" - Vue.js Best Practices
- "Component Design Patterns" - Advanced Vue
🎯 Travail à faire¶
Pour votre projet¶
-
Créer un diagramme de votre hiérarchie de composants
- Utilisez draw.io, Figma/Figjam ou papier/crayon
-
Créer la structure de dossiers dans votre projet
src/ ├── assets/ ├── components/ │ ├── common/ │ ├── layout/ │ └── specific/ ├── data/ ├── router/ ├── stores/ ├── views/ -
Créer vos composants et vos views ainsi que les balises de base
<template>,<script>,<styles>.- Créez les fichiers vides avec structure de base
- Exemple:
AppHeader.vue,RoomCard.vue,BaseButton.vue,RoomView.vue,HomeView.vue
-
Documenter vos composants et vos views
- Liste dans un fichier
COMPONENTS.md(sauvegarder dans le dossiersrcde votre projet) - Pour chaque composant: nom, responsabilité, props attendues, événements émis (emits)
- Liste dans un fichier
-
Pour Trace ton chemin, rédaction de vos chapitres dans un Word ou document textuel collaboratif.
❓ Questions fréquentes¶
Q: Combien de composants dois-je créer?
R: Pour votre projet, visez 15-20 composants. Mieux vaut trop découper que pas assez! Lorsque vous commencez à développer, priorisez les composants nécessaires au MVP (Minimum Viable Product) de votre projet.
Q: Quand créer un nouveau composant?
R: Dès que:
- Le code dépasse 150 lignes
- Vous copiez-collez du code
- Une section a une responsabilité claire
- Vous voulez réutiliser quelque chose
Q: Props ou Store?
R:
- Props: Données spécifiques parent → enfant immédiat
- Store: Données partagées entre plusieurs composants non liés
Q: Puis-je modifier une prop dans un composant enfant?
R: NON! Les props sont read-only. Utilisez $emit pour demander au parent de la modifier.