総合演習 — 入門編
第1フェーズの5つのレッスンが完了しましたので, すべてを統合する時が来ました。Charlieはチームに動作するMegaShopの基本版の構築を依頼しました。ホームページ, カテゴリページ, 商品詳細ページ, SSR事前取得, 適切なレイアウト切り替えを含みます。Aliceが顧客としてユーザー体験をテストします。
1. 学ぶ内容
- 完全なMegaShopページフレームワークの構築
- 「デフォルト」と「サイドバー」レイアウトの自動切り替えの実装
useFetchを使ってAPIから商品データを事前取得してレンダリング- 動的ルーティングパラメータと404ページの処理
- コードレビュー:SSRデータハイドレーションと自動インポートの正確性
2. チームの実話
(1) ペインポイント:断片的な知識を統合できない
Aliceはルーティング, コンポーネント, データ取得を学びましたが, 完全なプロジェクトにまとめる方法がわかりません。Bobが書いたページにはレイアウトがなく, 商品データが読み込まれた後にページがちらつきます。Charlieは投資家にデモするためのMVPが必要です。
(2) 実践的課題の総合的解決策
第1フェーズのすべての概念を1つのプロジェクトに統合します:ファイルルーティング+レイアウトシステム+useFetch SSR事前取得+自動コンポーネントインポートで, ゼロから動作するECプラットフォームを構築します。
(3) 効果:部品から製品へ
この章の終わりまでに, Aliceは動作するMegaShopの基本版を完成させます。4つのページ, 2つのレイアウト, SSR事前取得付きの商品データ, SEOに優しいHTML出力を備えています。
3. プロジェクト計画
(1) MegaShop基本版アーキテクチャ
flowchart TB
A[MegaShop フェーズ1] --> B[ページ]
A --> C[レイアウト]
A --> D[コンポーネント]
A --> E[APIルート]
B --> B1[/ → ホームページ]
B --> B2[/products → 商品一覧]
B --> B3[/products/[id] → 商品詳細]
B --> B4[/about → Aboutページ]
C --> C1[default → ヘッダー+フッター]
C --> C2[sidebar → 管理サイドバー]
D --> D1[AppHeader]
D --> D2[AppFooter]
D --> D3[ProductCard]
E --> E1[GET /api/products]
E --> E2[GET /api/products/:id]
(2) ページとルーティング設計
| URL | ページファイル | レイアウト | 機能 |
|---|---|---|---|
| / | pages/index.vue | default | ホーム:注目商品 |
| /products | pages/products/index.vue | default | 商品一覧 (SSR事前取得) |
| /products/[id] | pages/products/[id].vue | default | 商品詳細 (SSR事前取得) |
| /about | pages/about.vue | default | Aboutページ |
| /admin | pages/admin/index.vue | sidebar | 管理ダッシュボード |
| 404 | ページなし | default | カスタム404 |
4. ページ構造の構築
(1) ▶ サンプル:app.vueルートコンポーネント
VUE
<!-- app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
出力:
TEXT
// 実行成功
(2) ▶ サンプル:デフォルトレイアウト
VUE
<!-- layouts/default.vue -->
<template>
<div class="layout-default">
<AppHeader />
<main class="container">
<slot />
</main>
<AppFooter />
</div>
</template>
出力:
TEXT
// 実行成功
(3) ▶ サンプル:サイドバーレイアウト
VUE
<!-- layouts/sidebar.vue -->
<template>
<div class="layout-sidebar">
<AppHeader />
<div class="main-wrapper">
<aside class="sidebar">
<h3>管理パネル</h3>
<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
// 実行成功
5. コアAPIルーティング
(1) ▶ サンプル:商品一覧API
TYPESCRIPT
// server/api/products/index.get.ts
interface Product {
id: number; name: string; price: number
image: string; category: string; inStock: boolean
}
const mockProducts: Product[] = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
name: `Product ${i + 1}`,
price: Math.round(Math.random() * 500 * 100) / 100,
image: `/images/product-${i + 1}.webp`,
category: ['electronics', 'clothing', 'home'][i % 3],
inStock: Math.random() > 0.2
}))
export default defineEventHandler((event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 12
const category = query.category as string
let filtered = mockProducts
if (category) filtered = filtered.filter(p => p.category === category)
const start = (page - 1) * limit
return {
items: filtered.slice(start, start + limit),
total: filtered.length,
page,
limit
}
})
出力:
TEXT
// 実行成功
(2) ▶ サンプル:商品詳細API
TYPESCRIPT
// server/api/products/[id].get.ts
export default defineEventHandler((event) => {
const id = Number(getRouterParam(event, 'id'))
const product = {
id,
name: `Premium Product ${id}`,
price: 299.99,
image: `/images/product-${id}.webp`,
category: 'electronics',
inStock: true,
description: `High-quality product #${id} with premium features.`,
reviewCount: Math.floor(Math.random() * 5 * 1000),
rating: 4.5
}
if (id > 100) {
throw createError({ statusCode: 404, message: 'Product not found' })
}
return product
})
出力:
TEXT
// 実行成功
6. ページの実装
(1) ▶ サンプル:ホームページのSSR事前取得
VUE
<!-- pages/index.vue -->
<template>
<div class="homepage">
<section class="hero">
<h1>MegaShop</h1>
<p>100万以上の商品を世界中にお届け</p>
<NuxtLink to="/products" class="cta">今すぐショッピング</NuxtLink>
</section>
<section class="featured">
<h2>注目商品</h2>
<div class="grid">
<ProductCard v-for="p in featured" :key="p.id" :product="p" />
</div>
</section>
</div>
</template>
<script setup lang="ts">
const { data: featured } = await useFetch('/api/products', {
query: { limit: 6 },
transform: (data: any) => data.items
})
useHead({ title: 'MegaShop - Premium E-Commerce' })
</script>
出力:
TEXT
// 実行成功
(2) ▶ サンプル:商品詳細ページ
VUE
<!-- pages/products/[id].vue -->
<template>
<div v-if="product" class="product-detail">
<img :src="product.image" :alt="product.name" />
<div class="info">
<h1>{{ product.name }}</h1>
<p class="price">${{ product.price }} USD</p>
<p>{{ product.description }}</p>
<p>{{ product.reviewCount }} レビュー · {{ product.rating }} スター</p>
<button @click="addToCart">カートに追加</button>
</div>
</div>
<div v-else class="not-found">
<h1>商品が見つかりません</h1>
<NuxtLink to="/products">商品一覧に戻る</NuxtLink>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
useHead({
title: computed(() => product.value ? `${product.value.name} - MegaShop` : 'Not Found')
})
const cart = useState<any[]>('cart', () => [])
function addToCart() {
if (product.value) cart.value.push(product.value)
}
</script>
出力:
TEXT
// 実行成功
7. コードレビューチェックリスト
(1) SSRデータハイドレーションチェック
| チェック項目 | 正しいアプローチ | よくある間違い |
|---|---|---|
| useFetchの使用 | await useFetch()でデータを待機 |
awaitを忘れるとpending状態になる |
| SSRソースコード表示 | HTMLに商品データが含まれる | 空<div>のみ |
| Payload引き渡し | クライアントがリクエストを重複しない | ネットワークタブに重複APIリクエスト |
| route.params | setupの最上位で取得 |
onMountedで取得 (SSRではこのフックなし) |
(2) 自動インポート有効性チェック
| チェック項目 | 正しいアプローチ | よくある間違い |
|---|---|---|
| コンポーネント参照 | <ProductCard />自動インポート |
コンポーネントを手動インポート |
| コンポーザブル | useFetch()を直接使用 |
useFetchを手動インポート |
| useState | useState('key')を直接使用 |
'nuxt/app'から手動インポート |
| コンポーネント名 | ディレクトリプレフィックスルールに基づく参照 | コンポーネント名がファイルパスと不一致 |
8. 総合例:MegaShop基本版nuxt.config.ts
TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true,
app: {
head: {
title: 'MegaShop',
meta: [
{ name: 'description', content: '100万以上の商品を扱うプレミアムEC' }
]
}
},
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
components: [{ path: '~/components', pathPrefix: false }],
routeRules: {
'/': { prerender: true },
'/products': { swr: 60 },
'/products/**': { swr: 3600 },
'/admin/**': { ssr: false }
}
})
❓ よくある質問
Q ホームページをprerenderに設定するとデータが更新されないのはなぜですか?
A
prerender: trueを設定するとビルド時に静的HTMLが生成されるため, データは自動的に更新されません。再ビルドするか, swr戦略に切り替える必要があります。Q 商品詳細ページが時々SSRで時々そうでないのはなぜですか?
A
useFetchが正しくawaitと共に使用されているか確認してください。useFetchが条件文の中にある場合, SSRはそれをスキップする可能性があります。setupの最上位で呼び出すようにしてください。Q 404ページはどのようにトリガーされますか?
A Nuxt 3はルートがマッチしない場合に自動的に404ページを表示します。APIレベルで
createError({ statusCode: 404 })をスローすることもできます。Q レイアウト切り替え時にちらつきが発生する場合はどうすればよいですか?
A レイアウトは切り替え時に再レンダリングされます。
<Transition>でスロットをラップしてトランジションアニメーションを追加するか, pageTransition設定を使用できます。Q モックデータは本番環境でどのように置き換えられますか?
A フェーズ2でPrismaがモックを置き換えます。現在のAPIレスポンス形式は一貫性を保ちます。後でserver APIの実装を変更するだけで済みます。
Q SSRが動作しているかどうかを確認するにはどうすればよいですか?
A ブラウザの「ソースを表示」機能 (Ctrl+U)を使ってください。HTMLに商品名と価格が含まれていればSSRは動作しています。空のdivのみであればCSRです。
📖 まとめ
- MegaShop基本版は4つのコアページを含みます:ホーム, 商品一覧, 商品詳細, About
- 2つのレイアウト:「default」 (フロントエンド)と「sidebar」 (管理画面)。
definePageMetaで切り替え可能 - useFetch SSR事前取得によりHTMLに商品データが含まれ, SEOに優しい
- サーバーAPIはモックデータを使用。後続フェーズでPrismaを使用予定
- SSR検証:ソースコードにデータが含まれていること, ネットワークタブに重複リクエストがないこと, 自動インポートにエラーがないことを確認
📝 練習問題
- 基本問題 (難易度:⭐):MegaShop基本版のすべてのページを完成させ, すべてのルートがアクセス可能であることを確認してください。
- 応用問題 (難易度:⭐⭐):商品カテゴリフィルター機能を追加し, 商品一覧ページで
watchとuseFetchを使ってカテゴリ切り替え時に自動リフレッシュするようにしてください。 - チャレンジ (難易度:⭐⭐⭐):admin/productsページ (サイドバーレイアウト+CSRモード)を追加し, 商品テーブル表示を実装し, ソースコード表示時のCSRとSSRページの違いを比較してください。
---|



