Nuxt: 综合练习 — 入门基础
最后更新:2026-08-26
Phase 1 的 5 课内容已经学完,是时候把所有知识串起来。Charlie 要求团队搭建一个可运行的 MegaShop 基础版——有首页、分类页、商品详情页,还有 SSR 预取和正确的布局切换。Alice 会作为消费者验证页面体验。
1. 你将学到
- 搭建完整的 MegaShop 页面骨架
- 实现 default/sidebar 两种布局自动切换
- 使用 useFetch 从 API 预取商品数据并渲染
- 处理动态路由参数与 404 页面
- 代码审查:SSR 数据水合与 auto-import 正确性
2. 一个团队的真实故事
(1) 痛点:零散知识无法组装
Alice 学了路由、组件、数据获取,但不知道怎么组合成一个完整项目。Bob 写的页面没有布局,商品数据加载后页面闪烁。Charlie 需要一个能演示给投资人的 MVP。
(2) 综合实战的解法
把 Phase 1 的所有知识点融合到一个项目中:文件路由 + 布局系统 + useFetch SSR 预取 + 组件自动导入,从零搭建一个可运行的电商平台。
(3) 收益:从零件到产品
完成本章后,Alice 拥有一个可运行的 MegaShop 基础版——4 个页面、2 种布局、SSR 预取商品数据、SEO 友好的 HTML 输出。
3. 项目规划
(1) MegaShop 基础版架构
flowchart TB
A[MegaShop Phase 1] --> B[Pages]
A --> C[Layouts]
A --> D[Components]
A --> E[API Routes]
B --> B1[/ → Homepage]
B --> B2[/products → Product List]
B --> B3[/products/[id] → Product Detail]
B --> B4[/about → About Page]
C --> C1[default → Header + Footer]
C --> C2[sidebar → Admin 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 | 关于页面 |
| /admin | pages/admin/index.vue | sidebar | 管理后台 |
| 404 | 页面不存在 | default | 自定义 404 |
4. 搭建页面骨架
▶ 示例:app.vue 根组件
VUE
<!-- app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:default 布局
VUE
<!-- layouts/default.vue -->
<template>
<div class="layout-default">
<AppHeader />
<main class="container">
<slot />
</main>
<AppFooter />
</div>
</template>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:sidebar 布局
VUE
<!-- layouts/sidebar.vue -->
<template>
<div class="layout-sidebar">
<AppHeader />
<div class="main-wrapper">
<aside class="sidebar">
<h3>Admin Panel</h3>
<nav>
<NuxtLink to="/admin">Dashboard</NuxtLink>
<NuxtLink to="/admin/products">Products</NuxtLink>
<NuxtLink to="/admin/orders">Orders</NuxtLink>
</nav>
</aside>
<main class="content">
<slot />
</main>
</div>
</div>
</template>
输出:
TEXT
📖 仅展示
// 执行成功
5. 核心 API 路由
▶ 示例:商品列表 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
📖 仅展示
// 执行成功
▶ 示例:商品详情 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. 页面实现
▶ 示例:首页 SSR 预取
VUE
<!-- pages/index.vue -->
<template>
<div class="homepage">
<section class="hero">
<h1>MegaShop</h1>
<p>Over 1 million products, shipped worldwide</p>
<NuxtLink to="/products" class="cta">Shop Now</NuxtLink>
</section>
<section class="featured">
<h2>Featured Products</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
📖 仅展示
// 执行成功
▶ 示例:商品详情页
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 }} reviews · {{ product.rating }} stars</p>
<button @click="addToCart">Add to Cart</button>
</div>
</div>
<div v-else class="not-found">
<h1>Product Not Found</h1>
<NuxtLink to="/products">Back 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 传递 | 客户端不重复请求 | Network 里有重复 API 请求 |
| route.params | 在 setup 顶层获取 | 在 onMounted 里获取(SSR 无此钩子) |
(2) Auto-import 正确性检查
| 检查项 | 正确做法 | 常见错误 |
|---|---|---|
| 组件引用 | <ProductCard /> 自动导入 |
手动 import 组件 |
| Composable | useFetch() 直接使用 |
手动 import 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: 'Premium e-commerce with over 1 million products' }
]
}
},
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
components: [{ path: '~/components', pathPrefix: false }],
routeRules: {
'/': { prerender: true },
'/products': { swr: 60 },
'/products/**': { swr: 3600 },
'/admin/**': { ssr: false }
}
})
❓ 常见问题
Q 为什么首页预渲染后数据不更新?
A prerender: true 在构建时生成静态 HTML,数据不会自动更新。需要重新构建或改用 swr 策略。
Q 商品详情页有时 SSR 有时不 SSR?
A 检查 useFetch 是否正确 await。如果 useFetch 在条件语句内,SSR 可能跳过。确保在 setup 顶层调用。
Q 404 页面怎么触发?
A Nuxt 3 没有匹配到路由时自动显示 404。也可以在 API 层抛 createError({ statusCode: 404 })。
Q Layout 切换时有闪烁怎么办?
A Layout 切换时会重新渲染,可以用
<Transition> 包裹 slot 添加过渡动画,或使用 pageTransition 配置。Q mock 数据在生产环境怎么替换?
A Phase 2 会用 Prisma 替换 mock。当前 API 返回格式保持一致,后续只需改 server API 实现即可。
Q 怎么验证 SSR 是否生效?
A 浏览器"查看源代码"(Ctrl+U),如果 HTML 中包含商品名/价格,说明 SSR 生效。如果只有空 div,则是 CSR。
📖 小节
- MegaShop 基础版包含首页、商品列表、商品详情、关于页 4 个核心页面
- 两种布局:default(前台)和 sidebar(管理后台),通过 definePageMeta 切换
- useFetch SSR 预取确保 HTML 包含商品数据,SEO 友好
- server API 使用 mock 数据,后续 Phase 用 Prisma 替换
- 验证 SSR:查看源代码有数据、Network 无重复请求、auto-import 无报错
📝 作业
- 基础题(难度⭐):完成 MegaShop 基础版所有页面,确保所有路由可访问
- 进阶题(难度⭐⭐):添加商品分类筛选功能,在商品列表页用 watch + useFetch 实现分类切换自动刷新
- 挑战题(难度⭐⭐⭐):添加一个 admin/products 页面(sidebar 布局 + CSR 模式),实现商品表格展示,对比 CSR 与 SSR 页面在"查看源代码"时的差异
---|