Разделение компонентов

Компонентизация — это основная философия проектирования Vue.

В этой главе вы узнаете, как разбить страницу на небольшие переиспользуемые компоненты и обеспечить взаимодействие между компонентами через props и emit.


Зачем разделять компоненты?

По мере роста функциональности файл становится все длиннее, и сотни строк кода, втиснутых в один файл .vue, становится трудно поддерживать.

Цель разделения компонентов:Каждый компонент делает только одно, а вместе они реализуют сложную функциональность.。

Какая гранулярность уместна?

Хорошее практическое правило:Когда блок кода имеет собственные данные, собственное поведение и может быть переиспользован в другом месте,он должен стать независимым компонентом.

Область страницыСтоит ли выделять в компонентПричина
Панель навигацииДаТребуется на каждой странице; независимая функциональность
Карточка статьиДаКаждая статья в списке имеет одинаковую структуру
Группа кнопок фильтра категорийДаИмеет собственное состояние и взаимодействия
Простой контейнер-обёртканетТолько для разметки, без самостоятельного поведения

От родителя к дочернему компоненту: defineProps

Родительский компонент передаёт данные дочернему компоненту,propsреализуются через defineProps.

Можно представить это как "параметр функции" — когда родительский компонент вызывает дочерний компонент, он передаёт данные в качестве параметра.

Пример

<!-- Путь к файлу: src/components/BlogCard.vue -->
<script setup>
// Объявите props, которые получает этот компонент
// defineProps — это макрос компилятора, импорт не требуется
const props = defineProps({
title: String, // Заголовок статьи, строковый тип
summary: String, // Краткое описание статьи
date: String, // Дата публикации
category: String // Тег категории
})

// Это также можно записать в виде массива (без проверки типов):
// 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>

Когда родительский компонент использует BlogCard, он передаёт данные путём привязки атрибутов с помощью двоеточия:

Пример

<!-- Путь к файлу: src/views/HomeView.vue -->
<script setup>
import { ref } from 'vue'
import BlogCard from '../components/BlogCard.vue'

const articles = ref([
{ id: 1, title: 'Введение в Vue3', summary: 'fromzero изучает Vue3', date: '2024-05-10', category: 'Vue' },
{ id: 2, title: 'Promise в деталях', summary: 'Понимание асинхронности', date: '2024-05-08', category: 'JavaScript' },
])
</script>

<template>
  <div class="article-grid">
    <!-- Родительский компонент передаёт данные дочернему компоненту через :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>

props — этооднонаправленный поток данных: данные могут передаваться только от родителя → к дочернему компоненту; дочерний компонент не может изменять значение props. Если дочернему компоненту нужно изменить его, он должен уведомить родителя через emit, чтобы внести изменение.


От дочернего к родительскому: defineEmits

Когда дочернему компоненту нужно уведомить родительский компонент о том, что что-то произошло (например, "по мне кликнули"), он делает это с помощьюemit,генерируя событие.

Это можно представить как "функцию обратного вызова" — дочерний компонент вызывает событие, а родительский компонент слушает и реагирует.

Пример

<!-- Путь к файлу: src/components/CategoryFilter.vue -->
<script setup>
const props = defineProps({
categories: Array, // Список всех категорий
activeCategory: String Текущая выбранная категория
})

// Объявите события для отправки
const emit = defineEmits(['update-category'])

function selectCategory(cat) {
// Запустить событие, передав выбранную категорию дочернему компоненту
  emit('update-category', cat)
}
</script>

<template>
  <div class⟦⟧"filter"="filter-bar">
    <button
     v-for="cat in categories"
     :key="cat"
     :class="{ active: activeCategory === cat }"
     @click=selectCategory(cat)"
   >
      {{ cat }}
    </button>
  </div>
</template>

Родительский компонент использует@eventName="handlerFunction"чтобы прослушивать события от дочернего компонента:

Пример

<!-- Путь к файлу: src/views/HomeView.vue -->
<script setup>
import { ref, computed } from 'vue'
import CategoryFilter from '../components/CategoryFilter.vue'

