Nuxt: 综合练习 — 入门基础

最后更新:2026-08-26

Phase 1 的 5 课内容已经学完,是时候把所有知识串起来。Charlie 要求团队搭建一个可运行的 MegaShop 基础版——有首页、分类页、商品详情页,还有 SSR 预取和正确的布局切换。Alice 会作为消费者验证页面体验。

1. 你将学到


2. 一个团队的真实故事

(1) 痛点:零散知识无法组装

Alice 学了路由、组件、数据获取,但不知道怎么组合成一个完整项目。Bob 写的页面没有布局,商品数据加载后页面闪烁。Charlie 需要一个能演示给投资人的 MVP。

(2) 综合实战的解法

把 Phase 1 的所有知识点融合到一个项目中:文件路由 + 布局系统 + useFetch SSR 预取 + 组件自动导入,从零搭建一个可运行的电商平台。

(3) 收益:从零件到产品

完成本章后,Alice 拥有一个可运行的 MegaShop 基础版——4 个页面、2 种布局、SSR 预取商品数据、SEO 友好的 HTML 输出。


3. 项目规划

(1) MegaShop 基础版架构

100%
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。

📖 小节


📝 作业

  1. 基础题(难度⭐):完成 MegaShop 基础版所有页面,确保所有路由可访问
  2. 进阶题(难度⭐⭐):添加商品分类筛选功能,在商品列表页用 watch + useFetch 实现分类切换自动刷新
  3. 挑战题(难度⭐⭐⭐):添加一个 admin/products 页面(sidebar 布局 + CSR 模式),实现商品表格展示,对比 CSR 与 SSR 页面在"查看源代码"时的差异

---|

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