1. 服务端数据获取
在 App Router 中,默认导出的页面与布局组件可以是 异步 Server Components。这意味着你可以像写普通异步函数一样,在组件顶层直接使用 async/await 拉取数据,而不必把数据获取塞进 useEffect。
Next.js 对浏览器原生的 fetch 做了扩展:除了标准参数外,还支持 cache、next.revalidate、next.tags 等选项,用于与全路由缓存、按需失效策略对齐。
下面示例在 async 页面组件中请求 JSON 并渲染列表(路径可按项目调整):
type Post = { id: number; title: string };
async function getPosts(): Promise<Post[]> {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5", {
cache: "force-cache",
});
if (!res.ok) throw new Error("Failed to fetch posts");
return res.json();
}
export default async function Page() {
const posts = await getPosts();
return (
<main className="p-8">
<h1 className="text-2xl font-bold">文章列表</h1>
<ul className="mt-4 space-y-2">
{posts.map((post) => (
<li key={post.id} className="border-b pb-2">
<span className="font-mono text-gray-500">#{post.id}</span> {post.title}
</li>
))}
</ul>
</main>
);
}
💡 要点
在 Server Components 中可以直接 await fetch(...),数据在服务端解析后再把 HTML/RSC 载荷发给客户端,无需 useEffect 与客户端二次请求(除非你需要浏览器端交互或订阅)。
有 React SPA 经验时
在纯 CSR 应用中,常见模式是 useEffect + fetch;在 Next.js App Router 中,优先把可静态或服务端完成的读取放在 Server Component,减少客户端 JS 与水合成本。
2. 缓存策略
fetch 的第二个参数可控制数据缓存行为:
cache: 'force-cache'(默认):尽可能复用缓存结果,适合内容变化不频繁的请求。cache: 'no-store':每次请求都向源站拉取,适合强实时或用户私有数据(仍需配合鉴权)。
此外可使用 next: { revalidate: number } 做基于时间的 revalidation(ISR 风格):在指定秒数后,下次访问会触发后台重新验证。
三种典型写法:
// 1) 默认:静态缓存(构建期或首次请求后复用,具体行为见官方 Data Cache 说明)
await fetch("https://api.example.com/items", { cache: "force-cache" });
// 2) 禁用缓存:始终动态获取
await fetch("https://api.example.com/me", { cache: "no-store" });
// 3) 时间窗口 revalidate:每 60 秒最多后台刷新一次
await fetch("https://api.example.com/feed", {
next: { revalidate: 60 },
});
与 Route Handler 或外部工具联调时,可用 curl 快速验证接口(与 Next 缓存无关,仅便于调试):
curl -sS "https://jsonplaceholder.typicode.com/posts/1" | head -c 200
POST 请求体常用 JSON 格式,例如创建资源:
{
"title": "Hello Next.js 15",
"body": "Server Actions 与 fetch 缓存",
"userId": 1
}
| 方式 | 典型场景 | 行为概要 |
|---|---|---|
| cache: 'force-cache' | 博客文章、文档站公开内容 | 尽量命中 Data Cache,减少上游压力。 |
| cache: 'no-store' | 仪表盘、个性化接口 | 不做持久化缓存,每次执行都拉新数据。 |
| next: { revalidate: n } | 新闻列表、价格等需定期刷新 | 在 n 秒周期内可复用,过期后后台更新。 |
3. Server Actions
Server Actions 是在服务端执行的异步函数,用于处理表单提交、数据库写入、调用内部 API 等,无需单独暴露 HTTP 路由(编译器会生成安全端点)。
- 在文件或函数顶部使用
'use server'指令,将该模块或函数标记为服务端代码。 - 内联:在 Server Component 文件内直接定义 async 函数并加
'use server'(函数体级)。 - 独立文件:例如
app/actions.ts首行'use server',导出多个 action,供表单或客户端组件导入。
独立文件示例(src/app/actions.ts):
'use server';
export async function createPost(formData: FormData) {
const title = String(formData.get("title") ?? "");
const body = String(formData.get("body") ?? "");
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, body, userId: 1 }),
});
if (!res.ok) {
throw new Error("createPost failed");
}
return res.json();
}
在 Server Component 中内联定义并绑定到表单(简化演示):
export default async function Page() {
async function submit(formData: FormData) {
"use server";
const name = String(formData.get("name") ?? "");
// 此处可写入数据库或调用内部服务
console.log("server received:", name);
}
return (
<form action={submit}>
<input name="name" className="border px-2" />
<button type="submit">提交</button>
</form>
);
}
4. 表单与 Server Actions
原生 <form> 的 action 可直接绑定 Server Action:提交时由服务端执行,天然支持渐进增强(无 JS 也能提交)。
- useFormStatus(来自
react-dom):在表单内部子组件中读取pending,用于禁用按钮、显示「提交中…」。 - useActionState(React 19,来自
react):将 Server Action 包装为可接收上一次返回 state 的函数,适合错误信息、校验结果回显(原useFormState的继任者)。
完整示例:Server Action 返回消息 + 客户端展示 pending 与 state(拆成 Client 子组件以满足 hooks 规则):
// app/actions.ts
'use server';
export type FormState = { ok: boolean; message: string };
export async function subscribe(
_prevState: FormState,
formData: FormData
): Promise<FormState> {
const email = String(formData.get("email") ?? "").trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return { ok: false, message: "邮箱格式不正确" };
}
// 模拟写入
return { ok: true, message: "已订阅:" + email };
}
// app/ui/subscribe-form.tsx
'use client';
import { useFormStatus } from 'react-dom';
import { useActionState } from 'react';
import { subscribe, type FormState } from '@/app/actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="rounded bg-sky-600 px-4 py-2 text-white disabled:opacity-50"
>
{pending ? '提交中…' : '订阅'}
</button>
);
}
const initialState: FormState = { ok: false, message: '' };
export function SubscribeForm() {
const [state, formAction] = useActionState(subscribe, initialState);
return (
<form action={formAction} className="space-y-2">
<input
name="email"
type="email"
placeholder="you@example.com"
className="w-full rounded border px-3 py-2"
required
/>
<SubmitButton />
{state.message ? (
<p className={state.ok ? 'text-green-600' : 'text-red-600'}>
{state.message}
</p>
) : null}
</form>
);
}
// app/page.tsx(Server Component 中引用客户端表单)
import { SubscribeForm } from '@/app/ui/subscribe-form';
export default function Page() {
return (
<main className="p-8">
<h1 className="text-xl font-bold">邮件订阅</h1>
<SubscribeForm />
</main>
);
}
5. 数据获取模式
同一页面需要多个接口时,优先分析依赖关系:无依赖的请求应并行,有依赖的再串行。
- 并行:使用
Promise.all(或Promise.allSettled)同时发起,缩短总等待时间。 - 顺序:后一步依赖前一步结果时,使用连续的
await。 - 预加载与去重:React 的
cache(fn)包装数据读取函数,可在多次调用间复用同一请求结果(与 RSC 渲染去重配合)。
并行 vs 顺序:
import { cache } from 'react';
const getUser = cache(async (id: string) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
cache: 'no-store',
});
return res.json();
});
export default async function DashboardPage() {
// 并行:用户与文章列表互不依赖
const [user, posts] = await Promise.all([
getUser('1'),
fetch('https://jsonplaceholder.typicode.com/posts?userId=1', {
cache: 'no-store',
}).then((r) => r.json()),
]);
// 顺序:先取 post 再取评论(示例)
const firstPost = posts[0];
const comments = firstPost
? await fetch(
`https://jsonplaceholder.typicode.com/posts/${firstPost.id}/comments`,
{ cache: 'no-store' }
).then((r) => r.json())
: [];
return (
<section className="p-8">
<h1 className="text-2xl font-bold">{user.name}</h1>
<p className="text-gray-600">{user.email}</p>
<h2 className="mt-6 font-semibold">评论数:{comments.length}</h2>
</section>
);
}
💡 要点
尽量并行获取互不依赖的数据,避免在 Server Component 中写成长串无必要的 await 瀑布流;对重复调用的纯函数请求可用 cache() 收敛。
6. 按需 Revalidation
当 Server Action 或 Route Handler 中写入了新数据,你可能希望立即让某段缓存失效,而不是等到 revalidate 时间窗。Next.js 提供:
revalidatePath('/path'):按路径失效,下次访问该路由会重新获取数据。revalidateTag('tag'):失效所有在fetch上打了相同 tag 的缓存条目。
在 Server Action 更新后调用 revalidatePath:
'use server';
import { revalidatePath } from 'next/cache';
export async function addItem(formData: FormData) {
const title = String(formData.get('title') ?? '');
// await db.item.create({ data: { title } })
revalidatePath('/items');
}
为 fetch 打上 tag,并在变更后 revalidateTag:
// 读取侧(Server Component 或共享数据模块)
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
const posts = await res.json();
'use server';
import { revalidateTag } from 'next/cache';
export async function refreshPosts() {
revalidateTag('posts');
}
配合策略:列表页用 tag 聚合缓存,详情页也可用 revalidatePath('/posts/[id]') 精确刷新;按项目一致约定即可。
7. 本章要点
服务端拉数
Server Component 中直接使用 async/await 与扩展后的 fetch,理解默认缓存与 no-store、revalidate。
Server Actions
用 'use server' 声明服务端函数;表单 action= 绑定;复杂逻辑放到独立 actions.ts。
表单 UX
useFormStatus 处理 pending;useActionState 处理上一次返回值与校验错误。
模式与失效
Promise.all 并行、cache() 去重;写入后用 revalidatePath / revalidateTag 按需刷新。