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ónSíCada página la necesita; función independiente.
Tarjeta de artículoSíCada artículo de la lista tiene la misma estructura.
Grupo de botones de filtro por categoríaSíTiene su propio estado e interacción.
Contenedor envolvente simplenoSolo 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

<!-- Ruta del archivo: src/components/BlogCard.vue -->
<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

<!-- Ruta del archivo: src/views/HomeView.vue -->
<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

<!-- Ruta del archivo: src/components/CategoryFilter.vue -->
<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

<!-- Ruta del archivo: src/views/HomeView.vue -->
<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

<!-- Ruta del archivo: src/components/NavBar.vue -->
<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

<!-- Ruta del archivo: src/views/HomeView.vue -->
<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