Appearance
第12章:企业级进阶实战(综合应用)
实战 3:简易电商首页
在本章中,我们将创建一个简易电商首页,这是一个更加复杂的企业级应用。通过这个项目,你将学习如何构建具有多个页面、复杂状态管理和数据交互的 Next.js 应用。
12.1 项目架构设计
12.1.1 需求分析
我们要创建一个简易电商首页,包含以下功能:
- 首页:轮播图、商品列表、分类导航
- 商品详情页:商品信息、规格选择、加入购物车
- 购物车:商品增删改查、数量计算
- 状态管理:使用 Zustand 管理购物车状态
- 数据获取:服务端渲染商品列表,客户端获取购物车数据
- 性能优化:图片懒加载、组件懒加载、缓存策略
12.1.2 技术栈选择
- 前端框架:Next.js 14 + React 18
- 状态管理:Zustand(轻量级状态管理库)
- 样式方案:Tailwind CSS
- 数据获取:Server Components + fetch API
- 图片优化:Next.js Image 组件
- 路由:App Router
12.1.3 项目结构规划
src/
app/
layout.js # 全局布局
page.js # 首页
product/ # 商品相关路由
[id]/ # 商品详情页
page.js
cart/ # 购物车页面
page.js
api/ # API 路由
products/ # 商品相关 API
route.js
components/ # 组件
Header.js # 头部组件
Footer.js # 底部组件
Carousel.js # 轮播图组件
ProductCard.js # 商品卡片组件
CategoryNav.js # 分类导航组件
CartItem.js # 购物车商品项组件
lib/ # 工具函数
store.js # Zustand 状态管理
products.js # 商品数据处理
styles/ # 样式文件
globals.css # 全局样式12.2 核心功能实现
12.2.1 项目初始化
步骤 1:创建 Next.js 项目
bash
npx create-next-app@latest ecommerce --tailwind --eslint --app --src-dir步骤 2:安装依赖
bash
cd ecommerce
npm install zustand12.2.2 状态管理配置
javascript
// src/lib/store.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useCartStore = create(
persist(
(set, get) => ({
cart: [],
// 添加商品到购物车
addToCart: (product, quantity = 1) => {
const existingItem = get().cart.find(item => item.id === product.id);
if (existingItem) {
set(state => ({
cart: state.cart.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + quantity }
: item
)
}));
} else {
set(state => ({
cart: [...state.cart, { ...product, quantity }]
}));
}
},
// 从购物车移除商品
removeFromCart: (productId) => {
set(state => ({
cart: state.cart.filter(item => item.id !== productId)
}));
},
// 更新商品数量
updateQuantity: (productId, quantity) => {
if (quantity <= 0) {
get().removeFromCart(productId);
} else {
set(state => ({
cart: state.cart.map(item =>
item.id === productId
? { ...item, quantity }
: item
)
}));
}
},
// 清空购物车
clearCart: () => {
set({ cart: [] });
},
// 获取购物车总价
getTotalPrice: () => {
return get().cart.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
},
// 获取购物车商品数量
getTotalItems: () => {
return get().cart.reduce((total, item) => {
return total + item.quantity;
}, 0);
}
}),
{
name: 'cart-storage',
}
)
);
export default useCartStore;12.2.3 模拟商品数据
javascript
// src/lib/products.js
export const products = [
{
id: 1,
name: 'Next.js 开发实战',
price: 99.99,
image: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=Next.js%20book%20cover%20with%20modern%20design&image_size=square_hd',
description: '一本全面介绍 Next.js 开发的实战指南,涵盖从基础到进阶的所有知识点。',
category: '书籍',
stock: 100
},
{
id: 2,
name: 'React 高级编程',
price: 89.99,
image: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=React%20advanced%20programming%20book%20cover&image_size=square_hd',
description: '深入学习 React 高级特性,包括 Hooks、Context API、性能优化等。',
category: '书籍',
stock: 80
},
{
id: 3,
name: 'JavaScript 权威指南',
price: 129.99,
image: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=JavaScript%20definitive%20guide%20book%20cover&image_size=square_hd',
description: 'JavaScript 领域的权威参考书籍,适合各个层次的开发者。',
category: '书籍',
stock: 120
},
{
id: 4,
name: 'TypeScript 实战',
price: 79.99,
image: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=TypeScript%20practical%20guide%20book%20cover&image_size=square_hd',
description: 'TypeScript 实战指南,从基础语法到高级应用。',
category: '书籍',
stock: 90
}
];
export const categories = [
{ id: 1, name: '全部', slug: 'all' },
{ id: 2, name: '书籍', slug: 'books' },
{ id: 3, name: '电子产品', slug: 'electronics' },
{ id: 4, name: '服装', slug: 'clothing' },
{ id: 5, name: '家居', slug: 'home' }
];
export function getProducts() {
return products;
}
export function getProductById(id) {
return products.find(product => product.id === parseInt(id));
}
export function getProductsByCategory(category) {
if (category === 'all') {
return products;
}
return products.filter(product => product.category.toLowerCase() === category);
}12.2.4 首页实现
javascript
// src/app/page.js
import Carousel from '@/components/Carousel';
import CategoryNav from '@/components/CategoryNav';
import ProductCard from '@/components/ProductCard';
import { getProducts } from '@/lib/products';
export default function Home() {
const products = getProducts();
return (
<div>
<Carousel />
<CategoryNav />
<div className="container mx-auto px-4 py-8">
<h2 className="text-2xl font-bold mb-6">热门商品</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
</div>
);
}12.2.5 轮播图组件
javascript
// src/components/Carousel.js
import { useState, useEffect } from 'react';
export default function Carousel() {
const [currentIndex, setCurrentIndex] = useState(0);
const slides = [
{
id: 1,
image: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=Ecommerce%20website%20banner%20with%20modern%20design&image_size=landscape_16_9',
title: '新品上市',
description: '探索我们的最新产品',
link: '/'
},
{
id: 2,
image: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=Book%20sale%20banner%20with%20discount&image_size=landscape_16_9',
title: '限时折扣',
description: '全场图书 8 折起',
link: '/'
},
{
id: 3,
image: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=Tech%20gadgets%20promotion%20banner&image_size=landscape_16_9',
title: '科技新品',
description: '最新科技产品等你来抢',
link: '/'
}
];
useEffect(() => {
const interval = setInterval(() => {
setCurrentIndex((prevIndex) =>
(prevIndex + 1) % slides.length
);
}, 5000);
return () => clearInterval(interval);
}, [slides.length]);
return (
<div className="relative h-80 md:h-96 overflow-hidden">
{slides.map((slide, index) => (
<div
key={slide.id}
className={`absolute inset-0 transition-opacity duration-1000 ${index === currentIndex ? 'opacity-100' : 'opacity-0'}`}
>
<img
src={slide.image}
alt={slide.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black bg-opacity-40 flex items-center">
<div className="container mx-auto px-4">
<h2 className="text-3xl md:text-4xl font-bold text-white mb-2">
{slide.title}
</h2>
<p className="text-white text-lg mb-4">
{slide.description}
</p>
<a
href={slide.link}
className="bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700"
>
立即查看
</a>
</div>
</div>
</div>
))}
<div className="absolute bottom-4 left-0 right-0 flex justify-center space-x-2">
{slides.map((_, index) => (
<button
key={index}
className={`w-3 h-3 rounded-full ${index === currentIndex ? 'bg-white' : 'bg-white bg-opacity-50'}`}
onClick={() => setCurrentIndex(index)}
/>
))}
</div>
</div>
);
}12.2.6 分类导航组件
javascript
// src/components/CategoryNav.js
import Link from 'next/link';
import { categories } from '@/lib/products';
export default function CategoryNav() {
return (
<div className="bg-gray-100 py-4">
<div className="container mx-auto px-4">
<div className="flex overflow-x-auto space-x-6">
{categories.map(category => (
<Link
key={category.id}
href={`/?category=${category.slug}`}
className="whitespace-nowrap px-4 py-2 rounded-full bg-white shadow-sm hover:bg-gray-50"
>
{category.name}
</Link>
))}
</div>
</div>
</div>
);
}12.2.7 商品卡片组件
javascript
// src/components/ProductCard.js
import Link from 'next/link';
import Image from 'next/image';
export default function ProductCard({ product }) {
return (
<div className="bg-white rounded-lg shadow-sm overflow-hidden hover:shadow-md transition-shadow">
<div className="relative h-48">
<Image
src={product.image}
alt={product.name}
fill
className="object-cover"
priority={false}
/>
</div>
<div className="p-4">
<h3 className="font-semibold mb-2 line-clamp-2">
{product.name}
</h3>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
{product.description}
</p>
<div className="flex justify-between items-center">
<span className="text-lg font-bold text-blue-600">
${product.price.toFixed(2)}
</span>
<Link
href={`/product/${product.id}`}
className="bg-gray-100 hover:bg-gray-200 px-3 py-1 rounded-md text-sm"
>
查看详情
</Link>
</div>
</div>
</div>
);
}12.2.8 商品详情页
javascript
// src/app/product/[id]/page.js
'use client';
import { useState } from 'react';
import Image from 'next/image';
import useCartStore from '@/lib/store';
import { getProductById } from '@/lib/products';
export default function ProductDetailPage({ params }) {
const { id } = params;
const product = getProductById(id);
const [quantity, setQuantity] = useState(1);
const addToCart = useCartStore(state => state.addToCart);
if (!product) {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-2xl font-bold mb-4">商品未找到</h1>
<p>抱歉,您请求的商品不存在。</p>
</div>
);
}
const handleAddToCart = () => {
addToCart(product, quantity);
alert('商品已添加到购物车');
};
return (
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="relative h-96">
<Image
src={product.image}
alt={product.name}
fill
className="object-cover rounded-lg"
priority
/>
</div>
<div>
<h1 className="text-3xl font-bold mb-4">{product.name}</h1>
<p className="text-gray-600 mb-6">{product.description}</p>
<div className="text-2xl font-bold text-blue-600 mb-6">
${product.price.toFixed(2)}
</div>
<div className="mb-6">
<label className="block text-sm font-medium mb-2">数量</label>
<div className="flex items-center">
<button
onClick={() => setQuantity(Math.max(1, quantity - 1))}
className="bg-gray-100 px-3 py-1 rounded-l-md"
>
-
</button>
<input
type="number"
value={quantity}
onChange={(e) => setQuantity(Math.max(1, parseInt(e.target.value) || 1))}
className="w-16 px-3 py-1 border-y border-gray-300 text-center"
/>
<button
onClick={() => setQuantity(quantity + 1)}
className="bg-gray-100 px-3 py-1 rounded-r-md"
>
+
</button>
</div>
</div>
<button
onClick={handleAddToCart}
className="bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 w-full"
>
加入购物车
</button>
</div>
</div>
</div>
);
}12.2.9 购物车页面
javascript
// src/app/cart/page.js
'use client';
import CartItem from '@/components/CartItem';
import useCartStore from '@/lib/store';
export default function CartPage() {
const cart = useCartStore(state => state.cart);
const getTotalPrice = useCartStore(state => state.getTotalPrice);
const clearCart = useCartStore(state => state.clearCart);
if (cart.length === 0) {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-2xl font-bold mb-4">购物车</h1>
<p>您的购物车是空的。</p>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-2xl font-bold mb-6">购物车</h1>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2">
{cart.map(item => (
<CartItem key={item.id} item={item} />
))}
</div>
<div className="bg-gray-100 p-6 rounded-lg">
<h2 className="text-xl font-bold mb-4">订单摘要</h2>
<div className="space-y-2 mb-6">
<div className="flex justify-between">
<span>商品总价</span>
<span>${getTotalPrice().toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span>运费</span>
<span>免费</span>
</div>
<div className="border-t pt-2 flex justify-between font-bold">
<span>总计</span>
<span>${getTotalPrice().toFixed(2)}</span>
</div>
</div>
<button
className="bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 w-full mb-4"
>
结算
</button>
<button
onClick={clearCart}
className="bg-gray-200 text-gray-700 px-6 py-2 rounded-md hover:bg-gray-300 w-full"
>
清空购物车
</button>
</div>
</div>
</div>
);
}12.2.10 购物车商品项组件
javascript
// src/components/CartItem.js
import Image from 'next/image';
import useCartStore from '@/lib/store';
export default function CartItem({ item }) {
const removeFromCart = useCartStore(state => state.removeFromCart);
const updateQuantity = useCartStore(state => state.updateQuantity);
return (
<div className="bg-white rounded-lg shadow-sm p-4 mb-4 flex flex-col sm:flex-row gap-4">
<div className="relative w-24 h-24 sm:w-32 sm:h-32">
<Image
src={item.image}
alt={item.name}
fill
className="object-cover rounded-md"
/>
</div>
<div className="flex-1">
<h3 className="font-semibold mb-2">{item.name}</h3>
<p className="text-gray-600 text-sm mb-4">{item.description}</p>
<div className="flex justify-between items-center">
<div className="flex items-center">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="bg-gray-100 px-2 py-1 rounded-l-md"
>
-
</button>
<input
type="number"
value={item.quantity}
onChange={(e) => updateQuantity(item.id, parseInt(e.target.value) || 1)}
className="w-12 px-2 py-1 border-y border-gray-300 text-center"
/>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="bg-gray-100 px-2 py-1 rounded-r-md"
>
+
</button>
</div>
<div className="text-right">
<div className="font-bold">${(item.price * item.quantity).toFixed(2)}</div>
<button
onClick={() => removeFromCart(item.id)}
className="text-red-500 text-sm mt-1 hover:underline"
>
删除
</button>
</div>
</div>
</div>
</div>
);
}12.2.11 头部组件(包含购物车图标)
javascript
// src/components/Header.js
import Link from 'next/link';
import useCartStore from '@/lib/store';
export default function Header() {
const getTotalItems = useCartStore(state => state.getTotalItems);
const totalItems = getTotalItems();
return (
<header className="bg-white shadow-sm sticky top-0 z-10">
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
<Link href="/" className="text-2xl font-bold text-blue-600">
简易电商
</Link>
<nav className="hidden md:flex space-x-6">
<Link href="/" className="hover:text-blue-600">
首页
</Link>
<Link href="/cart" className="hover:text-blue-600">
购物车
{totalItems > 0 && (
<span className="ml-1 bg-blue-600 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{totalItems}
</span>
)}
</Link>
</nav>
<div className="md:hidden">
<Link href="/cart" className="relative">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25v-.007a60.116 60.116 0 0112.538-2.347M7.5 14.25v.191a59.905 59.905 0 01-3.374 2.443M15 12a3 3 0 11-6 0 3 3 0 016 0zm6 2.25a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
{totalItems > 0 && (
<span className="absolute -top-2 -right-2 bg-blue-600 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{totalItems}
</span>
)}
</Link>
</div>
</div>
</header>
);
}12.3 状态管理
我们使用 Zustand 来管理购物车状态,这是一个轻量级的状态管理库,比 Redux 更简单易用。Zustand 的核心特点是:
- 简洁的 API:使用 create 函数创建 store,无需繁琐的配置
- 中间件支持:内置 persist 中间件,轻松实现状态持久化
- 组件友好:支持 React 组件订阅状态变化
- 类型安全:支持 TypeScript
12.4 数据获取
12.4.1 服务端数据获取
在首页和商品详情页,我们使用 Server Components 来获取商品数据,这样可以:
- 减少客户端 JavaScript 体积
- 提高首屏加载速度
- 改善 SEO
12.4.2 客户端数据获取
在购物车页面,我们使用客户端组件来获取购物车数据,因为:
- 购物车状态需要在客户端实时更新
- 需要与用户交互(添加、删除、修改数量)
- 使用 Zustand 进行状态管理
12.5 性能优化
12.5.1 图片优化
使用 Next.js 的 Image 组件优化图片加载:
- 自动生成不同尺寸的图片
- 支持懒加载
- 提高页面加载速度
12.5.2 组件懒加载
对于大型组件,可以使用 React.lazy 和 Suspense 进行懒加载:
javascript
// 示例:懒加载购物车组件
import { lazy, Suspense } from 'react';
const Cart = lazy(() => import('./Cart'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Cart />
</Suspense>
);
}12.5.3 缓存策略
使用 Next.js 的缓存策略优化数据获取:
javascript
// 示例:缓存商品数据
export async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: {
revalidate: 3600, // 1小时重新验证
},
});
return res.json();
}12.5.4 代码分割
Next.js 自动进行代码分割,将不同页面的代码分离,减少初始加载时间。
12.6 项目总结
通过本实战项目的学习,你已经掌握了以下企业级应用开发的核心技能:
- 复杂状态管理:使用 Zustand 管理购物车状态
- 多页面应用:使用 App Router 创建多个页面
- 服务端渲染:使用 Server Components 优化首屏加载
- 客户端交互:使用客户端组件处理用户交互
- 性能优化:图片优化、组件懒加载、缓存策略
- 响应式设计:适配不同设备屏幕
这个简易电商首页虽然功能简单,但已经包含了企业级应用的核心要素。在实际开发中,你可以根据具体需求对这个项目进行扩展,例如:
- 添加用户认证系统
- 集成支付功能
- 实现订单管理
- 添加商品搜索和筛选
- 集成推荐系统
通过不断学习和实践,你可以逐步构建更加复杂和功能完整的电商应用。
