Arborescence (architecture) des composants¶
Table des matière de la page¶
- Qu'est-ce que l'arborescence des composants
- Fonctionnement
- 📚 1: Pourquoi des composants?
- 🏗️ 2: Principe de responsabilité unique
- 📁 3: Organisation des fichiers
- 🎨 4: Types de composants
- 🔗 5: Communication entre composants
- 🎯 6: Application à vos projets app web créative
L'arborescence des composants¶
L'arborescence (ou l'architecture) des composants d'une application est la structure hiérarchique qui organise les différents éléments réutilisables de l'interface utilisateur (UI).
Elle représente la manière dont les composants sont imbriqués les uns dans les autres, comme des éléments HTML, pour construire une application complète.
Cette structure permet de créer des applications complexes en divisant l'interface en morceaux plus petits, indépendants et gérables.
Fonctionnement¶
Imbrication de composants¶
Une application est construite en imbriquant des composants les uns dans les autres.
Par exemple, le composant racine peut contenir des composants enfants, qui à leur tour peuvent contenir d'autres composants.
App.vue
├── StoryView.vue
├── ChapterHeader.vue
├── ChapterText.vue
├── ChoicePanel.vue
│ └── ChoiceButton.vue
├── ContinueButton.vue
└── EndingScreen.vue
├── StatsSummary.vue
└── ChoiceHistory.vue
Division de l'interface (UI)¶
L'arborescence permet de diviser l'interface utilisateur en blocs de code réutilisables et isolés. Chaque composant encapsule sa propre structure (HTML), sa logique (JavaScript) et son style (CSS).
Exemple concret¶
L'arborescence d'une application de liste de tâches pourrait ressembler à ceci :
- Un composant racine
App. - Qui pourrait contenir un composant pour le champ textuel pour ajouter un tâche à la liste
BaseInput.vue, un composant pour le boutonBaseButton.vueet un composant listeTodoList.vue. - Et le
TodoList.vuepourrait contenir une liste de composants de tâche individuelleTodoItem.vue.
App.vue
├── BaseInput.vue
├── BaseButton.vue
├── TodoList.vue
├── TodoItem.vue
1: Pourquoi des composants?
📚 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
🏗️ 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/type projet
│ ├── 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¶
4.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
4.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
4.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 (ou RoomList.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
├── HomeView.vue
├── 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
Exercices pratiques en classe
📝 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
Solution Exercice 1: Identifier les composants¶
Regardez cette maquette et identifiez les composants à créer:
Pour Mémoires Interactives:
┌─────────────────────────────────────┐
│ [Logo] Musée │ ← AppHeader
├─────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Tokyo │ │ Paris │ │ ← RoomCard x3
│ │ │ │ │ │
│ │ 5 photos │ │ 3 photos │ │
│ └──────────┘ └──────────┘ │
│ ┌──────────┐ │
│ │ New York │ │
│ │ │ │
│ │ 0 photos │ │
│ └──────────┘ │
│ │
│ [+ Ajouter une destination] │ ← BaseButton
└─────────────────────────────────────┘
Question: Combien de composants différents identifiez-vous?
Réponse:
AppHeader(header)RoomGrid(conteneur)RoomCard(carte répétée)BaseButton(bouton ajout)
Solution Exercice 2: Props ou Emit? (5 min)¶
Pour chaque scenario, indiquez si vous utiliseriez Props ou Emit:
-
Afficher le titre d'une mémoire dans
MemoryCard- Réponse: Props ✅ (parent → enfant)
-
Notifier le parent qu'un bouton "Supprimer" a été cliqué
- Réponse: Emit ✅ (enfant → parent)
-
Passer l'URL d'une image à afficher
- Réponse: Props ✅
-
Informer qu'un formulaire a été soumis
- Réponse: Emit ✅
-
Afficher ou cacher un modal
- Réponse: Props ✅ (v-model aussi possible)
Checklist: Bon composant vs Mauvais composant
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
🎓 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
📚 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
Questions fréquentes
❓ 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.