Разделение компонентов
Компонентизация — это основная философия проектирования Vue.
В этой главе вы узнаете, как разбить страницу на небольшие переиспользуемые компоненты и обеспечить взаимодействие между компонентами через props и emit.
Зачем разделять компоненты?
По мере роста функциональности файл становится все длиннее, и сотни строк кода, втиснутых в один файл .vue, становится трудно поддерживать.
Цель разделения компонентов:Каждый компонент делает только одно, а вместе они реализуют сложную функциональность.。
Какая гранулярность уместна?
Хорошее практическое правило:Когда блок кода имеет собственные данные, собственное поведение и может быть переиспользован в другом месте,он должен стать независимым компонентом.
| Область страницы | Стоит ли выделять в компонент | Причина |
|---|---|---|
| Панель навигации | Да | Требуется на каждой странице; независимая функциональность |
| Карточка статьи | Да | Каждая статья в списке имеет одинаковую структуру |
| Группа кнопок фильтра категорий | Да | Имеет собственное состояние и взаимодействия |
| Простой контейнер-обёртка | нет | Только для разметки, без самостоятельного поведения |
От родителя к дочернему компоненту: defineProps
Родительский компонент передаёт данные дочернему компоненту,propsреализуются через defineProps.
Можно представить это как "параметр функции" — когда родительский компонент вызывает дочерний компонент, он передаёт данные в качестве параметра.
Пример
<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, он передаёт данные путём привязки атрибутов с помощью двоеточия:
Пример
<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,генерируя событие.
Это можно представить как "функцию обратного вызова" — дочерний компонент вызывает событие, а родительский компонент слушает и реагирует.
Пример
<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"чтобы прослушивать события от дочернего компонента:
Пример
<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 — Панель навигации
Пример
<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 — Интеграция всех компонентов
Пример
<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.
Другие расширения