服务端组件中的数据获取
-
核心思想: 在服务端组件(RSC)中,可以直接使用
async/await
语法来获取数据。组件本身可以是一个异步函数。Next.js 扩展了原生的fetch
API,使其能自动处理请求的缓存和重新验证。 -
与旧模式的对比:
- 旧模式(客户端
useEffect
): 页面 HTML 先加载 -> 页面 JS 再加载 ->useEffect
触发 ->fetch
开始 -> 渲染数据。这个过程会产生加载瀑布(request waterfall),用户会先看到一个 loading 状态。 - 新模式(RSC
async/await
): 在服务端,数据获取和页面渲染可以并行发生。最终直接生成包含数据的完整 HTML 发送给浏览器。用户能更快地看到最终内容,对 SEO 也更友好。
- 旧模式(客户端
-
代码示例: 一个典型的 RSC 数据获取页面。
// app/posts/page.tsx (这是一个服务端组件) interface Post { id: number; title: string; } // 组件函数本身就是 async export default async function PostsPage() { // 直接 await fetch 调用。就像在写后端代码一样。 const res = await fetch('https://jsonplaceholder.typicode.com/posts'); const posts: Post[] = await res.json(); return ( <div> <h1>Posts</h1> <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); }
缓存与重新验证 (Caching & Revalidation)
-
核心概念: Next.js 扩展后的
fetch
会自动缓存所有请求。默认情况下,请求结果会被永久缓存,直到你手动让它失效。 -
控制缓存行为: 可以通过
fetch
的第二个参数options
对象来精细控制。-
不缓存(动态渲染): 每次请求都重新获取数据,等同于旧的
getServerSideProps
。fetch('...', { cache: 'no-store' });
-
定时重新验证(ISR): 获取数据并缓存
n
秒。在n
秒后,如果再有新请求,会先返回旧的缓存数据,同时在后台重新请求新数据,更新缓存。// 缓存 1 小时 (3600 秒) fetch('...', { next: { revalidate: 3600 } });
-
-
按需重新验证(On-Demand Revalidation):
-
问题场景: 在 CMS(内容管理系统)里发布了一篇新文章,希望博客立即显示出来,而不是等待
revalidate
的时间。 -
解决方案:
revalidateTag
和revalidatePath
。可以在一个服务端操作(如 API 路由或 Server Action)中调用这些函数,来精确地清除指定标签或路径的缓存。 -
示例 (
revalidateTag
)-
在
fetch
时给数据打上标签:fetch('https://my-cms/posts', { next: { tags: ['posts'] } });
-
创建一个 API 路由,用于接收 CMS 的 webhook 通知:
// app/api/revalidate/route.ts import { NextRequest, NextResponse } from 'next/server'; import { revalidateTag } from 'next/cache'; export async function POST(request: NextRequest) { const tag = request.nextUrl.searchParams.get('tag'); if (!tag) { return NextResponse.json({ error: 'Tag is required' }, { status: 400 }); } revalidateTag(tag); // 清除所有带 'posts' 标签的缓存 return NextResponse.json({ revalidated: true, now: Date.now() }); }
现在,只要请求
POST /api/revalidate?tag=posts
,所有相关页面的缓存就会立即失效,下次访问时会获取最新数据。 -
-
Server Actions: 服务端的数据变更
-
核心概念: Server Action 是一些只能在服务端运行的函数,但它可以被客户端组件直接调用(例如,通过表单提交或按钮点击),而无需手动创建 API 路由。这是对数据变更(mutations)流程的巨大简化。
-
如何定义: 在一个异步函数内部或一个独立文件的顶部加上
'use server';
指令。 -
示例:表单提交
// 1. 在 Server Component 中定义 Action // app/page.tsx import { revalidatePath } from 'next/cache'; export default function Page() { async function createItem(formData: FormData) { 'use server'; // 声明这是一个 Server Action const itemName = formData.get('itemName') as string; // ... 假设这里是保存到数据库的逻辑 await db.items.create({ name: itemName }); // 清除当前页面的缓存,以便显示新添加的项 revalidatePath('/'); } return ( <form action={createItem}> <input type="text" name="itemName" /> <button type="submit">Add Item</button> </form> ); }
在这个例子中,当表单提交时,
createItem
函数会在服务端执行,完成数据库操作,然后刷新页面数据,整个过程没有写一行fetch
或 API 路由代码。
表单交互增强:useActionState
-
问题场景: 当 Server Action 运行时,如何处理加载状态(pending),以及如何从服务端返回成功或错误消息并显示在界面上?
-
解决方案: 使用 React 19 新增的
useActionState
Hook(它取代了旧的useFormState
)。 -
代码示例:带状态反馈的表单
-
修改 Action 函数
Action 函数现在可以接收前一个状态,并返回新的状态。
// lib/actions.ts 'use server'; export async function updateUser(previousState: any, formData: FormData) { const username = formData.get('username') as string; try { await db.user.update({ name: username }); revalidatePath('/profile'); return { status: 'success', message: 'Profile updated successfully!' }; } catch (e) { return { status: 'error', message: 'Failed to update profile.' }; } }
-
在客户端组件中使用
useActionState
// components/ProfileForm.tsx 'use client'; import { useActionState } from 'react'; import { updateUser } from '@/lib/actions'; const initialState = { status: '', message: '' }; export function ProfileForm() { // useActionState 返回 [state, formAction, isPending] const [state, formAction, isPending] = useActionState(updateUser, initialState); return ( <form action={formAction}> <input type="text" name="username" defaultValue="KK" /> <button type="submit" disabled={isPending}> {isPending ? 'Saving...' : 'Save'} </button> {state.status === 'success' && <p className="text-green-500">{state.message}</p>} {state.status === 'error' && <p className="text-red-500">{state.message}</p>} </form> ); }
现在,这个表单拥有了自动的 pending 状态管理和来自服务端的状态消息反馈,而无需手写任何
useState
或fetch
逻辑。 -
🚀 数据获取最佳实践
并行数据获取
// ❌ 串行获取(慢)
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await fetch(`/api/posts/${params.id}`).then(r => r.json());
const comments = await fetch(`/api/posts/${params.id}/comments`).then(r => r.json());
return <PostWithComments post={post} comments={comments} />;
}
// ✅ 并行获取(快)
export default async function PostPage({ params }: { params: { id: string } }) {
const [post, comments] = await Promise.all([
fetch(`/api/posts/${params.id}`).then(r => r.json()),
fetch(`/api/posts/${params.id}/comments`).then(r => r.json())
]);
return <PostWithComments post={post} comments={comments} />;
}
错误处理与重试
// 健壮的数据获取函数
async function fetchWithRetry(url: string, retries = 3): Promise<any> {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.warn(`Attempt ${i + 1} failed:`, error);
if (i === retries - 1) throw error; // 最后一次重试失败时抛出错误
// 指数退避
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
}
// 在组件中使用
export default async function ReliableDataPage() {
try {
const data = await fetchWithRetry('https://api.example.com/data');
return <DataDisplay data={data} />;
} catch (error) {
return <ErrorMessage error={error.message} />;
}
}
数据库查询优化
// ❌ N+1 查询问题
export default async function PostsPage() {
const posts = await db.post.findMany();
const postsWithAuthors = await Promise.all(
posts.map(async (post) => ({
...post,
author: await db.user.findUnique({ where: { id: post.authorId } })
}))
);
return <PostsList posts={postsWithAuthors} />;
}
// ✅ 使用 include 一次性获取关联数据
export default async function PostsPage() {
const postsWithAuthors = await db.post.findMany({
include: {
author: true,
comments: {
take: 5, // 只获取前5条评论
orderBy: { createdAt: 'desc' }
}
}
});
return <PostsList posts={postsWithAuthors} />;
}
🛠️ Server Actions 进阶技巧
表单验证
// lib/validations.ts
import { z } from 'zod';
export const CreatePostSchema = z.object({
title: z.string().min(1, 'Title is required').max(100),
content: z.string().min(10, 'Content too short'),
tags: z.string().optional()
});
// lib/actions.ts
'use server';
import { CreatePostSchema } from './validations';
export async function createPost(previousState: any, formData: FormData) {
const rawData = {
title: formData.get('title') as string,
content: formData.get('content') as string,
tags: formData.get('tags') as string
};
// 验证数据
const validation = CreatePostSchema.safeParse(rawData);
if (!validation.success) {
return {
status: 'error',
errors: validation.error.flatten().fieldErrors,
message: 'Validation failed'
};
}
try {
await db.post.create({ data: validation.data });
revalidatePath('/posts');
return { status: 'success', message: 'Post created!' };
} catch (error) {
return { status: 'error', message: 'Failed to create post' };
}
}
渐进式增强
// components/DeleteButton.tsx
'use client';
import { useActionState, useOptimistic } from 'react';
import { deletePost } from '@/lib/actions';
export function DeleteButton({ postId, onDelete }: { postId: string, onDelete?: () => void }) {
const [state, formAction, isPending] = useActionState(deletePost, { status: '', message: '' });
// 乐观更新:立即响应用户操作
const [optimisticDeleted, setOptimisticDeleted] = useOptimistic(false);
if (optimisticDeleted) {
return <span className="text-gray-500">Deleted...</span>;
}
return (
<form action={async (formData) => {
setOptimisticDeleted(true);
onDelete?.(); // 立即从 UI 中移除
await formAction(formData);
}}>
<input type="hidden" name="postId" value={postId} />
<button
type="submit"
disabled={isPending}
className="text-red-500 hover:text-red-700"
>
{isPending ? 'Deleting...' : 'Delete'}
</button>
</form>
);
}
⚠️ 常见问题与调试
问题1:缓存过度积极
现象: 数据更新了但页面没有反映 调试:
// 在开发环境禁用缓存来测试
const data = await fetch('/api/data', {
cache: process.env.NODE_ENV === 'development' ? 'no-store' : 'default'
});
问题2:Server Action 不生效
现象: 表单提交没有执行 Action 检查清单:
- 确保函数标记了
'use server'
- 确保在 form 的 action 属性中使用
- 检查 Next.js 版本(需要 13.4+)
问题3:数据获取时间过长
调试工具:
export default async function SlowPage() {
const start = Date.now();
try {
const data = await fetch('/api/slow-endpoint');
const duration = Date.now() - start;
console.log(`Data fetching took ${duration}ms`);
return <DataDisplay data={data} />;
} catch (error) {
console.error('Data fetching failed:', error);
throw error;
}
}
💡 性能监控
添加性能追踪
// lib/performance.ts
export function withPerformanceTracking<T extends any[], R>(
fn: (...args: T) => Promise<R>,
name: string
) {
return async (...args: T): Promise<R> => {
const start = performance.now();
try {
const result = await fn(...args);
const duration = performance.now() - start;
// 发送到监控服务
console.log(`[Performance] ${name}: ${duration.toFixed(2)}ms`);
return result;
} catch (error) {
console.error(`[Performance] ${name} failed:`, error);
throw error;
}
};
}
// 使用示例
const trackedFetch = withPerformanceTracking(
async (url: string) => {
const response = await fetch(url);
return response.json();
},
'API_FETCH'
);
📝 最佳实践检查表
- ✅ 优先使用 RSC 中的
async/await
进行数据获取 - ✅ 合理设置缓存策略(
revalidate
,no-store
,tags
) - ✅ 并行获取相互独立的数据
- ✅ 在 Server Actions 中添加错误处理和验证
- ✅ 使用
useActionState
处理表单状态 - ✅ 监控数据获取性能
- ✅ 设置合理的超时时间
- ✅ 为慢查询添加 loading 状态