Skip to content
工程与部署
Next.js 数据获取:缓存、重新验证与请求
Next.js 数据获取:缓存、重新验证与请求 的概念、用法、示例和注意点
2024/01/175 分钟工程与部署
Next.js 数据获取:缓存、重新验证与请求
概述
Next.js App Router 的数据获取主要发生在服务端组件中。组件自身可以是异步函数,直接在函数体内通过 await fetch() 获取数据,无需借助 getServerSideProps 或全局状态管理。父布局与子页面之间不能传递数据,因此每个页面或布局应当独立发起自己的请求。Next.js 通过请求记忆化(Request Memoization)在单次渲染过程中自动去重,避免重复网络开销。
服务端组件中的 fetch
在服务端组件内获取数据最简单的方式是直接调用 fetch 并等待响应。返回给浏览器的仅为最终的 HTML 文本,请求细节(头信息、响应处理等)保留在服务端,不会暴露到客户端。
tsx
// app/posts/page.tsx
export default async function PostsPage() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}组件在服务端完成渲染后,浏览器只会收到一份包含 <ul> 列表的静态 HTML。
缓存选项与重新验证
Next.js 对原生 fetch 进行了服务端扩展,通过 cache 选项和 next 扩展字段控制持久缓存行为。这与浏览器 fetch 的 cache 属性(作用域为 HTTP 缓存)名称相近,但控制的是 Next.js 服务端数据缓存(Data Cache)。
cache 选项
cache: 'force-cache':将请求结果存入 Data Cache,后续匹配的请求直接返回缓存,不再访问数据源。cache: 'no-store':完全跳过缓存,每次请求都直接从数据源获取最新结果。
ts
// 强制缓存(当未使用动态函数时为默认行为)
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache',
});
// 跳过缓存,始终实时获取
const res = await fetch('https://api.example.com/data', {
cache: 'no-store',
});若省略 cache 选项,Next.js 会根据当前路由是静态渲染还是动态渲染自动推断缓存策略(参见“静态渲染与动态渲染”一节)。
next.revalidate 选项
通过 next.revalidate 可以为缓存设置一个有限的生存时间(单位:秒)。在存活期内请求命中缓存时直接返回;超出后,下一次请求将触发后台重新获取(stale-while-revalidate),新响应会替换旧缓存。
ts
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 },
});revalidate 的可选值:
false:等同于cache: 'force-cache',无限期缓存。0:等同于cache: 'no-store',不缓存。number:缓存最多存续number秒。
当同时指定 cache 与 next.revalidate 时,revalidate 的语义会覆盖冲突部分。例如,cache: 'no-store' 已禁用缓存,再设置 revalidate: 60 将无实际效果。
请求记忆化
在单次渲染遍(render pass)中,Next.js 会自动记忆化(memoize)fetch 请求:相同的 URL 和选项只会发送一次,后续调用直接复用同一个 Promise 的结果。记忆化仅在单次路由渲染期间有效,不会跨请求持久化。
这一机制允许开发者在需要的组件内部就近发起请求,无需将数据提升至布局层再通过 props 层层转发。
tsx
// app/layout.tsx
async function fetchConfig() {
const res = await fetch('https://api.example.com/config');
return res.json();
}
export default async function Layout({ children }: { children: React.ReactNode }) {
// 两次调用同一函数,实际只会产生一次网络请求
const config = await fetchConfig();
const configAgain = await fetchConfig();
return (
<html lang="en">
<body>
<header>{/* 使用 config */}</header>
{children}
</body>
</html>
);
}记忆化同样适用于跨组件场景。若需要对非 fetch 请求(例如直接调用数据库)进行记忆化,可以结合 React.cache() 与 server-only 包,确保函数只在服务端执行且结果被缓存。
静态渲染与动态渲染
Next.js 在生成页面时会自动判断一条路由是静态渲染还是动态渲染。判断的关键在于是否使用了“动态函数”:cookies()、headers() 以及 searchParams 等请求期 API。一旦使用这些函数,整条路由被标记为动态渲染,框架不再缓存 HTML 输出,路由内所有 fetch 的默认行为也会从 force-cache 退化为 no-store(除非显式指定缓存策略)。
对于混合场景,可以通过 fetchCache 配置项强制路由内所有 fetch 的缓存行为。例如在页面文件顶部添加:
ts
export const fetchCache = 'force-no-store';该配置会将当前路由内所有 fetch 请求强制设为每次重新获取,即使个别请求已声明 force-cache 也会被覆盖。该选项属于段级覆盖,可选值包括 'auto'、'default-cache'、'only-cache'、'force-cache'、'default-no-store' 等。多数情况下,让框架的默认推断运作即可,无需显式配置。
按需重新验证
除了基于时间的 revalidate,Next.js 还提供按需缓存失效的能力,通过 next/cache 导出的 revalidatePath 和 revalidateTag 实现。
revalidatePath(path):使指定路径对应的整条路由缓存(页面与布局)失效。revalidateTag(tag):使所有被打上同一标签的fetch缓存条目失效。
为请求打标签的方式:
ts
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});在 Server Action 或 Route Handler 中按需触发失效:
ts
'use server';
import { revalidateTag } from 'next/cache';
export async function createPost(formData: FormData) {
// ... 写入数据源
revalidateTag('posts');
}调用 revalidateTag('posts') 后,所有携带 'posts' 标签的缓存条目被标记为过期,下一次相关请求会重新获取数据。路径级和标签级的清除可以组合使用,不会影响未命中的其他缓存。
示例:构建可缓存的数据列表
以下示例实现一个文章列表页面,满足:
- 从远程 API 获取数据。
- 首次渲染生成静态页面,并设置 30 秒的重新验证窗口。
- 提供一个“刷新”按钮,通过 Server Action 调用
revalidatePath主动使页面缓存失效。
tsx
// app/posts/page.tsx
import { revalidatePath } from 'next/cache';
interface Post {
id: number;
title: string;
}
async function getPosts(): Promise<Post[]> {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 30, tags: ['posts'] },
});
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<main>
<form
action={async () => {
'use server';
revalidatePath('/posts');
}}
>
<button type="submit">重新生成页面</button>
</form>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</main>
);
}点击“重新生成页面”按钮后,/posts 的数据缓存与页面 HTML 缓存同时失效。再次访问该路径时,Next.js 在后台重新执行服务端组件、获取最新数据并将结果写回缓存。结合 next.revalidate: 30,即使没有手动触发,30 秒后的首次访问同样会启动后台重取流程。
注意点
- 开发模式下的 HMR 缓存:本地运行
next dev时,服务端组件的fetch响应会被 HMR 缓存保留,即使设置了cache: 'no-store'也可能在模块热更新期间看不到数据变更,需要导航或整页刷新才能清除。此外,若后端响应头包含Cache-Control: no-cache,Next.js 会自动忽略next.revalidate和next.tags,每次请求直接从数据源获取。 - 仅对 GET 请求有效:Next.js 的
fetch缓存只适用于服务端组件中的 GET 请求,POST 等方法不会自动缓存。 revalidatePath与revalidateTag的作用边界:这两个 API 清除的是服务端的 Data Cache 和页面输出缓存,不影响客户端路由器缓存(Router Cache)。若需客户端立刻看到最新数据,还需要配合router.refresh()或重新导航。force-cache与动态函数混用:当路由中使用了cookies()等动态函数后,页面本身已为动态渲染,每次浏览器请求都会得到新的 HTTP 响应。此时即使为fetch显式指定cache: 'force-cache',Data Cache 层面仍可能复用,但最终输出不会被 CDN 缓存,需谨慎验证实际行为。- 请求记忆化不跨请求:记忆化只在同一次渲染遍内有效。两次独立的页面请求不会共享 fetch 结果,需要跨请求缓存时必须依赖持久缓存机制(
revalidate、tags等)。
参考链接
- [1] https://nextjs.org/docs/14/app/building-your-application/data-fetching/patterns
- [3] https://nextjs.org/docs/app/api-reference/functions/fetch
- [6] https://nextjs.org/docs/app/guides/caching-without-cache-components
- [8] https://developers.cloudflare.com/cache/concepts/cache-control
- [9] https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
- [10] https://developer.mozilla.org/en-US/docs/Web/API/Request/cache
相关推荐
2024/01/15 · 7 分钟Next.js 渲染策略:服务端组件与客户端组件
Next.js 渲染策略:服务端组件与客户端组件 的概念、用法、示例和注意点
2024/01/11 · 6 分钟Next.js 概述:从 React 到全栈框架Next.js 概述:从 React 到全栈框架 的概念、用法、示例和注意点
2025/03/07 · 7 分钟Nuxt.js 中间件、插件与服务端路由Nuxt.js 中间件、插件与服务端路由 的概念、用法、示例和注意点
2025/02/25 · 6 分钟Nuxt.js 概述Nuxt.js 概述 的概念、用法、示例和注意点
