Nuxt: 综合练习 — 核心功能
最后更新:2026-08-26
Phase 2 学了 Pinia、Composable、Server API、Middleware、Plugins——现在是把它们串起来的时候。Charlie 要求:购物车从 API 到 Store 到页面完整打通,Bob 的管理后台要有权限守卫,Alice 的购物体验要流畅无断点。
1. 你将学到
- 实现完整的购物车 CRUD API
- 集成 Pinia 购物车 Store 与 SSR 状态同步
- 开发 useCart、useProductFilter 自定义 Composable
- 配置 auth 全局中间件保护结算页
- 端到端联调:API → Composable → Store → 页面渲染
2. 一个团队的真实故事
(1) 痛点:各模块独立运行但无法串联
Alice 的购物车页面能显示商品,但刷新后数据丢失。Bob 的 API 写好了但前端没调用。中间件写了但没接入。每个模块单独看没问题,连起来就断链。
(2) 端到端联调的解法
把 API → Composable → Store → 页面这条数据链完整打通,每个环节的数据流向清晰。
(3) 收益:完整的购物流程
Alice 能添加商品到购物车 → 刷新不丢失 → 进入结算页 → 管理后台有权限保护,全流程无断点。
3. Phase 2 核心功能模块协作架构
graph TB
A[MegaShop Phase 2] --> B[Server API Layer]
A --> C[Composable Layer]
A --> D[State Layer - Pinia]
A --> E[Middleware Layer]
A --> F[Plugin Layer]
B --> B1[/api/products CRUD]
B --> B2[/api/cart CRUD]
B --> B3[/api/auth Login/Register]
C --> C1[useCart → Store + API]
C --> C2[useProductFilter → API]
C --> C3[usePriceFormat → Pure]
D --> D1[cartStore → items/total]
D --> D2[userStore → auth/profile]
E --> E1[auth.global.ts → Login check]
E --> E2[admin.ts → Role check]
F --> F1[logger → Debug]
F --> F2[stripe.client → Payment]
C1 --> D1
C1 --> B2
C2 --> B1
(1) 数据流完整路径
| 操作 | 数据流 | 涉及模块 |
|---|---|---|
| 浏览商品 | API → useFetch → 页面 | Server API |
| 添加购物车 | API → useCart → cartStore → 页面 | API + Composable + Store |
| 查看购物车 | cartStore → 页面 | Store |
| 结算 | cartStore → useCheckout → stripe → API | Store + Composable + Plugin + API |
| 管理后台 | auth middleware → admin middleware → 页面 | Middleware |
4. 完整购物车 API
▶ 示例:购物车 API 全套
TYPESCRIPT
// server/api/cart/index.get.ts
export default defineEventHandler((event) => {
const sessionId = getCookie(event, 'session-id') || 'default'
const cart = mockCarts.find(c => c.sessionId === sessionId)
return cart || { sessionId, items: [], total: 0 }
})
输出:
TEXT
📖 仅展示
// 执行成功
TYPESCRIPT
// server/api/cart/add.post.ts
export default defineEventHandler(async (event) => {
const sessionId = getCookie(event, 'session-id') || 'default'
const { productId, quantity = 1 } = await readBody(event)
const product = mockProducts.find(p => p.id === productId)
if (!product) throw createError({ statusCode: 404, message: 'Product not found' })
let cart = mockCarts.find(c => c.sessionId === sessionId)
if (!cart) {
cart = { sessionId, items: [], total: 0 }
mockCarts.push(cart)
}
const existing = cart.items.find(i => i.productId === productId)
if (existing) {
existing.quantity += quantity
} else {
cart.items.push({ productId, name: product.name, price: product.price, quantity, image: product.image })
}
cart.total = cart.items.reduce((s, i) => s + i.price * i.quantity, 0)
setCookie(event, 'session-id', sessionId, { httpOnly: true, maxAge: 86400 * 30 })
return cart
})
TYPESCRIPT
// server/api/cart/remove.delete.ts
export default defineEventHandler(async (event) => {
const sessionId = getCookie(event, 'session-id') || 'default'
const { productId } = await readBody(event)
const cart = mockCarts.find(c => c.sessionId === sessionId)
if (!cart) throw createError({ statusCode: 404, message: 'Cart not found' })
cart.items = cart.items.filter(i => i.productId !== productId)
cart.total = cart.items.reduce((s, i) => s + i.price * i.quantity, 0)
return cart
})
5. Pinia Store + Composable 集成
▶ 示例:增强版 cartStore
TYPESCRIPT
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const isLoading = ref(false)
const totalItems = computed(() => items.value.reduce((s, i) => s + i.quantity, 0))
const totalPrice = computed(() => items.value.reduce((s, i) => s + i.price * i.quantity, 0))
const formattedTotal = computed(() =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(totalPrice.value)
)
async function fetchCart() {
isLoading.value = true
try {
const cart = await $fetch('/api/cart')
items.value = cart.items || []
} finally {
isLoading.value = false
}
}
async function addItem(productId: number, quantity = 1) {
const cart = await $fetch('/api/cart/add', {
method: 'POST', body: { productId, quantity }
})
items.value = cart.items
}
async function removeItem(productId: number) {
const cart = await $fetch('/api/cart/remove', {
method: 'DELETE', body: { productId }
})
items.value = cart.items
}
function clearCart() {
items.value = []
}
return { items, isLoading, totalItems, totalPrice, formattedTotal, fetchCart, addItem, removeItem, clearCart }
})
interface CartItem { productId: number; name: string; price: number; quantity: number; image: string }
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:useCart Composable
TYPESCRIPT
// composables/useCart.ts
export function useCart() {
const cartStore = useCartStore()
const { $logger } = useNuxtApp()
const { notify } = useNotification()
async function addToCart(productId: number, productName: string) {
try {
await cartStore.addItem(productId)
notify(`${productName} added to cart`, 'success')
$logger.info('Item added to cart', { productId })
} catch (err) {
notify('Failed to add item', 'error')
$logger.error('Add to cart failed', { productId, err })
}
}
async function removeFromCart(productId: number) {
try {
await cartStore.removeItem(productId)
notify('Item removed from cart', 'info')
} catch (err) {
notify('Failed to remove item', 'error')
}
}
return {
items: computed(() => cartStore.items),
totalItems: computed(() => cartStore.totalItems),
totalPrice: computed(() => cartStore.totalPrice),
formattedTotal: computed(() => cartStore.formattedTotal),
isLoading: computed(() => cartStore.isLoading),
addToCart,
removeFromCart
}
}
输出:
TEXT
📖 仅展示
// 执行成功
6. 权限中间件配置
▶ 示例:完整的中间件体系
TYPESCRIPT
// middleware/01-auth.global.ts
export default defineNuxtRouteMiddleware((to) => {
const publicPaths = ['/', '/products', '/about', '/login', '/register']
if (publicPaths.some(p => to.path === p || to.path.startsWith('/products'))) return
const token = useCookie('auth-token')
if (!token.value) return navigateTo(`/login?redirect=${encodeURIComponent(to.path)}`)
})
输出:
TEXT
📖 仅展示
// 执行成功
TYPESCRIPT
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to) => {
const userCookie = useCookie('user-data')
if (!userCookie.value) return navigateTo('/login')
if ((userCookie.value as any).role !== 'admin') {
throw createError({ statusCode: 403, message: 'Admin access required' })
}
})
7. 端到端联调页面
▶ 示例:商品详情页(完整数据流)
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">{{ formattedPrice }}</p>
<p>{{ product.reviewCount }} reviews</p>
<button @click="handleAddToCart" :disabled="adding">
{{ adding ? 'Adding...' : 'Add to Cart' }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
const { formatted } = usePriceFormat(computed(() => product.value?.price || 0))
const formattedPrice = formatted
const { addToCart } = useCart()
const adding = ref(false)
async function handleAddToCart() {
if (!product.value) return
adding.value = true
await addToCart(product.value.id, product.value.name)
adding.value = false
}
</script>
输出:
TEXT
📖 仅展示
// 执行成功
8. 联调检查清单
| 检查项 | 验证方法 | 预期结果 |
|---|---|---|
| API 可用 | curl /api/products | 返回商品 JSON |
| Store 状态共享 | 首页加商品 → 详情页看数量 | 数量一致 |
| Composable 调用 | 点击 Add to Cart | 成功添加 + 通知 |
| 中间件保护 | 未登录访问 /admin | 跳转 /login |
| 插件工作 | 查看控制台 | 有 logger 输出 |
| SSR 数据水合 | 查看源代码 | HTML 含商品数据 |
| Cookie 持久化 | 刷新页面 | 购物车数据保留 |
❓ 常见问题
Q Store 和 API 数据不同步怎么办?
A Store 的 addItem 调用 API 后用返回值更新 items,保证 Store 和后端一致。不要只改 Store 不调 API。
Q Composable 里同时用 Store 和 $fetch 会不会冗余?
A 不会。Store 管客户端状态,$fetch 同步到后端。useCart 把两者封装在一起,调用方无需关心细节。
Q 中间件和 API middleware 都做鉴权,会冲突吗?
A 不会。页面中间件保护路由跳转(前端),server middleware 保护 API 请求(后端)。两者互补,都需要。
Q 如何调试端到端数据流?
A 用 Nuxt DevTools 的 Pinia 面板看 Store 状态,Network 面板看 API 请求,console 看 logger 输出。从 API 返回 → Store 更新 → 页面渲染,逐环节排查。
Q SSR 时购物车数据怎么初始化?
A SSR 时通过 cookie 读取 session-id,API 根据 sessionId 返回购物车。客户端水合后 Store 从 API payload 获取数据。
Q Phase 2 的 mock 数据什么时候换成真数据库?
A Phase 4 第 20 课会用 Prisma 替换 mock。当前 API 接口格式已设计好,后续只需改 server API 内部实现。
📖 小节
- 购物车完整数据流:API → useCart → cartStore → 页面,每个环节数据一致
- Pinia Store 调用 API 后用返回值更新状态,保证客户端与服务端同步
- useCart Composable 封装 Store + API + 通知,页面只需调用 addToCart
- 全局 auth + 命名 admin 中间件实现完整的路由权限守卫
- 端到端联调:逐环节验证 API/Store/Composable/Middleware/Plugin
📝 作业
- 基础题(难度⭐):完成购物车 API + cartStore 集成,在商品详情页点击 Add to Cart 后购物车数量更新
- 进阶题(难度⭐⭐):实现完整的中间件体系:未登录访问 /checkout 跳转登录,普通用户访问 /admin 返回 403
- 挑战题(难度⭐⭐⭐):实现端到端购物流程:浏览商品 → 加购 → 查看购物车 → 结算(用 stripe 插件模拟支付),全流程数据一致
---|