Découper les composants
L'approche par composants est la philosophie de conception fondamentale de Vue.
Dans ce chapitre, vous apprendrez à découper une page en petits composants réutilisables et à assurer la communication entre composants via props et emit.
Pourquoi découper les composants ?
À mesure que les fonctionnalités se développent, un fichier devient de plus en plus long, et des centaines de lignes de code entassées dans un seul fichier .vue deviennent difficiles à maintenir.
Le but du découpage en composants :Chaque composant ne fait qu'une seule chose, et ensemble ils accomplissent des fonctionnalités complexes.。
Quel niveau de granularité est approprié ?
Une bonne règle empirique est :Lorsqu'un bloc de code possède ses propres données, son propre comportement et peut être réutilisé ailleurs,il devrait devenir un composant indépendant.
| Zone de page | Faut-il découper en composant ? | Raison |
|---|---|---|
| Barre de navigation | Oui | Nécessaire sur chaque page ; fonctionnalité indépendante |
| Carte d'article | Oui | Chaque article de la liste partage la même structure |
| Groupe de boutons de filtre par catégorie | Oui | Possède son propre état et ses propres interactions |
| Conteneur d'encapsulation simple | non | Uniquement pour la mise en page, aucun comportement indépendant |
Parent vers enfant : defineProps
Le composant parent transmet des données au composant enfant,propsimplémentés via defineProps.
Vous pouvez le considérer comme un « paramètre de fonction » — lorsque le composant parent appelle le composant enfant, il transmet des données en tant que paramètre.
Exemple
<script setup>
// Déclarer les props que ce composant reçoit
// defineProps est une macro du compilateur, aucun import nécessaire
const props = defineProps({
title: String, // Titre de l'article, type chaîne
summary: String, // Résumé de l'article
date: String, // Date de publication
category: String // Étiquette de catégorie
})
// Il peut également être écrit sous forme de tableau (sans vérification de type) :
// const props = defineProps(['title', 'summary', 'date', 'category'])
</script>
<template>
<div class="card">
<span class="tag">{{ category }}</span>
<h3>{{ title }}</h3>
<p>{{ summary }}</p>
<span class="date">{{ date }}</span>
</div>
</template>
<style scoped>
.card {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.tag {
display: inline-block;
padding: 2px 10px;
background: #E8EEF5;
color: #427DB8;
border-radius: 12px;
font-size: 12px;
}
.card h3 { margin: 10px 0 8px; font-size: 18px; }
.card p { color: #666; font-size: 14px; line-height: 1.6; }
.date { font-size: 12px; color: #999; }
</style>
Lorsque le composant parent utilise BlogCard, il transmet les données en liant les attributs avec le signe deux-points :
Exemple
<script setup>
import { ref } from 'vue'
import BlogCard from '../components/BlogCard.vue'
const articles = ref([
{ id: 1, title: 'Introduction à Vue3', summary: 'fromzero apprend Vue3', date: '2024-05-10', category: 'Vue' },
{ id: 2, title: 'Promise en détail', summary: 'Comprendre Async', date: '2024-05-08', category: 'JavaScript' },
])
</script>
<template>
<div class="article-grid">
<!-- Le composant parent transmet les données au composant enfant via :prop-name="value" -->
<BlogCard
v-for="article in articles"
:key="article.id"
:title="article.title"
:summary="article.summary"
:date="article.date"
:category="article.category"
/>
</div>
</template>
les props sontun flux de données unidirectionnel: les données ne peuvent circuler que du parent → enfant ; le composant enfant ne peut pas modifier la valeur des props. Si le composant enfant a besoin de la modifier, il doit notifier le parent via emit pour effectuer la modification.
De l'enfant au parent : defineEmits
Lorsqu'un composant enfant doit informer le composant parent que quelque chose s'est produit (par ex., « on a cliqué sur moi »), il le fait en utilisantemitpour émettre un événement.
Vous pouvez le voir comme une « fonction de rappel » — le composant enfant déclenche un événement, et le composant parent l'écoute et y répond.
Exemple
<script setup>
const props = defineProps({
categories: Array, // Liste de toutes les catégories
activeCategory: String // Catégorie actuellement sélectionnée
})
// Déclarer les événements à émettre
const emit = defineEmits(['update-category'])
function selectCategory(cat) {
// Déclencher l'événement, en transmettant la catégorie sélectionnée au composant parent
emit('update-category', cat)
}
</script>
<template>
<div class="filter-bar">
<button
v-for="cat in categories"
:key="cat"
:class="{ active: activeCategory === cat }"
@click="selectCategory(cat)"
>
{{ cat }}
</button>
</div>
</template>
Le composant parent utilise@eventName="handlerFunction"pour écouter les événements provenant du composant enfant :
Exemple
<script setup>
import { ref, computed } from 'vue'
import CategoryFilter from '../components/CategoryFilter.vue'
const activeCategory = ref('Toutes')
const categories = ['All', 'Vue', 'JavaScript', 'CSS']
// Réagir à l'événement update-category émis par le composant enfant
function handleCategoryChange(cat) {
activeCategory.value = cat
}
</script>
<template>
<!-- @update-category="handleCategoryChange" -->
<CategoryFilter
:categories="categories"
:active-category="activeCategory"
@update-category="handleCategoryChange"
/>
</template>
Flux de données complet
Le composant parent, viaprops, transmet les données vers le bas ;le composant enfant, viaemit,envoie des événements vers le haut.
Les données sont toujours "détenues" et modifiées par le composant parent ; le composant enfant est uniquement responsable du rendu et de la notification.
Mise en pratique : extraire trois composants
Le projet de blog nécessite trois composants, avec la structure de répertoires suivante :
src/ ├── components/ │ ├── NavBar.vue # 顶部导航栏 │ ├── BlogCard.vue # 单篇文章卡片 │ └── CategoryFilter.vue # 分类筛选按钮组 ├── views/ │ └── HomeView.vue # 首页(父组件) └── App.vue
NavBar.vue — Barre de navigation
Exemple
<script setup>
// La barre de navigation doit seulement s'afficher pour l'instant ; la recherche et le basculement en mode sombre seront ajoutés plus tard
</script>
<template>
<header class="navbar">
<a href="/" class="logo">EXAMPLE Blog</a>
<nav>
<a href="/">Accueil</a>
<a href="#">À propos</a>
</nav>
</header>
</template>
<style scoped>
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 40px;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.logo {
font-size: 22px;
font-weight: bold;
color: #427DB8;
text-decoration: none;
}
nav a {
margin-left: 24px;
text-decoration: none;
color: #333;
font-size: 15px;
}
nav a:hover {
color: #427DB8;
}
</style>
HomeView.vue — Intégration de tous les composants
Exemple
<script setup>
import { ref, computed } from 'vue'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'
// Données des articles
const articles = ref([
{ id: 1, title: 'Guide complet de démarrage avec Vue3', summary: 'Apprendre la Composition API de Vue3 depuis zéro', date: '2024-05-10', category: 'Vue' },
{ id: 2, title: 'Programmation asynchrone en JS en détail', summary: Comprendre Promise et async/await, date: '2024-05-08', category: 'JavaScript' },
{ id: 3, title: Mise en page pratique avec CSS Grid, summary: 'Implémenter une mise en page adaptative avec Grid', date: '2024-05-05', category: 'CSS' },
{ id: 4, title: 'Principe de réactivité de Vue3', summary: 'Comprendre en profondeur ref et reactive', date: '2024-05-03', category: 'Vue' },
{ id: 5, title: 'Le guide complet de Flexbox', summary: 'Apprendre la mise en page Flex en un seul article', date: '2024-05-01', category: 'CSS' },
])
// Extraire toutes les catégories (dédoublonner)
const categories = computed(() => {
const cats = articles.value.map(a => a.category)
return ['Toutes', ...new Set(cats)]
})
// La catégorie actuellement sélectionnée
const activeCategory = ref('Toutes')
// Filtrer les articles par catégorie
const filteredArticles = computed(() => {
if (activeCategory.value === 'Toutes') return articles.value
return articles.value.filter(a => a.category === activeCategory.value)
})
// Gérer l'événement de changement de catégorie émis par le composant enfant
function handleCategoryChange(cat) {
activeCategory.value = cat
}
</script>
<template>
<div class="home">
<h2 class="section-title">Derniers articles</h2>
<!-- Composant de filtre de catégorie -->
<CategoryFilter
:categories="categories"
:active-category="activeCategory"
@update-category="handleCategoryChange"
/>
<!-- État vide -->
<p v-if="filteredArticles.length === 0" class="empty-tip">
Aucun article dans cette catégorie pour le moment.
</p>
<!-- Liste des cartes d'articles -->
<div v-else class="article-grid">
<BlogCard
v-for="article in filteredArticles"
:key="article.id"
:title="article.title"
:summary="article.summary"
:date="article.date"
:category="article.category"
/>
</div>
</div>
</template>
<style scoped>
.home { max-width: 960px; margin: 0 auto; padding: 20px; }
.section-title { font-size: 24px; margin-bottom: 20px; }
.article-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
margin-top: 20px;
}
.empty-tip { text-align: center; color: #999; padding: 60px 0; }
</style>
Résumé du chapitre
Dans ce chapitre, vous avez maîtrisé les trois concepts clés de la composantisation : pourquoi découper les composants, utiliser props (defineProps) pour la communication de parent à enfant, et utiliser emit (defineEmits) pour la communication d'enfant à parent.
Les trois composants du projet de blog — NavBar, BlogCard et CategoryFilter — sont maintenant en place, et la structure du code est bien plus claire que le fouillis du chapitre 1.
Autres extensions