Vue 3 コンポーネントの基礎:SFC(シングルファイルコンポーネント)+コンポーネントの登録
Vueコンポーネントは再利用可能なVueインスタンスであり、各コンポーネントは独自のテンプレート、ロジック、スタイルをカプセル化しています。Vue 3の中核となるアーキテクチャは「コンポーネントツリー」です:ルートコンポーネント(App.vue)→ ページコンポーネント → ビジネスコンポーネント → 基本コンポーネント(Button/Input)。
コンポーネントの基礎を習得することは、大規模なVueアプリケーションを構築する上で極めて重要なステップです。このレッスンでは、コンポーネントの本質、登録方法、命名規則、そしてVueにおいてSFC(シングルファイルコンポーネント)が推奨される理由について理解を深めることができます。
1. 学習内容
- コンポーネントとは何か? コンポーネントとVueインスタンスの関係
- シングルファイルコンポーネント(SFC)の3つの構成要素:テンプレート、スクリプト、スタイル
- グローバル登録とローカル登録の違い、およびどちらを選ぶべきか
- コンポーネントの命名規則(PascalCase / kebab-case)
- コンポーネントの再利用(コードの重複を避ける)
propsおよびemitの基本概念(次の2つのレッスンで詳しく解説します)- 5つの設計原則
2. コードの重複に悩まされる30ページのEコマース・バックエンド
(1) 課題:商品カード30枚、コピー&ペーストが30回必要
アリスのEコマース管理画面では、30枚の商品カードを複数のページにまたがって表示する必要がありました。彼女が最初に取ったアプローチは次のとおりでした:
<!-- ❌ The "Flip" Version: Copy Product Card 30 times -->
<!-- In HomeView.vue -->
<div class="product-card">
<img src="iphone.jpg">
<h3>iPhone 15 Pro</h3>
<p>$999</p>
<button>Add to Cart</button>
</div>
<div class="product-card">
<img src="macbook.jpg">
<h3>MacBook Pro</h3>
<p>$2499</p>
<button>Add to Cart</button>
</div>
<!-- ... And also 28 more ... -->
<!-- The same code appears again in ProductList.vue, SearchResults.vue, Cart.vue -->
プロダクトマネージャーのチャーリーが、5つの新しい要件を追加しました:
「アリス、『カートに追加』というボタンのテキストを『バッグに追加』に変更してほしい。あと、カードには在庫状況が表示されるようにして。それから、モバイルでもレスポンシブに対応するようにして。」
アリスは30以上のHTMLファイルを3回編集する必要がある=90回の編集。これは維持不可能です。
(2) Vueコンポーネントによる解決策:ProductCard.vueファイルを1つ作成し、30回再利用
<!-- components/ProductCard.vue - 1 Component Definitions -->
<template>
<div class="product-card">
<img :src="product.image">
<h3>{{ product.name }}</h3>
<p>${{ product.price }}</p>
<button :disabled="product.stock === 0" @click="addToCart">
{{ product.stock === 0 ? 'Out of Stock' : 'Add to Bag' }}
</button>
</div>
</template>
<script setup>
const props = defineProps({ product: Object })
const emit = defineEmits(['add-to-cart'])
function addToCart() {
emit('add-to-cart', props.product.id)
}
</script>
<!-- HomeView.vue / ProductList.vue / SearchResults.vue -->
<template>
<ProductCard v-for="product in products" :key="product.id" :product="product" @add-to-cart="handleAdd" />
</template>
1 ProductCard.vue → 30回再利用。ボタンのテキストを変更するには、1か所だけ変更すれば済みます。
(3) 収益
コンポーネント化後の状態:
- コード量:30 × 15行 × 3ページ = 1350行 → 1コンポーネント + 30行 × 3 = 90行 (-93%)
- 1件の変更:30件の商品カードが自動的に更新されます
- 新機能を追加:コンポーネントに追加すると、すべてのページに自動的に表示されます
- テスト可能性:ProductCard.vue は単独でテスト可能です
3. コンポーネントとは何か?
(1) 定義
Vueにおけるコンポーネントとは、独自のロジックとスタイルを持つ再利用可能なUI単位のことです。各コンポーネントは、1つのVueインスタンスと、それ固有のデータ、メソッド、ライフサイクルで構成されています。
graph TB
A[App.vue<br/>Root Component] --> B[HomeView.vue<br/>Home]
A --> C[AboutView.vue<br/>About]
A --> D[DashboardView.vue<br/>Dashboard]
B --> E[ProductCard.vue<br/>Product Card]
B --> F[ProductList.vue<br/>Product List]
B --> G[FilterBar.vue<br/>Filter Bar]
E --> H[Button.vue<br/>Button]
E --> I[Badge.vue<br/>Badge]
F --> E
G --> J[Select.vue<br/>Scroll down]
G --> K[Input.vue<br/>Input]
style A fill:#42b883,color:#fff
style E fill:#42b883,color:#fff
style H fill:#42b883,color:#fff
(2) 5つの主な特徴
| 機能 | 説明 |
|---|---|
| 再利用可能 | 1つのコンポーネントをNか所で使用可能 |
| カプセル化 | 互いに干渉しないように、データ、メソッド、およびスタイルを分離すること |
| モジュール式 | 小さな構成要素を組み合わせて、より大きな構成要素を形成する |
| テスト可能 | 単一コンポーネントのユニットテスト |
| 保守性 | 1か所を変更 = Nか所を変更 |
(3) コンポーネントと関数
| 寸法 | 機能 | 構成部品 |
|---|---|---|
| 再利用可能なコンポーネント | コードロジック | UI + ロジック |
| 入力 | 引数 (args) | プロパティ |
| 出力 | 戻り値 | 出力 |
| 状態 | ローカル変数 | データ / 参照 |
| 副作用 | なし | ライフサイクルフック |
4. シングルファイルコンポーネント(SFC)
(1) SFCとは何ですか?
SFC(シングルファイルコンポーネント)=1つの.vueファイル=1つのコンポーネント。 template、script、styleの各セクションを1つのファイルにまとめることで、管理が容易になります。
<!-- ProductCard.vue - SFC Single-File Components -->
<template>
<!-- 1. template:HTML Template(Required) -->
<div class="product-card">
<h3>{{ product.name }}</h3>
<p>${{ product.price }}</p>
</div>
</template>
<script setup>
// 2. script:JS Logic(Required)
import { ref } from 'vue'
const props = defineProps({ product: Object })
const count = ref(0)
</script>
<style scoped>
/* 3. style:CSS Style(Optional) */
.product-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1rem;
}
</style>
(2) SFC第3部の詳細な解説
| セクション | 機能 | 必須? | 言語 |
|---|---|---|---|
<template> |
HTMLテンプレート | ✅ | HTML + Vueコマンド |
<script setup> |
JS Logic | ✅ | JavaScript / TypeScript |
<style scoped> |
CSSスタイル | ❌ | CSS / SCSS / Less |
(3) SFC 対 HTML 文字列
// ❌ No need SFC(String Templates,Vue 2 Style)
Vue.component('my-component', {
template: '<div>{{ msg }}</div>',
data() { return { msg: 'Hello' } }
})
// ✅ Use SFC (Recommended)
// MyComponent.vue
<template>
<div>{{ msg }}</div>
</template>
<script setup>
const msg = 'Hello'
</script>
SFCのメリット:
- エディタの機能(構文強調表示、ナビゲーション、自動補完)
- ツールチェーンのサポート(Viteのホットリローディング、TypeScriptの型チェック)
- 関心の分離(HTML、JS、CSSをそれぞれ1ブロックずつ)
5. コンポーネントの登録
(1) グローバル登録(大規模なプロジェクトには推奨されません)
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import ProductCard from './components/ProductCard.vue'
const app = createApp(App)
// Global Registration:All components are available <ProductCard />
app.component('ProductCard', ProductCard)
app.mount('#app')
(2) 現地登録(推奨)
<!-- HomeView.vue -->
<script setup>
// 1. Import
import ProductCard from '@/components/ProductCard.vue'
// 2. Use in a template(No registration required,Automatically Available)
</script>
<template>
<ProductCard :product="product" />
</template>
(3) 2つの登録方法の比較
| 次元 | グローバル登録 | ローカル登録 |
|---|---|---|
| 適用範囲 | アプリケーション全体 | 現在のコンポーネント |
| コードの位置 | main.js |
コンポーネント <script setup> 内 |
| ツリーシェーキング | ❌ 未対応 | ✅ 対応 |
| 型ヒント | 弱 | 強 (IDEの自動補完) |
| おすすめ度 | ⭐⭐ プラグイン用 | ⭐⭐⭐⭐⭐ ビジネス利用用 |
6. コンポーネントの命名規則
(1) 3 命名規則
| スタイル | 例 | 使い方 | 推奨事項 |
|---|---|---|---|
| PascalCase | ProductCard.vue |
JSのインポート / テンプレート内 | ⭐⭐⭐⭐⭐ |
| ケバブ式大文字小文字 | product-card.vue |
ファイル名 / ケバブ式大文字小文字のテンプレート | ⭐⭐⭐ |
| camelCase | productCard.vue |
推奨されません | ⭐ |
(2) テンプレートでの使用
<template>
<!-- PascalCase(Recommendations,IDE Friendly) -->
<ProductCard />
<UserProfile />
<!-- kebab-case(Also supported) -->
<product-card />
<user-profile />
<!-- ✅ Either one works.,PascalCase Easier to read -->
</template>
(3) フォルダ構成
src/
├── components/ # Public Components(For use across pages)
│ ├── ProductCard.vue
│ ├── Button.vue
│ └── Header.vue
├── views/ # Page Components(For routing)
│ ├── HomeView.vue
│ ├── ProductListView.vue
│ └── DashboardView.vue
├── composables/ # Composite Functions
│ ├── useAuth.ts
│ └── useFetch.ts
├── router/ # Router Configuration
└── App.vue # Root Component
7. props と emit の基礎
(1) プロパティ:親から子へ渡される
<!-- Parent Component Parent.vue -->
<template>
<ProductCard :product="myProduct" :show-stock="true" />
</template>
<!-- Child component ProductCard.vue -->
<template>
<div>
<h3>{{ product.name }}</h3>
<p v-if="showStock">Stock: {{ product.stock }}</p>
</div>
</template>
<script setup>
// Receive the data passed from the parent component props
const props = defineProps({
product: { type: Object, required: true },
showStock: { type: Boolean, default: false }
})
</script>
(2) 発散:子から親へ
<!-- Child component ProductCard.vue -->
<template>
<button @click="handleAdd">Add to Cart</button>
</template>
<script setup>
const emit = defineEmits(['add-to-cart'])
function handleAdd() {
emit('add-to-cart', { id: 1, name: 'iPhone' })
}
</script>
<!-- Parent Component Parent.vue -->
<template>
<ProductCard @add-to-cart="handleAddToCart" />
</template>
<script setup>
function handleAddToCart(product) {
console.log('Add to cart:', product)
}
</script>
(3) props と emit のクイックリファレンス
| カテゴリ | API | 目的 |
|---|---|---|
| 親 → 子 | defineProps() |
データ転送 |
| 子 → 親 | defineEmits() |
イベント通知 |
8. 完全な例:5つのProductCardの再利用
(1) ▶ サンプル:. ProductCard.vue シングルファイルコンポーネント
<!-- src/components/ProductCard.vue -->
<template>
<div :class="['product-card', { 'out-of-stock': product.stock === 0 }]">
<img :src="product.image" :alt="product.name">
<h3>{{ product.name }}</h3>
<p class="price">${{ product.price }}</p>
<span class="stock" v-if="product.stock < 10 && product.stock > 0">
Only {{ product.stock }} left
</span>
<button
:disabled="product.stock === 0"
@click="handleAddToCart"
>
{{ product.stock === 0 ? 'Out of Stock' : 'Add to Cart' }}
</button>
</div>
</template>
<script setup>
const props = defineProps({
product: {
type: Object,
required: true,
validator: (val) => val.id && val.name && val.price
}
})
const emit = defineEmits(['add-to-cart'])
function handleAddToCart() {
if (props.product.stock > 0) {
emit('add-to-cart', props.product.id)
}
}
</script>
<style scoped>
.product-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1rem;
transition: box-shadow 0.2s;
}
.product-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.out-of-stock {
opacity: 0.6;
}
.price {
color: #42b883;
font-weight: bold;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
(2) ▶ サンプル:. 3つのページでProductCardを再利用する
<!-- src/views/HomeView.vue -->
<template>
<h1>Featured Products</h1>
<div class="grid">
<ProductCard
v-for="product in featured"
:key="product.id"
:product="product"
@add-to-cart="handleAdd"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ProductCard from '@/components/ProductCard.vue'
import { useCart } from '@/composables/useCart'
const featured = ref([
{ id: 1, name: 'iPhone 15 Pro', price: 999, stock: 50, image: 'iphone.jpg' },
{ id: 2, name: 'MacBook Pro', price: 2499, stock: 20, image: 'macbook.jpg' }
])
const { addToCart } = useCart()
function handleAdd(productId) {
addToCart(productId)
}
</script>
<!-- src/views/ProductListView.vue -->
<template>
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
@add-to-cart="handleAdd"
/>
</template>
<script setup>
import ProductCard from '@/components/ProductCard.vue'
// ... Exactly the same citation,Reusable Components
</script>
(3) ▶ サンプル:. グローバル登録とローカル登録の比較
// ❌ main.js Global Registration(Not recommended)
import ProductCard from './components/ProductCard.vue'
app.component('ProductCard', ProductCard) // The entire app is available
// ✅ HomeView.vue Local Registration(Recommendations)
<script setup>
import ProductCard from '@/components/ProductCard.vue'
// Only at HomeView.vue Available in China
</script>
(4) ▶ サンプル:. 小道具の5つの主な種類
<script setup>
defineProps({
// Basic Types
name: String,
age: Number,
active: Boolean,
// Complex Types
user: Object,
items: Array,
// Required + Default value
title: { type: String, required: true },
pageSize: { type: Number, default: 20 },
// Validator
email: {
type: String,
validator: (val) => val.includes('@')
},
// Custom Types
status: {
type: String,
validator: (val) => ['active', 'inactive'].includes(val)
}
})
</script>
(5) ▶ サンプル:. コンポーネントの5つの設計原則
| 原則 | 説明 |
|---|---|
| 単一責任の原則 | 1つのコンポーネントは1つのことだけを行う |
| 再利用可能 | 抽象的な汎用UI(ボタン/入力フィールド/モーダル) |
| 設定可能 | props は動作を制御し、slots はコンテンツを制御する |
| モジュール式 | 小さな構成要素を組み合わせて、より大きな構成要素を形成する |
| テストが容易 | 独立したコンポーネントにより、ユニットテストが容易 |
(6) ▶ サンプル:. よくある5つの間違い
| エラー | 症状 | 解決策 |
|---|---|---|
| コンポーネント一覧ファイル | IDEエラー | PascalCaseを使用してください: ProductCard.vue |
| グローバル登録が多すぎる | バンドルサイズが大きい | ローカル登録はツリーシェーキングに対応 |
| コンポーネントが大きすぎる | メンテナンスが困難 | より小さなコンポーネントに分割する |
| props を直接変更 | Vue の警告 | props は読み取り専用です。代わりに emit を使用してください |
| コンポーネントのインポートを忘れた | テンプレートに「不明なコンポーネント」と表示される | <script setup> のインポート設定を確認してください |
❓ よくある質問
<el-button>など);(2) アプリケーション全体で使用される基本コンポーネント(Button/Icon)。ビジネスコンポーネントは、ツリーシェイクが有効になるよう、常にローカルに登録する必要があります。<template>と<script>は必須ですが、<style>は任意です。コンポーネントがJavaScriptのみで構成されている場合(例えば、データカプセル化のみを目的としている場合など)、代わりにテンプレートを省略することも可能です(render関数を使用します)。mount(ProductCard, { props: { product: mockProduct } })。このチュートリアルのフェーズ 4.6 で詳しく解説しています。<el-button> を直接使用します。詳細については、Phase の第 4.7 章を参照してください。📖 まとめ
- コンポーネントとは、テンプレート、スクリプト、スタイルの各セクションをカプセル化した、再利用可能な Vue インスタンスのことです。
- SFC(.vueファイル)はVue 3で推奨されているアプローチであり、エディタやツールチェーンによって十分にサポートされています。
- ローカル登録 > グローバル登録(ツリーシェイク対応)
- 命名規則:PascalCase(ProductCard) > kebab-case(product-card)
- props:親 → 子(defineProps)、emit:子 → 親(defineEmits)
- コンポーネント設計の原則:単一責任、再利用性、設定可能性、組み合わせ可能性、テスト可能性
- フォルダ構成:components/(共有コンポーネント)、views/(ページコンポーネント)、composables/(コンポーザブル関数)
📝 練習問題
(1) 基礎問題(難易度:⭐)
Button コンポーネントを作成する src/components/BaseButton.vue:
- props:
text(文字列),variant(文字列, 'primary'/'success'/'danger') - 3つのバリエーションは3つのクラスに対応しています
clickイベントを発行する- HomeView.vue で 3 種類のバリエーションを使用する
(2) 上級問題(難易度:⭐⭐)
商品リストとカードシステムの作成:
- ProductCard.vue: 単一商品カード(props: product、emit: add-to-cart)
- ProductList.vue: 商品リストのコンテナ(v-for + フィルタリング)
- App.vue:ルートコンポーネント。データを渡し、「カートに追加」イベントを処理する
- ネストされた構造で使用される3つのファイルと3つのコンポーネント
(3) チャレンジ問題(難易度:⭐⭐⭐)
完全な「ユーザーカード」コンポーネントシステムを実装する:
- Avatar.vue: アバターコンポーネント(URLモードとイニシャルモードの両方に対応)
- UserCard.vue:ユーザーカード(アバター付き)
- UserList.vue: ユーザー一覧(UserCard を使用)
- App.vue:ルートコンポーネント(データ+インタラクション)
- 5つのプロパティ(アバター/名前/メールアドレス/役割/アクティブ)
- 3 イベントの発生(編集/削除/選択)
- スコープによるスタイルの分離



