コンポーネント分割
コンポーネント化は Vue の最も中核となる設計思想です。
この章では、ページを再利用可能な小さなコンポーネントに分割し、props と emit を使ってコンポーネント間通信を実装する方法を学びます。
なぜコンポーネントを分割するのか?
機能が増えるにつれて、1つのファイルがどんどん長くなり、数百行のコードが1つの .vue ファイルに詰め込まれて、保守が困難になります。
コンポーネント分割の目的:各コンポーネントは1つのことだけを行い、組み合わせることで複雑な機能を実現する。
どの粒度が適切か?
良い経験則は次のとおりです:コードブロックが独自のデータ、独自の動作を持ち、他の場所で再利用できる場合、それは独立したコンポーネントになるべきです。
| ページ領域 | コンポーネントに分割すべきか | 理由 |
|---|---|---|
| ナビゲーションバー | である | すべてのページで必要、独立した機能 |
| 記事カード | である | リスト内の各記事が同じ構造 |
| カテゴリ絞り込みボタングループ | である | 独自の状態とインタラクションを持つ |
| 単純なラッパーコンテナ | 否 | レイアウトのためだけで、独立した動作がない |
親から子へ:defineProps
親コンポーネントが子コンポーネントにデータを渡すには、propsを使用します。
これは「関数の引数」と考えることができます——親コンポーネントが子コンポーネントを呼び出すときに、データを引数として渡します。
例
<script setup>
// このコンポーネントが受け取る props を宣言
// これはコンパイルマクロなので、インポート不要で使用可能
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: 'ゼロからVue3を学ぶ', date: '2024-05-10', category: 'Vue' },
{ id: 2, title: 'Promise 詳解', summary: '非同期を理解する', date: '2024-05-08', category: 'JavaScript' },
])
</script>
<template>
<div class="article-grid">
<!-- 親コンポーネントは :属性名="値" で子コンポーネントにデータを渡す -->
<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-bar">
<button
v-for="cat in categories"
:key="cat"
:class="{ active: activeCategory === cat }"
@click="selectCategory(cat)"
>
{{ cat }}
</button>
</div>
</template>
親コンポーネントは@イベント名="処理関数"で子コンポーネントのイベントをリッスンします:
例
<script setup>
import { ref, computed } from 'vue'
import CategoryFilter from '../components/CategoryFilter.vue'
const activeCategory = ref('すべて')
const categories = ['すべて', '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 で上方向にイベントを送信します。
データは常に親コンポーネントが「所有」して変更し、子コンポーネントは表示と通知だけを担当します。
実践:3つのコンポーネントに分割する
ブログプロジェクトには3つのコンポーネントが必要です。ディレクトリ構造は次のとおりです:
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">サンプル Blog</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: 'ゼロからVue3 Composition APIを学ぶ', 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: '1記事でフレックスレイアウトをマスター', date: '2024-05-01', category: 'CSS' },
])
// すべてのカテゴリを抽出(重複排除)
const categories = computed(() => {
const cats = articles.value.map(a => a.category)
return ['すべて', ...new Set(cats)]
})
// 現在選択中のカテゴリ
const activeCategory = ref('すべて')
// カテゴリに基づいて記事をフィルタリング
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>
章のまとめ
この章では、コンポーネント化の3つの中核知識を習得しました:なぜコンポーネントを分割するのか、親から子へは props(defineProps)、子から親へは emit(defineEmits)です。
ブログプロジェクトの3つのコンポーネント NavBar、BlogCard、CategoryFilter が揃いました。コード構造は第1章のゴチャゴチャした状態よりずっと明確になりました。
その他の拡張