Appearance
Server Components实战案例
学习目标
通过本章学习,你将掌握:
- 完整的Server Components应用架构
- 博客系统实现
- 电商平台实现
- 社交媒体实现
- Dashboard应用实现
- 实时数据展示
- 复杂表单处理
- 文件上传管理
- 性能优化实践
第一部分:博客系统
1.1 文章列表页
jsx
// app/posts/page.jsx
import { Suspense } from 'react';
import { db } from '@/lib/database';
import PostCard from './PostCard';
import Pagination from './Pagination';
import PostsSkeleton from './PostsSkeleton';
// Server Component - 获取文章列表
async function getposts(page = 1, limit = 10) {
const skip = (page - 1) * limit;
const [posts, total] = await Promise.all([
db.posts.findMany({
skip,
take: limit,
include: {
author: {
select: {
id: true,
name: true,
avatar: true
}
},
_count: {
select: {
comments: true,
likes: true
}
}
},
orderBy: {
createdAt: 'desc'
}
}),
db.posts.count()
]);
return { posts, total, totalPages: Math.ceil(total / limit) };
}
// Server Component - 文章列表页
export default async function PostsPage({ searchParams }) {
const page = Number(searchParams.page) || 1;
const { posts, totalPages } = await getPosts(page);
return (
<div className="posts-page">
<header>
<h1>博客文章</h1>
<CreatePostButton />
</header>
<Suspense fallback={<PostsSkeleton />}>
<div className="posts-grid">
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
</Suspense>
<Pagination
currentPage={page}
totalPages={totalPages}
/>
</div>
);
}
// Server Component - 文章卡片
function PostCard({ post }) {
return (
<article className="post-card">
<Link href={`/posts/${post.id}`}>
{post.coverImage && (
<img src={post.coverImage} alt={post.title} />
)}
<h2>{post.title}</h2>
<p className="excerpt">{post.excerpt}</p>
<div className="meta">
<img
src={post.author.avatar}
alt={post.author.name}
className="avatar"
/>
<span>{post.author.name}</span>
<time>{formatDate(post.createdAt)}</time>
</div>
<div className="stats">
<span>❤️ {post._count.likes}</span>
<span>💬 {post._count.comments}</span>
</div>
</Link>
</article>
);
}
// Client Component - 创建文章按钮
'use client';
function CreatePostButton() {
const router = useRouter();
return (
<button onClick={() => router.push('/posts/new')}>
✏️ 写文章
</button>
);
}1.2 文章详情页
jsx
// app/posts/[id]/page.jsx
import { Suspense } from 'react';
import { notFound } from 'next/navigation';
import { db } from '@/lib/database';
import LikeButton from './LikeButton';
import CommentSection from './CommentSection';
import RelatedPosts from './RelatedPosts';
// 生成静态路径
export async function generateStaticParams() {
const posts = await db.posts.findMany({
select: { id: true }
});
return posts.map(post => ({
id: post.id
}));
}
// 生成元数据
export async function generateMetadata({ params }) {
const post = await db.posts.findUnique({
where: { id: params.id }
});
if (!post) return {};
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage]
}
};
}
// 获取文章详情
async function getPost(id) {
const post = await db.posts.findUnique({
where: { id },
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
bio: true
}
},
_count: {
select: {
comments: true,
likes: true
}
}
}
});
if (!post) notFound();
return post;
}
// Server Component - 文章详情页
export default async function PostPage({ params }) {
const post = await getPost(params.id);
return (
<div className="post-page">
<article className="post-content">
<header>
<h1>{post.title}</h1>
<div className="author-info">
<img src={post.author.avatar} alt={post.author.name} />
<div>
<div className="name">{post.author.name}</div>
<time>{formatDate(post.createdAt)}</time>
</div>
</div>
</header>
{post.coverImage && (
<img
src={post.coverImage}
alt={post.title}
className="cover-image"
/>
)}
<div
className="content"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
<footer>
<div className="tags">
{post.tags.map(tag => (
<Link key={tag} href={`/tags/${tag}`}>
#{tag}
</Link>
))}
</div>
<LikeButton
postId={post.id}
initialLikes={post._count.likes}
/>
</footer>
</article>
{/* 评论区 */}
<Suspense fallback={<CommentsSkeleton />}>
<CommentSection postId={post.id} />
</Suspense>
{/* 相关文章 */}
<Suspense fallback={<RelatedSkeleton />}>
<RelatedPosts
postId={post.id}
tags={post.tags}
/>
</Suspense>
</div>
);
}
// Server Component - 评论区
async function CommentSection({ postId }) {
const comments = await db.comments.findMany({
where: { postId },
include: {
author: {
select: {
id: true,
name: true,
avatar: true
}
}
},
orderBy: {
createdAt: 'desc'
}
});
return (
<div className="comments-section">
<h2>评论 ({comments.length})</h2>
<CommentForm postId={postId} />
<div className="comments-list">
{comments.map(comment => (
<CommentItem key={comment.id} comment={comment} />
))}
</div>
</div>
);
}
// Client Component - 点赞按钮
'use client';
import { likePost } from './actions';
function LikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [liked, setLiked] = useState(false);
const [pending, setPending] = useState(false);
const handleLike = async () => {
if (pending) return;
setPending(true);
setLiked(true);
setLikes(prev => prev + 1);
try {
await likePost(postId);
} catch (error) {
setLiked(false);
setLikes(prev => prev - 1);
alert('点赞失败');
} finally {
setPending(false);
}
};
return (
<button
className={`like-button ${liked ? 'liked' : ''}`}
onClick={handleLike}
disabled={pending}
>
{liked ? '❤️' : '🤍'} {likes}
</button>
);
}1.3 文章创建/编辑
jsx
// app/posts/new/page.jsx
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/database';
import PostForm from './PostForm';
// Server Action - 创建文章
async function createPost(formData) {
'use server';
const session = await getSession();
if (!session) {
throw new Error('未登录');
}
const title = formData.get('title');
const content = formData.get('content');
const excerpt = formData.get('excerpt');
const tags = formData.get('tags')?.split(',').map(t => t.trim()) || [];
// 验证
if (!title || title.length < 3) {
return { error: '标题至少3个字符' };
}
if (!content || content.length < 100) {
return { error: '内容至少100个字符' };
}
// 创建文章
const post = await db.posts.create({
data: {
title,
content,
excerpt,
tags,
authorId: session.userId
}
});
revalidatePath('/posts');
redirect(`/posts/${post.id}`);
}
// Server Component
export default async function NewPostPage() {
const session = await getSession();
if (!session) {
redirect('/login');
}
return (
<div className="new-post-page">
<h1>写文章</h1>
<PostForm action={createPost} />
</div>
);
}
// Client Component - 文章表单
'use client';
function PostForm({ action, initialData = {} }) {
const [formData, setFormData] = useState(initialData);
const [errors, setErrors] = useState({});
const [pending, setPending] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
// 客户端验证
const newErrors = {};
if (!formData.title || formData.title.length < 3) {
newErrors.title = '标题至少3个字符';
}
if (!formData.content || formData.content.length < 100) {
newErrors.content = '内容至少100个字符';
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setPending(true);
try {
const formData = new FormData(e.target);
const result = await action(formData);
if (result?.error) {
setErrors({ submit: result.error });
}
} catch (error) {
setErrors({ submit: '提交失败,请重试' });
} finally {
setPending(false);
}
};
return (
<form onSubmit={handleSubmit} className="post-form">
<div className="form-group">
<label>标题</label>
<input
name="title"
value={formData.title || ''}
onChange={(e) => setFormData({...formData, title: e.target.value})}
placeholder="输入文章标题..."
/>
{errors.title && <span className="error">{errors.title}</span>}
</div>
<div className="form-group">
<label>摘要</label>
<textarea
name="excerpt"
value={formData.excerpt || ''}
onChange={(e) => setFormData({...formData, excerpt: e.target.value})}
placeholder="简短描述..."
rows={3}
/>
</div>
<div className="form-group">
<label>内容</label>
<MarkdownEditor
name="content"
value={formData.content || ''}
onChange={(value) => setFormData({...formData, content: value})}
/>
{errors.content && <span className="error">{errors.content}</span>}
</div>
<div className="form-group">
<label>标签</label>
<input
name="tags"
value={formData.tags || ''}
onChange={(e) => setFormData({...formData, tags: e.target.value})}
placeholder="用逗号分隔,如: React, TypeScript"
/>
</div>
{errors.submit && (
<div className="error">{errors.submit}</div>
)}
<button type="submit" disabled={pending}>
{pending ? '发布中...' : '发布文章'}
</button>
</form>
);
}第二部分:电商平台
2.1 商品列表页
jsx
// app/products/page.jsx
import { db } from '@/lib/database';
import ProductCard from './ProductCard';
import FilterSidebar from './FilterSidebar';
import SortSelect from './SortSelect';
// Server Component - 获取商品
async function getProducts(filters = {}) {
const { category, minPrice, maxPrice, sortBy = 'newest' } = filters;
const where = {};
if (category) {
where.categoryId = category;
}
if (minPrice !== undefined || maxPrice !== undefined) {
where.price = {};
if (minPrice) where.price.gte = Number(minPrice);
if (maxPrice) where.price.lte = Number(maxPrice);
}
const orderBy = {
newest: { createdAt: 'desc' },
price_asc: { price: 'asc' },
price_desc: { price: 'desc' },
popular: { salesCount: 'desc' }
}[sortBy] || { createdAt: 'desc' };
const products = await db.products.findMany({
where,
orderBy,
include: {
category: true,
_count: {
select: {
reviews: true
}
}
}
});
return products;
}
// Server Component - 商品列表页
export default async function ProductsPage({ searchParams }) {
const filters = {
category: searchParams.category,
minPrice: searchParams.minPrice,
maxPrice: searchParams.maxPrice,
sortBy: searchParams.sortBy
};
const products = await getProducts(filters);
const categories = await db.categories.findMany();
return (
<div className="products-page">
<FilterSidebar
categories={categories}
currentFilters={filters}
/>
<main>
<div className="toolbar">
<h1>商品列表</h1>
<SortSelect currentSort={filters.sortBy} />
</div>
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</main>
</div>
);
}
// Server Component - 商品卡片
function ProductCard({ product }) {
const discount = product.originalPrice
? Math.round((1 - product.price / product.originalPrice) * 100)
: 0;
return (
<article className="product-card">
<Link href={`/products/${product.id}`}>
<div className="image-wrapper">
<img src={product.images[0]} alt={product.name} />
{discount > 0 && (
<span className="discount-badge">-{discount}%</span>
)}
</div>
<h3>{product.name}</h3>
<div className="price">
<span className="current">¥{product.price}</span>
{product.originalPrice && (
<span className="original">¥{product.originalPrice}</span>
)}
</div>
<div className="rating">
<Stars rating={product.averageRating} />
<span>({product._count.reviews})</span>
</div>
</Link>
<AddToCartButton
productId={product.id}
productName={product.name}
price={product.price}
/>
</article>
);
}2.2 商品详情页
jsx
// app/products/[id]/page.jsx
import { notFound } from 'next/navigation';
import { db } from '@/lib/database';
import ImageGallery from './ImageGallery';
import AddToCartForm from './AddToCartForm';
import ProductTabs from './ProductTabs';
import Reviews from './Reviews';
import Recommendations from './Recommendations';
// 获取商品详情
async function getProduct(id) {
const product = await db.products.findUnique({
where: { id },
include: {
category: true,
specifications: true,
_count: {
select: {
reviews: true
}
}
}
});
if (!product) notFound();
return product;
}
// Server Component - 商品详情页
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
<div className="product-page">
<div className="product-detail">
<ImageGallery images={product.images} />
<div className="product-info">
<h1>{product.name}</h1>
<div className="rating-row">
<Stars rating={product.averageRating} />
<span>{product._count.reviews} 评价</span>
</div>
<div className="price-row">
<span className="price">¥{product.price}</span>
{product.originalPrice && (
<span className="original">¥{product.originalPrice}</span>
)}
</div>
<div className="description">
{product.description}
</div>
<AddToCartForm
product={product}
addToCartAction={addToCart}
/>
<div className="specs">
<h3>商品参数</h3>
<dl>
{product.specifications.map(spec => (
<React.Fragment key={spec.key}>
<dt>{spec.key}</dt>
<dd>{spec.value}</dd>
</React.Fragment>
))}
</dl>
</div>
</div>
</div>
<ProductTabs
description={product.detailedDescription}
specs={product.specifications}
/>
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={product.id} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations
productId={product.id}
categoryId={product.categoryId}
/>
</Suspense>
</div>
);
}
// Server Action - 添加到购物车
async function addToCart(formData) {
'use server';
const session = await getSession();
if (!session) {
redirect('/login');
}
const productId = formData.get('productId');
const quantity = Number(formData.get('quantity'));
// 检查库存
const product = await db.products.findUnique({
where: { id: productId }
});
if (!product || product.stock < quantity) {
return { error: '库存不足' };
}
// 添加到购物车
await db.cartItems.upsert({
where: {
userId_productId: {
userId: session.userId,
productId
}
},
create: {
userId: session.userId,
productId,
quantity
},
update: {
quantity: {
increment: quantity
}
}
});
revalidatePath('/cart');
return { success: true };
}
// Client Component - 添加到购物车表单
'use client';
function AddToCartForm({ product, addToCartAction }) {
const [quantity, setQuantity] = useState(1);
const [pending, setPending] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setPending(true);
try {
const formData = new FormData();
formData.set('productId', product.id);
formData.set('quantity', quantity.toString());
const result = await addToCartAction(formData);
if (result.success) {
alert('已添加到购物车');
} else {
alert(result.error);
}
} catch (error) {
alert('添加失败');
} finally {
setPending(false);
}
};
return (
<form onSubmit={handleSubmit} className="add-to-cart-form">
<div className="quantity-selector">
<button
type="button"
onClick={() => setQuantity(Math.max(1, quantity - 1))}
>
-
</button>
<span>{quantity}</span>
<button
type="button"
onClick={() => setQuantity(Math.min(product.stock, quantity + 1))}
>
+
</button>
</div>
<button
type="submit"
disabled={pending || product.stock === 0}
>
{pending ? '添加中...' : product.stock === 0 ? '已售罄' : '加入购物车'}
</button>
<span className="stock-info">
库存: {product.stock} 件
</span>
</form>
);
}第三部分:用户Dashboard
3.1 Dashboard主页
jsx
// app/dashboard/page.jsx
import { Suspense } from 'react';
import { redirect } from 'next/navigation';
import { db } from '@/lib/database';
import StatsCards from './StatsCards';
import RecentActivity from './RecentActivity';
import Charts from './Charts';
// Server Component - Dashboard
export default async function DashboardPage() {
const session = await getSession();
if (!session) {
redirect('/login');
}
return (
<div className="dashboard-page">
<h1>欢迎回来, {session.user.name}</h1>
<Suspense fallback={<StatsCardsSkeleton />}>
<StatsCards userId={session.userId} />
</Suspense>
<div className="dashboard-grid">
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity userId={session.userId} />
</Suspense>
<Suspense fallback={<ChartsSkeleton />}>
<Charts userId={session.userId} />
</Suspense>
</div>
</div>
);
}
// Server Component - 统计卡片
async function StatsCards({ userId }) {
const stats = await db.$transaction([
db.posts.count({ where: { authorId: userId } }),
db.comments.count({ where: { authorId: userId } }),
db.likes.count({ where: { post: { authorId: userId } } }),
db.followers.count({ where: { followingId: userId } })
]);
return (
<div className="stats-cards">
<StatCard title="文章" value={stats[0]} icon="📝" />
<StatCard title="评论" value={stats[1]} icon="💬" />
<StatCard title="点赞" value={stats[2]} icon="❤️" />
<StatCard title="粉丝" value={stats[3]} icon="👥" />
</div>
);
}
// Server Component - 最近活动
async function RecentActivity({ userId }) {
const activities = await db.activities.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
take: 10,
include: {
post: {
select: {
id: true,
title: true
}
}
}
});
return (
<div className="recent-activity">
<h2>最近活动</h2>
<ul>
{activities.map(activity => (
<li key={activity.id}>
<ActivityItem activity={activity} />
</li>
))}
</ul>
</div>
);
}注意事项
1. 始终验证权限
jsx
// ✅ 每个Server Action都要验证权限
'use server';
export async function deletePost(postId) {
const session = await getSession();
if (!session) {
throw new Error('未登录');
}
const post = await db.posts.findUnique({
where: { id: postId }
});
if (post.authorId !== session.userId) {
throw new Error('无权限');
}
await db.posts.delete({
where: { id: postId }
});
}2. 优化数据查询
jsx
// ✅ 使用include预加载,避免N+1查询
const posts = await db.posts.findMany({
include: {
author: true,
_count: {
select: {
comments: true,
likes: true
}
}
}
});3. 合理设置缓存
jsx
// ✅ 根据数据特性设置缓存
// 静态数据
export const revalidate = 3600; // 1小时
// 动态数据
export const revalidate = 0; // 不缓存
// 按需重新验证
revalidatePath('/posts');常见问题
Q1: 如何处理大量并发请求?
A: 使用数据库连接池、缓存层、CDN和负载均衡。
Q2: Server Components会增加服务器成本吗?
A: 会增加服务器负载,但通过缓存可以大大减少。整体上仍比纯客户端渲染更高效。
Q3: 如何优化大型列表性能?
A: 使用分页、虚拟滚动和增量加载。
总结
Server Components实战要点
✅ 合理组织组件结构
✅ Server Component获取数据
✅ Client Component处理交互
✅ Server Actions处理表单
✅ 优化数据库查询
✅ 实现错误处理
✅ 添加Loading状态
✅ 验证权限和输入性能优化
✅ 并行数据获取
✅ 预加载关联数据
✅ 合理使用缓存
✅ 流式渲染
✅ 按需加载组件
✅ 优化图片资源
✅ 使用CDN通过这些实战案例,你应该能够构建完整的Server Components应用了!