const activeCategory = ref('All')

const categories = ['All', 'Vue', 'JavaScript', 'CSS']

// Реагировать на событие update-category, отправляемое дочерним компонентом
function handleCategoryChange(cat) {
  activeCategory.value = cat
}
</script>

<template>
  <!-- @update-category="handleCategoryChange" -->
  <CategoryFilter
   :categories="categories"
   :active-category="activeCategory"
   @update-category="handleCategoryChange"
 />
</template>

Полный поток данных

Родительский компонент черезprops передаёт данные вниз;дочерний компонент черезemit,отправляет события вверх.

Данные всегда "принадлежат" родительскому компоненту и изменяются им; дочерний компонент отвечает только за рендеринг и отправку уведомлений.


Практика: выделяем три компонента

Для проекта блога нужны три компонента, структура каталогов следующая:

src/
├── components/
│   ├── NavBar.vue          # 顶部导航栏
│   ├── BlogCard.vue        # 单篇文章卡片
│   └── CategoryFilter.vue  # 分类筛选按钮组
├── views/
│   └── HomeView.vue        # 首页(父组件)
└── App.vue

NavBar.vue — Панель навигации

Пример

<!-- Путь к файлу: src/components/NavBar.vue -->
<script setup>
// Панель навигации пока должна только отображаться; поиск и переключатель тёмного режима будут добавлены позже
</script>

<template>
  <header class="navbar">
    <a href="/" class="logo">ПРИМЕР Блог</a>
    <nav>
      <a href="/">Главная</a>
      <a href="#">О нас</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 — Интеграция всех компонентов

Пример

<!-- Путь к файлу: src/views/HomeView.vue -->
<script setup>
import { ref, computed } from 'vue'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'

// Данные статей
const articles = ref([
{ id: 1, title: 'Vue3: Полное руководство для начинающих', summary: 'с нуля изучить Composition API Vue3', date: '2024-05-10', category: 'Vue' },
{ id: 2, title: 'Асинхронное программирование на JS подробно', summary: Понимание Promise и async/await, date: '2024-05-08', category: 'JavaScript' },
{ id: 3, title: Практическая вёрстка с CSS Grid, summary: 'Реализация адаптивной вёрстки с Grid', date: '2024-05-05', category: 'CSS' },
{ id: 4, title: 'Принцип реактивности Vue3', summary: 'Глубокое понимание ref и reactive', date: '2024-05-03', category: 'Vue' },
{ id: 5, title: 'Полное руководство по Flexbox', summary: 'Изучите Flex-вёрстку в одной статье', date: '2024-05-01', category: 'CSS' },
])

// Извлечь все категории (удалить дубликаты)
const categories = computed(() => {
  const cats = articles.value.map(a => a.category)
return ['Все', ...new Set(cats)]
})

// Текущая выбранная категория
const activeCategory = ref('All')

// Фильтровать статьи по категории
const filteredArticles = computed(() => {
if (activeCategory.value === 'Все') return articles.value
  return articles.value.filter(a => a.category === activeCategory.value)
})

// Обработка события смены категории, отправленного дочерним компонентом
function handleCategoryChange(cat) {
  activeCategory.value = cat
}
</script>

<template>
  <div class="home">
    <h2 class="section-title">Последние статьи</h2>

    <!-- Компонент фильтрации категорий -->
    <CategoryFilter
     :categories="categories"
     :active-category="activeCategory"
     @update-category="handleCategoryChange"
   />

    <!-- Пустое состояние -->
    <p v-if="filteredArticles.length === 0" class="empty-tip">
В этой категории пока нет статей.
    </p>

    <!-- Список карточек статей -->
    <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>

Краткое содержание главы

В этой главе вы освоили три ключевые концепции компонентизации: зачем разбивать компоненты, использование props (defineProps) для связи от родителя к дочернему компоненту и использование emit (defineEmits) для связи от дочернего компонента к родителю.

Три компонента блог-проекта — NavBar, BlogCard и CategoryFilter — уже на месте, и структура кода стала намного понятнее, чем беспорядок в главе 1.

Другие расширения