Appearance
Server Components数据获取
学习目标
通过本章学习,你将掌握:
- Server Components中的数据获取方式
- 直接数据库访问
- 并行和串行数据获取
- 数据缓存策略
- 错误处理机制
- 性能优化技巧
- 与传统方式的对比
- 实战最佳实践
第一部分:基础数据获取
1.1 async/await模式
jsx
// Server Component可以直接async
async function BlogPost({ id }) {
// 直接await数据获取
const post = await fetchPost(id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// 无需:
// ❌ useState
// ❌ useEffect
// ❌ loading状态
// ❌ error状态1.2 数据库直接访问
jsx
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// 直接查询数据库
async function UserList() {
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
_count: {
select: { posts: true }
}
},
orderBy: {
createdAt: 'desc'
},
take: 20
});
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {user._count.posts} posts
</li>
))}
</ul>
);
}
// Prisma客户端代码不会发送到客户端!
// 数据库连接字符串安全1.3 API调用
jsx
// Server Component中的API调用
async function WeatherWidget({ city }) {
const response = await fetch(
`https://api.weather.com/v1/current?city=${city}`,
{
headers: {
'Authorization': `Bearer ${process.env.WEATHER_API_KEY}`
}
}
);
if (!response.ok) {
throw new Error('Failed to fetch weather');
}
const weather = await response.json();
return (
<div className="weather">
<h3>{weather.city}</h3>
<p>{weather.temperature}°C</p>
<p>{weather.description}</p>
</div>
);
}
// API密钥安全地保存在服务器环境变量中第二部分:并行数据获取
2.1 Promise.all并行
jsx
// 并行获取多个独立数据源
async function Dashboard({ userId }) {
// 同时启动所有请求
const [user, posts, stats, notifications] = await Promise.all([
fetchUser(userId),
fetchUserPosts(userId),
fetchUserStats(userId),
fetchNotifications(userId)
]);
return (
<div className="dashboard">
<UserHeader user={user} />
<StatsPanel stats={stats} />
<PostsList posts={posts} />
<NotificationsList notifications={notifications} />
</div>
);
}
// 对比传统方式:需要多个useEffect或复杂的Promise管理2.2 嵌套Suspense并行
jsx
// 父组件
async function Page({ userId }) {
// 立即创建所有Promise(开始并行加载)
return (
<div>
{/* 用户信息 */}
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId={userId} />
</Suspense>
{/* 文章列表 */}
<Suspense fallback={<PostsSkeleton />}>
<UserPosts userId={userId} />
</Suspense>
{/* 统计数据 */}
<Suspense fallback={<StatsSkeleton />}>
<UserStats userId={userId} />
</Suspense>
</div>
);
}
// 子组件独立获取数据
async function UserProfile({ userId }) {
const user = await fetchUser(userId);
return <div>{user.name}</div>;
}
async function UserPosts({ userId }) {
const posts = await fetchPosts(userId);
return <PostsList posts={posts} />;
}
async function UserStats({ userId }) {
const stats = await fetchStats(userId);
return <Stats data={stats} />;
}
// 三个请求并行执行,各自独立显示2.3 条件并行
jsx
async function ProductPage({ productId, includeReviews = false }) {
// 基础数据总是获取
const productPromise = fetchProduct(productId);
// 条件性数据获取
const promises = [productPromise];
if (includeReviews) {
promises.push(fetchReviews(productId));
}
const results = await Promise.all(promises);
const product = results[0];
const reviews = results[1] || null;
return (
<div>
<ProductDetail product={product} />
{reviews && <Reviews data={reviews} />}
</div>
);
}第三部分:串行数据获取
3.1 依赖数据获取
jsx
// 第二个请求依赖第一个请求的结果
async function UserWithRecentPosts({ username }) {
// 1. 先获取用户信息
const user = await fetchUserByUsername(username);
// 2. 使用用户ID获取文章
const posts = await fetchPostsByUserId(user.id);
// 3. 获取每篇文章的评论统计
const postsWithCommentCount = await Promise.all(
posts.map(async (post) => ({
...post,
commentCount: await fetchCommentCount(post.id)
}))
);
return (
<div>
<h1>{user.name}的文章</h1>
<ul>
{postsWithCommentCount.map(post => (
<li key={post.id}>
{post.title} - {post.commentCount} 评论
</li>
))}
</ul>
</div>
);
}3.2 瀑布流优化
jsx
// ❌ 问题:瀑布流(串行)
async function SlowPage({ userId }) {
const user = await fetchUser(userId);
// 等待user完成
const posts = await fetchPosts(user.id);
// 等待posts完成
const comments = await fetchComments(posts[0].id);
// 等待comments完成
return <div>...</div>;
}
// ✅ 优化:提前启动请求
async function FastPage({ userId }) {
// 立即启动第一个请求
const userPromise = fetchUser(userId);
// 可以在外部组件中启动更多请求
return (
<Suspense fallback={<Loading />}>
<UserContent userPromise={userPromise} userId={userId} />
</Suspense>
);
}
async function UserContent({ userPromise, userId }) {
const user = await userPromise;
// 立即启动下一个请求
const postsPromise = fetchPosts(user.id);
return (
<div>
<h1>{user.name}</h1>
<Suspense fallback={<PostsLoading />}>
<PostsContent postsPromise={postsPromise} />
</Suspense>
</div>
);
}3.3 组件级数据预取
jsx
// layout.jsx - 提前启动数据获取
export default async function Layout({ params, children }) {
// 在布局层启动数据获取
const userPromise = fetchUser(params.userId);
return (
<div>
<Suspense fallback={<NavSkeleton />}>
<Nav userPromise={userPromise} />
</Suspense>
<main>
{children}
</main>
</div>
);
}
// page.jsx - 可以复用父级的Promise
export default async function Page({ params }) {
// 如果layout已经获取,这里会使用缓存
const user = await fetchUser(params.userId);
return <div>{user.name}</div>;
}第四部分:数据缓存
4.1 fetch缓存
jsx
// Next.js 自动缓存fetch请求
async function CachedComponent() {
// 默认缓存
const data = await fetch('https://api.example.com/data');
// 自定义缓存选项
const freshData = await fetch('https://api.example.com/data', {
cache: 'no-store' // 不缓存
});
const revalidatedData = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 1小时后重新验证
});
return <div>...</div>;
}4.2 React Cache API
jsx
import { cache } from 'react';
// 创建缓存的数据获取函数
const getUser = cache(async (userId) => {
console.log('Fetching user:', userId);
const response = await fetch(`/api/users/${userId}`);
return response.json();
});
// 多个组件调用相同参数
async function Header({ userId }) {
const user = await getUser(userId); // 发起请求
return <div>{user.name}</div>;
}
async function Sidebar({ userId }) {
const user = await getUser(userId); // 使用缓存
return <div>{user.email}</div>;
}
async function Main({ userId }) {
const user = await getUser(userId); // 使用缓存
return <div>{user.bio}</div>;
}
// 相同userId只会请求一次!4.3 自定义缓存层
jsx
// 创建内存缓存
class DataCache {
constructor() {
this.cache = new Map();
}
async get(key, fetcher, ttl = 5 * 60 * 1000) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
console.log('Cache hit:', key);
return cached.data;
}
console.log('Cache miss:', key);
const data = await fetcher();
this.cache.set(key, {
data,
timestamp: Date.now()
});
return data;
}
invalidate(key) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
}
const dataCache = new DataCache();
// 使用缓存
async function CachedData({ userId }) {
const user = await dataCache.get(
`user:${userId}`,
() => fetchUser(userId),
10 * 60 * 1000 // 10分钟TTL
);
return <div>{user.name}</div>;
}第五部分:错误处理
5.1 try-catch
jsx
async function SafeComponent({ userId }) {
try {
const user = await fetchUser(userId);
return (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
} catch (error) {
console.error('Error fetching user:', error);
// 返回错误UI
return (
<div className="error">
<h2>加载失败</h2>
<p>{error.message}</p>
</div>
);
}
}5.2 ErrorBoundary配合
jsx
// 抛出错误让ErrorBoundary处理
async function UserProfile({ userId }) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// 抛出错误
throw new Error(`Failed to load user: ${response.status}`);
}
const user = await response.json();
return <div>{user.name}</div>;
}
// 使用ErrorBoundary
function Page() {
return (
<ErrorBoundary fallback={<ErrorUI />}>
<Suspense fallback={<Loading />}>
<UserProfile userId={123} />
</Suspense>
</ErrorBoundary>
);
}5.3 降级处理
jsx
async function ProductWithFallback({ productId }) {
try {
// 尝试获取完整产品信息
const product = await fetchFullProduct(productId);
return <FullProductView product={product} />;
} catch (error) {
console.warn('Failed to load full product, using basic info');
// 降级到基础信息
try {
const basicProduct = await fetchBasicProduct(productId);
return <BasicProductView product={basicProduct} />;
} catch (fallbackError) {
// 最后的降级
return (
<div className="error">
<h3>商品暂时无法显示</h3>
<p>请稍后再试</p>
</div>
);
}
}
}第六部分:性能优化
6.1 查询优化
jsx
import { prisma } from '@/lib/database';
async function OptimizedUserList() {
// ✅ 只查询需要的字段
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
// 不查询不需要的字段(如password, token等)
},
// ✅ 使用索引字段排序
orderBy: {
createdAt: 'desc'
},
// ✅ 限制返回数量
take: 50
});
return <UserList users={users} />;
}6.2 预加载关联数据
jsx
async function PostsWithAuthors() {
// ✅ 使用include预加载关联数据
const posts = await prisma.post.findMany({
include: {
author: {
select: {
id: true,
name: true,
avatar: true
}
},
_count: {
select: {
comments: true
}
}
}
});
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>作者: {post.author.name}</p>
<p>评论: {post._count.comments}</p>
</li>
))}
</ul>
);
}
// 避免N+1查询问题6.3 数据聚合
jsx
async function DashboardStats() {
// ✅ 在数据库层面聚合
const stats = await prisma.$queryRaw`
SELECT
COUNT(*) as total_users,
COUNT(CASE WHEN created_at > NOW() - INTERVAL '7 days' THEN 1 END) as new_users,
AVG(post_count) as avg_posts_per_user
FROM users
`;
return (
<div className="stats">
<StatCard title="总用户" value={stats[0].total_users} />
<StatCard title="新用户(7天)" value={stats[0].new_users} />
<StatCard title="平均文章数" value={stats[0].avg_posts_per_user} />
</div>
);
}
// 在服务器聚合,不传输原始数据6.4 流式渲染
jsx
// 使用Suspense实现流式渲染
async function ProductPage({ id }) {
return (
<div>
{/* 快速显示的部分 */}
<Suspense fallback={<HeaderSkeleton />}>
<ProductHeader productId={id} />
</Suspense>
{/* 慢速显示的部分 */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={id} />
</Suspense>
{/* 最慢的部分 */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations productId={id} />
</Suspense>
</div>
);
}
// 渐进式显示,无需等待全部完成第七部分:实战模式
7.1 分页数据
jsx
async function PostsPage({ searchParams }) {
const page = Number(searchParams.page) || 1;
const limit = 20;
const offset = (page - 1) * limit;
const [posts, total] = await Promise.all([
prisma.post.findMany({
skip: offset,
take: limit,
orderBy: { createdAt: 'desc' }
}),
prisma.post.count()
]);
const totalPages = Math.ceil(total / limit);
return (
<div>
<PostsList posts={posts} />
<Pagination
currentPage={page}
totalPages={totalPages}
/>
</div>
);
}7.2 搜索功能
jsx
async function SearchResults({ searchParams }) {
const query = searchParams.q || '';
if (!query) {
return <EmptySearch />;
}
const results = await prisma.post.findMany({
where: {
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ content: { contains: query, mode: 'insensitive' } }
]
},
include: {
author: {
select: { name: true }
}
},
take: 50
});
return (
<div>
<h2>找到 {results.length} 个结果</h2>
<ResultsList results={results} />
</div>
);
}7.3 实时数据
jsx
async function LiveStats() {
// 每次请求都获取最新数据
const stats = await fetch('https://api.example.com/stats', {
cache: 'no-store' // 不缓存
}).then(r => r.json());
return (
<div>
<StatCard title="在线用户" value={stats.onlineUsers} />
<StatCard title="活跃会话" value={stats.activeSessions} />
<time>更新时间: {new Date().toLocaleTimeString()}</time>
</div>
);
}注意事项
1. 避免过度串行
jsx
// ❌ 不好:串行获取
async function Slow() {
const a = await fetchA();
const b = await fetchB(); // 等待a
const c = await fetchC(); // 等待a和b
return <div>{a + b + c}</div>;
}
// ✅ 好:并行获取
async function Fast() {
const [a, b, c] = await Promise.all([
fetchA(),
fetchB(),
fetchC()
]);
return <div>{a + b + c}</div>;
}2. 合理使用缓存
jsx
// ✅ 根据数据特性设置缓存
// 静态数据:长期缓存
const staticData = await fetch('/api/config', {
next: { revalidate: 86400 } // 24小时
});
// 动态数据:短期缓存
const dynamicData = await fetch('/api/stats', {
next: { revalidate: 60 } // 1分钟
});
// 实时数据:不缓存
const liveData = await fetch('/api/live', {
cache: 'no-store'
});3. 错误处理全面
jsx
// ✅ 完整的错误处理
async function RobustComponent({ id }) {
try {
const data = await fetchData(id);
if (!data) {
return <NotFound />;
}
return <DataView data={data} />;
} catch (error) {
if (error.status === 404) {
return <NotFound />;
}
if (error.status === 403) {
return <Forbidden />;
}
throw error; // 其他错误向上抛出
}
}常见问题
Q1: Server Component中能使用useState吗?
A: 不能。Server Component无状态,不能使用任何Hooks。
Q2: 如何处理用户交互触发的数据获取?
A: 使用Server Actions或在Client Component中获取。
Q3: 数据获取失败会怎样?
A: 会抛出错误,需要ErrorBoundary捕获。
总结
Server Components数据获取优势
✅ 直接async/await
✅ 访问服务器资源
✅ 并行请求优化
✅ 自动缓存
✅ 流式渲染
✅ 更少的客户端代码
✅ 更好的性能最佳实践
1. 优先使用并行请求
2. 合理设置缓存策略
3. 使用Suspense流式渲染
4. 完善错误处理
5. 优化数据库查询
6. 避免过度获取数据
7. 利用React Cache APIServer Components让数据获取变得简单而强大!