Next.js 数据获取方法该怎么选才合适?
很多人查 Next.js 数据获取方法时,最容易混在一起的是两件事:Pages Router 的 getStaticProps / getServerSideProps,和 App Router 里的服务器组件 fetch。它们不是同一套 API,缓存规则也不完全一样。选错了,轻则页面更新不及时,重则把本来能静态缓存的页面做成每次请求都跑服务器。
先分清两套路由
如果项目还在 pages/ 目录里,数据获取主要靠 Pages Router 的三个函数:getStaticProps、getServerSideProps、getStaticPaths。
如果项目使用 app/ 目录,优先在 Server Component 里直接 await fetch(),再配合 cache、next.revalidate、next.tags、revalidateTag 和 revalidatePath 控制缓存。客户端交互数据再交给 useEffect、SWR 或 TanStack Query。
Pages Router:三个函数各管一类场景
getStaticProps:构建时生成,适合可公开缓存的数据
getStaticProps 只用于 pages/ 下的页面文件。它在服务端运行,不会进浏览器 bundle;生产环境默认在 next build 时执行,返回的 props 会生成 HTML 和 JSON。用户通过 next/link 跳转时,Next.js 读取这份 JSON,不会在浏览器里重新执行 getStaticProps。
jsexport async function getStaticProps() { const post = await getPost() return { props: { post }, revalidate: 60, notFound: !post, } } export default function Page({ post }) { return <article>{post.title}</article> }
它适合博客、文档、商品详情这类“不需要根据当前用户变化”的页面。revalidate 开启 ISR 后,旧页面会先继续可用,Next.js 在后台重新生成新版本。不要把 token、内部权限信息放进 props,因为这些数据会随初始 HTML 暴露给客户端。
getServerSideProps:每次请求执行,适合强实时或个性化页面
getServerSideProps 也只能从 pages/ 页面导出。它在每次请求时运行,可以读取 req、res、cookies、headers、query、params,所以适合用户中心、权限页面、AB 实验、地理位置相关内容。
jsexport async function getServerSideProps({ req }) { const token = req.cookies.token const profile = await getProfile(token) return { props: { profile } } }
代价也明显:每次访问都要等服务端取数和渲染。能用 getStaticProps + revalidate 解决的内容,不要习惯性上 SSR。确实要缓存 SSR 响应时,可以在 res 上设置 Cache-Control,但优先判断 ISR 是否已经够用。
getStaticPaths:给动态路由提前列路径
动态路由如果配合 getStaticProps,就需要 getStaticPaths 告诉 Next.js 哪些路径要预生成。
jsexport async function getStaticPaths() { const posts = await getPosts() return { paths: posts.map((post) => ({ params: { slug: post.slug } })), fallback: 'blocking', } } export async function getStaticProps({ params }) { const post = await getPost(params.slug) return { props: { post }, revalidate: 300 } }
fallback 的选择很关键:
| fallback | 行为 | 适合场景 |
|---|---|---|
false | 没在 paths 里的页面直接 404 | 页面数量少,构建时能全部生成 |
true | 先返回 fallback 页面,再在后台生成真实页面 | 有自定义骨架屏,能处理临时空状态 |
'blocking' | 首次访问时服务端生成,生成完再返回 HTML | 详情页很多,又希望首屏就是完整 HTML |
SEO 页面通常更偏向 false 或 'blocking'。如果用 true,页面组件要处理 router.isFallback,否则用户可能先看到缺数据的内容。
App Router:默认在服务器组件里取数
App Router 不再使用 getStaticProps 和 getServerSideProps。在 app/ 下,页面、布局和服务器组件可以直接写成 async 函数。
jsexport default async function Page() { const res = await fetch('https://api.example.com/posts', { next: { revalidate: 300, tags: ['posts'] }, }) if (!res.ok) throw new Error('Failed to fetch posts') const posts = await res.json() return <PostList posts={posts} /> }
这段代码运行在服务端,浏览器拿到的是渲染结果,不会把取数逻辑直接打包给客户端。Server Component 里相同 URL、相同配置的 GET fetch 在一次服务端渲染过程中会被自动 memoize,同一个请求树里多处调用通常只会真正请求一次。
App Router 的 fetch 缓存别按旧印象理解
Next.js 扩展了服务端 fetch。这里的 cache 不是浏览器 HTTP 缓存,而是 Next.js 服务端缓存策略。
jsawait fetch(url, { cache: 'force-cache' }) await fetch(url, { cache: 'no-store' }) await fetch(url, { next: { revalidate: 60 } }) await fetch(url, { next: { tags: ['posts'] } })
常用规则可以这样记:
- 默认是
auto no cache:开发环境通常每次请求源站;生产构建时,如果路由能静态预渲染,可能在 build 阶段取一次并进入静态结果;如果路由使用了 cookies、headers、searchParams 等请求时 API,通常会在每次请求时取数。 cache: 'force-cache':优先读 Next.js 服务端缓存;没有命中或已过期时再请求源站并更新缓存。cache: 'no-store':每次都请求源站,适合强实时、用户私有、不能共享缓存的数据。next.revalidate: false:语义上接近长期缓存;0表示不缓存;数字表示最多缓存多少秒。- 不要同时写
{ cache: 'no-store', next: { revalidate: 60 } },这是冲突配置,开发环境会警告,配置会被忽略。 - 同一路由里相同 URL 如果设置了不同的
revalidate,更小的时间会生效;单个 fetch 的revalidate低于路由默认值时,也会拉低整个路由的重新验证间隔。
如果项目启用了较新的 Cache Components 模型,'use cache'、cacheLife()、cacheTag() 会成为更明确的缓存表达方式;但在大量 App Router 项目里,fetch 的 cache、next.revalidate、next.tags 仍然是最常见的取数入口。
revalidate、tags、revalidateTag 和 revalidatePath 怎么配合
next.revalidate 解决的是“多久自动刷新一次”。如果内容由 CMS webhook、后台发布、管理端操作触发更新,就需要按需重新验证。
js// app/blog/page.js export default async function BlogPage() { const posts = await fetch('https://api.example.com/posts', { next: { tags: ['posts'] }, }).then((res) => res.json()) return <PostList posts={posts} /> }
然后在 Server Action 或 Route Handler 里触发:
js'use server' import { revalidatePath, revalidateTag } from 'next/cache' export async function publishPost() { await savePost() revalidateTag('posts', 'max') revalidatePath('/blog') }
两者用途不同:
revalidateTag('posts', 'max'):让所有使用posts标签的数据变旧。推荐传第二个参数'max',它采用 stale-while-revalidate 语义:下次访问先可用旧内容,同时后台刷新。revalidatePath('/blog'):让指定页面或布局路径重新验证。传动态路由模式时要带类型,例如revalidatePath('/blog/[slug]', 'page')。- 只调用
revalidatePath('/blog')不会自动刷新其他也用了posts标签的页面;只调用revalidateTag('posts', 'max')也不等于指定某个路径重新生成。需要一致性时两个一起用。
标签也有限制:自定义 tag 最长 256 个字符,一次最多 128 个。tag 名大小写敏感,最好用稳定的业务名,比如 post:${id}、posts、category:${slug}。
Suspense 和 Streaming:解决“慢数据拖慢整页”
App Router 里可以把慢数据拆到子组件,用 <Suspense> 包起来。静态外壳先返回,慢组件完成后再流式补上。
jsimport { Suspense } from 'react' async function Comments({ postId }) { const comments = await fetch(`https://api.example.com/posts/${postId}/comments`, { cache: 'no-store', }).then((res) => res.json()) return <CommentList comments={comments} /> } export default function Page({ params }) { return ( <> <PostDetail id={params.id} /> <Suspense fallback={<p>评论加载中...</p>}> <Comments postId={params.id} /> </Suspense> </> ) }
注意,Suspense 本身不是“强制动态渲染”的开关。它负责 fallback 和 streaming;是否缓存、是否每次请求取数,还要看组件里的数据访问方式,比如 no-store、请求时 API,或 Cache Components 下是否使用了 'use cache'。
客户端数据获取:只在需要浏览器状态时使用
不是所有数据都应该放到客户端取。客户端取数会晚于 HTML,到达首屏时更容易出现 loading,也不利于 SEO。它适合这些场景:依赖浏览器状态、用户操作频繁、需要焦点重试、轮询、乐观更新,或者数据完全不影响搜索索引。
useEffect:够简单,但很多事要自己处理
js'use client' import { useEffect, useState } from 'react' export default function Profile() { const [data, setData] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) useEffect(() => { let ignore = false fetch('/api/profile') .then((res) => { if (!res.ok) throw new Error('Request failed') return res.json() }) .then((json) => !ignore && setData(json)) .catch((err) => !ignore && setError(err)) .finally(() => !ignore && setLoading(false)) return () => { ignore = true } }, []) if (loading) return <p>加载中...</p> if (error) return <p>加载失败</p> return <pre>{JSON.stringify(data, null, 2)}</pre> }
useEffect 胜在没有依赖,缺点是缓存、去重、重试、竞态处理都要自己写。页面稍微复杂一点,SWR 或 TanStack Query 通常更稳。
SWR:适合轻量客户端缓存
js'use client' import useSWR from 'swr' const fetcher = (url) => fetch(url).then((res) => res.json()) export default function UserCard() { const { data, error, isLoading } = useSWR('/api/user', fetcher, { revalidateOnFocus: false, dedupingInterval: 60000, }) if (isLoading) return <p>加载中...</p> if (error) return <p>加载失败</p> return <p>{data.name}</p> }
SWR 的心智模型很直接:先用缓存,后台重新验证。适合用户信息、通知数量、列表筛选这类客户端体验数据。
TanStack Query:适合复杂交互和服务端状态
js'use client' import { useQuery } from '@tanstack/react-query' function getUser() { return fetch('/api/user').then((res) => { if (!res.ok) throw new Error('Request failed') return res.json() }) } export default function UserPanel() { const { data, error, isLoading } = useQuery({ queryKey: ['user'], queryFn: getUser, staleTime: 60_000, gcTime: 300_000, }) if (isLoading) return <p>加载中...</p> if (error) return <p>加载失败</p> return <p>{data.name}</p> }
TanStack Query 更适合有分页、筛选、mutation、乐观更新、失效联动的后台系统。用 v5 时是 gcTime,老文章里的 cacheTime 是 v4 叫法。
错误和加载状态要按路由类型处理
Pages Router 里,getStaticProps 可以返回 notFound 或 redirect,getServerSideProps 抛错会走 500 页面。页面组件里的客户端请求还要自己渲染 loading 和 error。
App Router 里,常用文件约定更清楚:
loading.js/loading.tsx:当前路由段的加载 UI。error.js/error.tsx:当前路由段的错误边界,必须是 Client Component。not-found.js/not-found.tsx:配合notFound()渲染 404。<Suspense fallback={...}>:给局部慢组件提供 fallback,并支持 streaming。
取数代码里不要只写 return res.json()。至少检查 res.ok,否则 404、500 返回的错误 HTML 也可能被当成 JSON 解析,最后报一个很绕的异常。
并行数据获取:别无意中写成瀑布流
如果两个请求互不依赖,不要先 await A 再 await B。
jsexport default async function Dashboard() { const postsPromise = getPosts() const statsPromise = getStats() const userPromise = getUser() const [posts, stats, user] = await Promise.all([ postsPromise, statsPromise, userPromise, ]) return <DashboardView posts={posts} stats={stats} user={user} /> }
如果 B 依赖 A 的结果,顺序请求没问题;如果只是写起来顺手,就会把 300ms + 500ms 变成 800ms。App Router 里拆组件时也一样,能并行就把 promise 提前创建,慢组件再交给 Suspense。
到底该选哪种方法?
| 需求 | Pages Router | App Router |
|---|---|---|
| 内容公开、变化不频繁、重视 SEO | getStaticProps + revalidate | Server Component fetch + next.revalidate 或 force-cache |
| 动态详情页很多 | getStaticPaths + 'blocking' | generateStaticParams 预生成热门路径,其余按需处理 |
| 每个用户看到的数据不同 | getServerSideProps | 使用请求时 API,或 cache: 'no-store' 的服务端取数 |
| 由后台发布触发刷新 | ISR / 按需 revalidate | next.tags + revalidateTag,必要时加 revalidatePath |
| 浏览器交互驱动的数据 | 客户端请求、SWR、TanStack Query | Client Component + SWR / TanStack Query |
| 慢数据不想挡住整页 | 自定义 loading 或拆客户端请求 | <Suspense> + Streaming |
一句话判断:能静态就别 SSR,能在服务器取就别放客户端,能按标签失效就别全站刷新。Next.js 的数据获取并不难,难的是把“数据什么时候变、给谁看、能不能缓存”这三个问题先想清楚。