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 pageFaut-il découper en composant ?Raison
Barre de navigationOuiNécessaire sur chaque page ; fonctionnalité indépendante
Carte d'articleOuiChaque article de la liste partage la même structure
Groupe de boutons de filtre par catégorieOuiPossède son propre état et ses propres interactions
Conteneur d'encapsulation simplenonUniquement 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

<!-- Chemin du fichier : src/components/BlogCard.vue -->
<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

<!-- Chemin de fichier : src/views/HomeView.vue -->
<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

<!-- Chemin du fichier : src/components/CategoryFilter.vue -->
<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

<!-- Chemin de fichier : src/views/HomeView.vue -->
<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

<!-- Chemin du fichier : src/components/NavBar.vue -->
<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

<!-- Chemin de fichier : src/views/HomeView.vue -->
<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