在 App Router 里写数据层,最容易乱的不是语法,而是默认行为。fetch 是否缓存、页面是否静态、何时该用 Server Actions,决定了你线上看到的是「秒开的旧数据」还是「每次都打到源站」。
三种常见路径
- 服务端
fetch(RSC):适合读多写少、可接受短暂陈旧的内容。 revalidate/ 标签失效:适合「大体静态,偶尔更新」的列表与详情。- Server Actions:适合表单提交、写操作,以及必须在服务端完成的副作用。
// 60 秒内复用结果;过期后下一次请求触发后台再生成
export default async function PostsPage() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60, tags: ["posts"] },
});
const posts = await res.json();
return <PostList posts={posts} />;
}什么时候关掉缓存
用户私有数据、购物车、后台配置面板,通常应该:
await fetch(url, { cache: "no-store" });或者在页面段配置 export const dynamic = "force-dynamic"。原则很简单:能被 CDN 共享的才缓存;带身份上下文的默认不缓存。
Server Actions 的边界
Server Actions 不是「前端调后端的万能 RPC」。它适合:
- 表单写入、删除、状态变更
- 需要密钥、只能放服务端的逻辑
不适合把所有读接口都改成 Action——读路径继续用 fetch / 数据库直查,语义更清晰,也更好做 HTTP 缓存。
实践建议
- 先画数据所有权:公共 / 用户私有 / 运营可变。
- 公共内容优先 ISR(
revalidate),私有内容no-store。 - 写操作后用
revalidateTag/revalidatePath主动失效,不要指望用户手动刷新。
小结
App Router 的数据获取没有银弹,只有匹配场景的默认值。把缓存策略写进代码评审清单,比记住某一个 API 名更重要。