コンポーネントとレイアウト
Charlieのチームは5人の開発者で構成されていますが, コンポーネントの書き方がバラバラです。手動でインポートする人もいれば, グローバル登録する人もいて, 命名規約も統一されていません。Aliceはページが不要なコンポーネントを大量に読み込んでおり, ファーストビューの読み込みが遅くなっています。Bobは管理ダッシュボードに別のレイアウトを使いたいと思っていますが, 切り替え方がわかりません。
1. 学ぶ内容
- 自動コンポーネントインポート:ディレクトリスキャンルールと命名規約
- レイアウトシステム:デフォルトレイアウトとカスタムレイアウトの切り替え
- コンポーネント遅延読み込み:「Lazy」プレフィックスとSuspense
- PropsとEmitsのベストプラクティス
- MegaShop Header/Footer/ProductCardのハンズオンガイド
2. チームの実話
(1) ペインポイント:乱雑なコンポーネント管理
Charlieの5人チームはバラバラに作業しています。Aliceは各コンポーネントを手動インポートし, Bobはグローバル登録を使ってバンドルサイズを肥大化させ, 新入社員はコンポーネント名がわかりません。1つのProductCardを異なるファイルで3つの異なる方法で参照しています。
(2) Nuxt 3自動インポートによる解決策
Nuxt 3はcomponents/ディレクトリを自動スキャンし, ルールに従ってコンポーネント名を生成し, 全員が一貫した方法で参照できるようにします:
VUE
<!-- インポート不要, そのまま使う -->
<ProductCard :product="item" />
(3) 効果:標準化+サイズ削減
チームのコンポーネント参照方法が標準化されました。「Lazy」プレフィックスにより, 可視範囲のコンポーネントのみがファーストビューで読み込まれ, MegaShopのファーストビューJavaScriptサイズが40%削減されました。
3. 自動コンポーネントインポート
(1) ディレクトリスキャンと命名規約
graph TB
A[components/] --> B[ルートレベル]
A --> C[サブディレクトリ]
A --> D[ネストディレクトリ]
B --> E[ProductCard.vue → ProductCard]
C --> F[product/Card.vue → ProductCard ①]
D --> G[shop/product/Card.vue → ShopProductCard]
style G fill:#ffe,stroke:#f90
⚠️ 注意: ①
pathPrefix: falseを設定するとproduct/Card.vue → Cardとなり, 名前の競合が発生する可能性があります。デフォルトのプレフィックスを維持することを推奨します。
(2) 命名規約の比較
| ファイルパス | デフォルトコンポーネント名 | pathPrefix: false |
|---|---|---|
| components/AppHeader.vue | AppHeader | AppHeader |
| components/product/Card.vue | ProductCard | Card |
| components/shop/ProductCard.vue | ShopProductCard | ProductCard |
| components/admin/user/Table.vue | AdminUserTable | Table |
(1) ▶ サンプル:自動コンポーネントインポート
VUE
<!-- components/ProductCard.vue -->
<template>
<div class="product-card">
<img :src="product.image" :alt="product.name" />
<h3>{{ product.name }}</h3>
<p>${{ product.price }} USD</p>
<button @click="$emit('add-to-cart', product)">カートに追加</button>
</div>
</template>
<script setup lang="ts">
interface Product {
id: number
name: string
price: number
image: string
}
defineProps<{ product: Product }>()
defineEmits<{ 'add-to-cart': [product: Product] }>()
</script>
出力:
TEXT
// 実行成功
(2) ▶ サンプル:ページで自動インポートコンポーネントを使用
VUE
<!-- pages/products/index.vue -->
<template>
<div>
<h1>全商品</h1>
<div class="grid">
<!-- ProductCardは自動インポート -->
<ProductCard
v-for="p in products"
:key="p.id"
:product="p"
@add-to-cart="handleAdd"
/>
</div>
</div>
</template>
<script setup lang="ts">
const { data: products } = await useFetch('/api/products')
function handleAdd(product: any) {
console.log('追加:', product.name)
}
</script>
出力:
TEXT
// 実行成功
4. レイアウトシステム
(1) レイアウトの仕組み
graph TB
A[app.vue] --> B[NuxtLayout]
B --> C{現在のレイアウト}
C -->|default| D[layouts/default.vue]
C -->|sidebar| E[layouts/sidebar.vue]
D --> F[NuxtPage - ページコンテンツ]
E --> F
(2) app.vueとレイアウトの関係
VUE
<!-- app.vue - レイアウト対応のルートコンポーネント -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
(1) ▶ サンプル:デフォルトレイアウト
VUE
<!-- layouts/default.vue -->
<template>
<div class="layout-default">
<AppHeader />
<main class="content">
<!-- ページコンテンツはここにレンダリング -->
<slot />
</main>
<AppFooter />
</div>
</template>
出力:
TEXT
// 実行成功
(2) ▶ サンプル:サイドバーレイアウト
VUE
<!-- layouts/sidebar.vue -->
<template>
<div class="layout-sidebar">
<AppHeader />
<div class="main-wrapper">
<aside class="sidebar">
<nav>
<NuxtLink to="/admin">ダッシュボード</NuxtLink>
<NuxtLink to="/admin/products">商品</NuxtLink>
<NuxtLink to="/admin/orders">注文</NuxtLink>
</nav>
</aside>
<main class="content">
<slot />
</main>
</div>
</div>
</template>
出力:
TEXT
// 実行成功
(3) ▶ サンプル:ページでのレイアウト切替
VUE
<!-- pages/admin/index.vue -->
<template>
<div>
<h1>管理ダッシュボード</h1>
<p>ようこそ, Bobさん</p>
</div>
</template>
<script setup lang="ts">
// このページでサイドバーレイアウトに切り替え
definePageMeta({
layout: 'sidebar'
})
</script>
出力:
TEXT
// 実行成功
(3) レイアウトとネストルートの比較
| 項目 | レイアウト | ネストルーティング |
|---|---|---|
| 定義場所 | layouts/*.vue | pages/parent.vue |
| コンテンツスロット | <slot /> |
<NuxtPage /> |
| 切替方法 | definePageMeta | 自動 (ルーティング階層) |
| データ取得 | 非推奨 | 各自が必要なものを取得 |
| ユースケース | 全体的なページレイアウト | ルーティング階層間のUI共有 |
5. コンポーネントの遅延読み込み
(1) 遅延読み込みの仕組み
Nuxt 3は各コンポーネントの「Lazy」バージョンを自動登録し, コンポーネントが初めてレンダリングされる時点でのみコードを読み込みます。
(1) ▶ サンプル:Lazyプレフィックスによる遅延読み込み
VUE
<template>
<div>
<!-- 即時読み込み - 初期バンドルに含まれる -->
<AppHeader />
<!-- Lazy読み込み - コード分割, 初回レンダリング時に読み込み -->
<LazyProductCard
v-for="p in products"
:key="p.id"
:product="p"
/>
<!-- v-if付きLazy - 条件が真の時のみ読み込み -->
<LazyProductReviewModal
v-if="showReviewModal"
:product-id="selectedProductId"
@close="showReviewModal = false"
/>
</div>
</template>
出力:
TEXT
// 実行成功
(2) ▶ サンプル:Suspenseによる非同期読み込み
VUE
<template>
<div>
<Suspense>
<template #default>
<LazyHeavyChart :data="chartData" />
</template>
<template #fallback>
<div class="loading">チャートを読み込み中...</div>
</template>
</Suspense>
</div>
</template>
出力:
TEXT
// 実行成功
(2) 遅延読み込み戦略の比較
| 戦略 | 実装方法 | 読み込みタイミング | 適用シナリオ |
|---|---|---|---|
| 即時読み込み | <ProductCard /> |
ページ読み込み時 | ファーストビューのコアコンポーネント |
| Lazyプレフィックス | <LazyProductCard /> |
初回レンダリング時 | フォールド下のコンポーネント |
| v-if + Lazy | <LazyModal v-if="show" /> |
条件が真の時 | ポップアップ/ドロワー |
| Suspense | <Suspense>でラップ |
非同期操作の準備完了時 | 「読み込み中」状態が必要な場合 |
6. PropsとEmitsのベストプラクティス
(1) ▶ サンプル:型安全なPropsとEmits
VUE
<!-- components/ProductCard.vue -->
<script setup lang="ts">
interface Product {
id: number
name: string
price: number
image: string
inStock: boolean
}
// デフォルト値付きの型安全なprops
const props = withDefaults(
defineProps<{
product: Product
showStock?: boolean
currency?: string
}>(),
{
showStock: true,
currency: 'USD'
}
)
// 型安全なemits
const emit = defineEmits<{
'add-to-cart': [product: Product]
'toggle-wishlist': [productId: number]
}>()
</script>
<template>
<div class="card">
<img :src="product.image" :alt="product.name" />
<h3>{{ product.name }}</h3>
<p>{{ product.price }} {{ currency }}</p>
<p v-if="showStock">
{{ product.inStock ? '在庫あり' : '在庫切れ' }}
</p>
<button @click="emit('add-to-cart', product)">カートに追加</button>
<button @click="emit('toggle-wishlist', product.id)">お気に入り</button>
</div>
</template>
出力:
TEXT
// 実行成功
7. 総合例:MegaShopレイアウトコンポーネントシステム
VUE
<!-- components/AppHeader.vue -->
<template>
<header class="app-header">
<NuxtLink to="/" class="logo">MegaShop</NuxtLink>
<nav>
<NuxtLink to="/products">商品</NuxtLink>
<NuxtLink to="/categories">カテゴリ</NuxtLink>
</nav>
<div class="user-area">
<NuxtLink to="/cart">カート ({{ cartCount }})</NuxtLink>
<NuxtLink v-if="isLoggedIn" to="/admin">管理</NuxtLink>
<NuxtLink v-else to="/login">ログイン</NuxtLink>
</div>
</header>
</template>
<script setup lang="ts">
const cart = useState<any[]>('cart', () => [])
const cartCount = computed(() => cart.value.length)
const isLoggedIn = useState('isLoggedIn', () => false)
</script>
VUE
<!-- components/AppFooter.vue -->
<template>
<footer class="app-footer">
<p>© 2026 MegaShop. All rights reserved.</p>
<nav>
<NuxtLink to="/about">About</NuxtLink>
<NuxtLink to="/contact">お問い合わせ</NuxtLink>
<NuxtLink to="/privacy">プライバシーポリシー</NuxtLink>
</nav>
</footer>
</template>
VUE
<!-- layouts/default.vue -->
<template>
<div class="layout-default">
<AppHeader />
<main>
<slot />
</main>
<AppFooter />
</div>
</template>
❓ よくある質問
Q コンポーネントが自動インポートされないのはなぜですか?
A ファイルが
components/ディレクトリにあるか, ファイル名が大文字で始まっているか, nuxt.config.tsのcomponents設定でパスが除外されていないかを確認してください。開発サーバーの再起動も試してみてください。Q
Lazyプレフィックスはコンポーネントのレンダリングを遅らせますか?A いいえ。
LazyはコンポーネントのJavaScriptコードの読み込みを遅らせるだけで, レンダリング後は正常に動作します。コード分割を行うものであり, レンダリングの遅延ではありません。Q レイアウトでデータを取得できますか?
A 技術的には可能ですが, 推奨されません。レイアウトはUIフレームワークであり, データ取得はページまたはコンポーザブルで処理するべきです。レイアウトはレイアウト構造のみを担当します。
Q 1つのページで複数のレイアウトを使用できますか?
A いいえ。各ページは
definePageMetaで1つのレイアウトのみ指定できます。異なるセクションで異なるレイアウトが必要な場合は, コンポーネントの組み合わせで実現してください。Q
definePageMetaはscript setupの最上位になければなりませんか?A はい。
definePageMetaはコンパイラマクロであり, <script setup>の最上位で直接呼び出す必要があります。条件文や関数の中に置くことはできません。Q
components/の下に深すぎるサブディレクトリにコンポーネントを配置するとパフォーマンスに影響しますか?A ランタイムパフォーマンスには影響しません。Nuxt 3は実際に使用されるコンポーネントのみをバンドルするからです。ただし, ディレクトリ階層が深すぎると命名が複雑になるため, 2階層までに制限することを推奨します。
📖 まとめ
- Nuxt 3自動コンポーネントインポート:ディレクトリパスに基づいてコンポーネント名を生成し, 手動インポートが不要
- レイアウトシステムは
definePageMetaでページレイアウトを切り替え,slotがページコンテンツを保持 - 「Lazy」プレフィックスでコンポーネントの遅延読み込みが可能になり, ファーストビューのJavaScriptサイズを削減
- Props/Emits:TypeScriptジェネリクスで型安全性を実現
- MegaShopはフロントエンドと管理画面用に「default」と「sidebar」の2つのレイアウトを使用
📝 練習問題
- 基本問題 (難易度:⭐):AppHeaderとAppFooterコンポーネントを作成し, デフォルトレイアウトで使用してください。
- 応用問題 (難易度:⭐⭐):サイドバーレイアウトを作成し, 管理ページで
definePageMetaを使って2つのレイアウトを切り替え, 結果を比較してください。 - チャレンジ (難易度:⭐⭐⭐):Lazyプレフィックスとv-ifを使って, クリック時のみ読み込まれる商品詳細ポップアップコンポーネントを実装し, Suspenseで読み込み中状態を表示してください。
---|



