División de componentes
La componentización es la idea de diseño más central de Vue.
En este capítulo aprenderás a dividir una página en pequeños componentes reutilizables y a lograr la comunicación entre componentes mediante props y emit.
¿Por qué dividir componentes?
A medida que aumentan las funcionalidades, un archivo se vuelve cada vez más largo; cientos de líneas de código metidas en un archivo .vue son difíciles de mantener.
El propósito de dividir componentes:Cada componente hace una sola cosa; combinados, completan funciones complejas.。
¿Qué granularidad es adecuada?
Una buena regla empírica es:Cuando un bloque de código tiene sus propios datos, su propio comportamiento y puede reutilizarse en otro lugar,debería convertirse en un componente independiente.
| Área de la página | ¿Debe dividirse en un componente? | Razón |
|---|---|---|
| Barra de navegación | Sí | Cada página la necesita; función independiente. |
| Tarjeta de artículo | Sí | Cada artículo de la lista tiene la misma estructura. |
| Grupo de botones de filtro por categoría | Sí | Tiene su propio estado e interacción. |
| Contenedor envolvente simple | no | Solo es para la maquetación; no tiene un comportamiento independiente. |
Padre a hijo: defineProps
El componente padre transmite datos al componente hijo medianteprops`defineProps`.
Puedes entenderlo como el «parámetro de una función»: cuando el componente padre llama al componente hijo, le pasa los datos como parámetro.
Ejemplo
<script setup>
// Declara los props que recibe este componente
// defineProps es una macro de compilación; no requiere importación para usarse
const props = defineProps({
title: String, // Título del artículo, tipo String
summary: String, // Resumen del artículo
date: String, // Fecha de publicación
category: String // Etiqueta de categoría
})
// También se puede escribir en forma de array (sin validar tipos):
// 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>
Cuando el componente padre usa BlogCard, pasa los datos mediante el enlace de atributos con dos puntos:
Ejemplo
<script setup>
import { ref } from 'vue'
import BlogCard from '../components/BlogCard.vue'
const articles = ref([
{ id: 1, title: 'Introducción a Vue3', summary: 'Aprende Vue3 desde cero', date: '2024-05-10', category: 'Vue' },
{ id: 2, title: 'Explicación detallada de Promise', summary: 'Entender la asincronía', date: '2024-05-08', category: 'JavaScript' },
])
</script>
<template>
<div class="article-grid">
<!-- El componente padre transmite datos al componente hijo mediante :nombre-del-atributo="valor" -->
<BlogCard
v-for="article in articles"
:key="article.id"
:title="article.title"
:summary="article.summary"
:date="article.date"
:category="article.category"
/>
</div>
</template>
props esun flujo de datos unidireccional: solo de padre → hijo; el componente hijo no puede modificar el valor de props. Si el componente hijo necesita modificarlo, debe notificar al padre mediante emit para que lo cambie.
Hijo a padre: defineEmits
El componente hijo debe notificar al padre que ha ocurrido algo (por ejemplo, «me han clicado»), medianteemitel envío de un evento (`emit`).
Puedes entenderlo como una «función de retorno» (callback): el componente hijo dispara el evento y el componente padre lo escucha y responde.
Ejemplo
<script setup>
const props = defineProps({
categories: Array, // Lista de todas las categorías
activeCategory: String // Categoría seleccionada actualmente
})
// Declara el evento que se va a enviar
const emit = defineEmits(['update-category'])
function selectCategory(cat) {
// Dispara el evento y pasa la categoría seleccionada al componente padre
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>
El componente padre, mediante@nombreDeEvento="función de manejo"`@update-category`, escucha los eventos del componente hijo:
Ejemplo
<script setup>
import { ref, computed } from 'vue'
import CategoryFilter from '../components/CategoryFilter.vue'
const activeCategory = ref('Todos')
const categories = ['Todas', 'Vue', 'JavaScript', 'CSS']
// Responde al evento update-category enviado por el componente hijo
function handleCategoryChange(cat) {
activeCategory.value = cat
}
</script>
<template>
<!-- @update-category="handleCategoryChange" -->
<CategoryFilter
:categories="categories"
:active-category="activeCategory"
@update-category="handleCategoryChange"
/>
</template>
Flujo de datos completo
El componente padre, medianteprops, hacia abajo,transmite datos; el componente hijo, medianteemit, hacia arriba,envía eventos.
Los datos siempre son «poseídos» y modificados por el componente padre; el componente hijo solo se encarga de mostrarlos y de notificar.
Manos a la obra: divide en tres componentes
El proyecto del blog necesita tres componentes; la estructura de directorios es la siguiente:
src/ ├── components/ │ ├── NavBar.vue # 顶部导航栏 │ ├── BlogCard.vue # 单篇文章卡片 │ └── CategoryFilter.vue # 分类筛选按钮组 ├── views/ │ └── HomeView.vue # 首页(父组件) └── App.vue
NavBar.vue — Barra de navegación
Ejemplo
<script setup>
// La barra de navegación solo necesita mostrarse por ahora; más adelante se añadirán la búsqueda y el modo oscuro
</script>
<template>
<header class="navbar">
<a href="/" class="logo">EJEMPLO Blog</a>
<nav>
<a href="/">Inicio</a>
<a href="#">Acerca de</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 — Integra todos los componentes
Ejemplo
<script setup>
import { ref, computed } from 'vue'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'
// Datos de los artículos
const articles = ref([
{ id: 1, title: 'Guía completa de introducción a Vue3', summary: 'Aprende la Composition API de Vue3 desde cero', date: '2024-05-10', category: 'Vue' },
{ id: 2, title: 'Explicación detallada de la programación asíncrona en JS', summary: 'Entiende Promise y async/await', date: '2024-05-08', category: 'JavaScript' },
{ id: 3, title: 'Práctica de CSS Grid', summary: 'Implementa un diseño responsive con Grid', date: '2024-05-05', category: 'CSS' },
{ id: 4, title: 'Principios de reactividad de Vue3', summary: 'Comprensión profunda de ref y reactive', date: '2024-05-03', category: 'Vue' },
{ id: 5, title: 'Guía completa de Flexbox', summary: 'Aprende el diseño flexible en un artículo', date: '2024-05-01', category: 'CSS' },
])
// Extrae todas las categorías (sin duplicados)
const categories = computed(() => {
const cats = articles.value.map(a => a.category)
return ['Todos', ...new Set(cats)]
})
// Categoría seleccionada actualmente
const activeCategory = ref('Todos')
// Filtra los artículos según la categoría
const filteredArticles = computed(() => {
if (activeCategory.value === 'Todos') return articles.value
return articles.value.filter(a => a.category === activeCategory.value)
})
// Maneja el evento de cambio de categoría enviado por el componente hijo
function handleCategoryChange(cat) {
activeCategory.value = cat
}
</script>
<template>
<div class="home">
<h2 class="section-title">Artículos recientes</h2>
<!-- Componente de filtrado por categoría -->
<CategoryFilter
:categories="categories"
:active-category="activeCategory"
@update-category="handleCategoryChange"
/>
<!-- Estado vacío -->
<p v-if="filteredArticles.length === 0" class="empty-tip">
No hay artículos en esta categoría
</p>
<!-- Lista de tarjetas de artículo -->
<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>
Resumen del capítulo
En este capítulo has dominado los tres conceptos clave de la componentización: por qué dividir componentes, de padre a hijo con props (defineProps) y de hijo a padre con emit (defineEmits).
Los tres componentes del proyecto del blog, NavBar, BlogCard y CategoryFilter, ya están en su lugar; la estructura del código está mucho más clara que el lío del primer capítulo.
Otras extensiones