大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
数据获取
这一课你将能做到
- 在 Server Component 中直接用
fetch获取数据,并理解其缓存行为 - 用
revalidate和revalidatePath/revalidateTag控制缓存更新 - 用 Server Actions 处理表单提交和数据变更
- 用
loading.tsx和<Suspense>实现数据加载的骨架屏 - 区分「请求时获取」和「构建时获取」,避免过度渲染
为什么要学
数据获取是 Next.js 的核心战场。传统 React 里你用 useEffect + fetch 或 SWR/React Query,数据在浏览器请求, loading 状态自己管,缓存自己实现。Next.js Server Component 把数据获取挪到服务端:组件直接 await fetch,渲染时数据已经 ready,HTML 里直接带内容。这不仅更快,还解决了「瀑布请求」——组件嵌套时,父组件等子组件的数据,子组件等孙组件的数据,一层层串行请求。
人话:传统方式是「服务员(浏览器)每上一道菜都要跑一次厨房(API)」,Server Component 是「厨房一次性把所有菜做好,服务员一次性端上来」。
先建立图像
传统 React(CSR):
浏览器请求 → 服务器返回空 HTML + JS → 浏览器执行 JS → 浏览器请求 API → 渲染
Next.js Server Component:
浏览器请求 → 服务器执行组件(内部 fetch)→ 服务器返回完整 HTML → 浏览器直接显示
Server Action:
浏览器提交表单 → POST 到服务器 → 服务器执行 Action → 重新渲染页面返回
关键区别:数据获取发生在服务器渲染期间,不是浏览器渲染之后。
跟着做
步骤 1:Server Component 基础 fetch
app/posts/page.tsx:
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
if (!res.ok) throw new Error('获取文章失败');
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<main style={{ padding: 40 }}>
<h1>文章列表</h1>
<ul>
{posts.map((p: any) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
</main>
);
}
查看页面源代码,文章列表已经在 HTML 里——数据在服务器获取,不是浏览器。
步骤 2:缓存控制
Next.js 默认缓存 Server Component 的 fetch。验证方式:连续刷新页面,观察网络面板,第二次请求返回 304 或来自内存缓存。
强制不缓存(SSR 模式):
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
cache: 'no-store',
});
return res.json();
}
定时缓存(ISR 模式):
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { revalidate: 3600 }, // 1 小时
});
return res.json();
}
按标签缓存,精确刷新:
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { tags: ['posts'] },
});
return res.json();
}
然后在 Server Action 或 Route Handler 里调用 revalidateTag('posts'),所有带 posts 标签的缓存立即失效。
步骤 3:并行数据获取
避免瀑布请求,用 Promise.all 并行:
async function getData() {
const [postsRes, usersRes] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts?_limit=5'),
fetch('https://jsonplaceholder.typicode.com/users?_limit=5'),
]);
const [posts, users] = await Promise.all([postsRes.json(), usersRes.json()]);
return { posts, users };
}
export default async function DashboardPage() {
const { posts, users } = await getData();
return (
<main>
<section>
<h2>最新文章</h2>
<ul>{posts.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
</section>
<section>
<h2>活跃用户</h2>
<ul>{users.map((u: any) => <li key={u.id}>{u.name}</li>)}</ul>
</section>
</main>
);
}
两个请求同时发出,总耗时是较慢的那个,不是两者之和。
步骤 4:Server Actions 处理表单
Server Action 是「在服务器执行的函数」,可以直接操作数据库,不需要写 API 路由。
app/actions.ts:
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// 模拟写入数据库
console.log('创建文章:', { title, content });
await new Promise(r => setTimeout(r, 500));
// 刷新文章列表缓存
revalidatePath('/posts');
// 跳转到列表页
redirect('/posts');
}
app/posts/new/page.tsx:
import { createPost } from '@/app/actions';
export default function NewPostPage() {
return (
<main style={{ padding: 40 }}>
<h1>写文章</h1>
<form action={createPost}>
<div>
<label htmlFor="title">标题</label>
<input id="title" name="title" type="text" required />
</div>
<div>
<label htmlFor="content">内容</label>
<textarea id="content" name="content" rows={5} required />
</div>
<button type="submit">发布</button>
</form>
</main>
);
}
表单提交后,浏览器 POST 到当前 URL,Next.js 执行 createPost,完成后 redirect 到 /posts。整个过程没有手写 API 路由。
步骤 5:Server Action 的 useFormStatus
app/posts/new/SubmitButton.tsx:
'use client';
import { useFormStatus } from 'react-dom';
export default function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '发布中…' : '发布'}
</button>
);
}
然后在表单里用 <SubmitButton /> 替换原生 button。提交时按钮自动禁用并显示加载态。
步骤 6:数据库直读(Server Component 独有优势)
Server Component 可以直接连接数据库,不经过 API 层。以 Prisma 为例:
npm install prisma @prisma/client
npx prisma init
prisma/schema.prisma:
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
content String
createdAt DateTime @default(now())
}
npx prisma db push
lib/db.ts:
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
app/posts/page.tsx(直读数据库):
import { prisma } from '@/lib/db';
export default async function PostsPage() {
const posts = await prisma.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
});
return (
<main style={{ padding: 40 }}>
<h1>文章列表(来自数据库)</h1>
<ul>
{posts.map(p => (
<li key={p.id}>
<strong>{p.title}</strong>
<p>{p.content}</p>
</li>
))}
</ul>
</main>
);
}
没有 API 路由,没有 fetch,组件直接查数据库。这是 Server Component 的核心优势。
讲清楚要点
- fetch 缓存层级:Next.js 对 Server Component 的 fetch 做了四层缓存:Request Memoization(单次渲染去重)、Data Cache(跨请求持久化)、Full Route Cache(整页 HTML 缓存)、Router Cache(客户端路由缓存)。
cache: 'no-store'跳过 Data Cache,revalidate控制 Data Cache 过期时间。 - revalidatePath vs revalidateTag:
revalidatePath('/posts')刷新整个路径的缓存;revalidateTag('posts')只刷新带该标签的 fetch。前者粗粒度,后者精确。 - Server Action 是 POST 到当前页:表单
action={serverAction}提交后,浏览器 POST 到当前 URL,Next.js 内部路由到 Action 执行。支持FormData,也支持直接传对象(需要序列化)。 - Server Action 的返回值:Action 可以返回可序列化的值(不能返回函数、类实例),客户端可以用
useActionState或useFormStatus获取状态。 - 数据库直读的条件:只在 Server Component 里直读,Client Component 必须通过 API 或 Server Action。数据库连接要全局单例(避免开发环境热重载时创建过多连接)。
- loading.tsx 是路由级 loading:如果页面有多个异步区域,用
<Suspense>局部包裹比loading.tsx更精细。
你可能会懵的地方
-
Server Component 的 fetch 和浏览器的 fetch 一样吗? 语法一样,但 Next.js 在服务端扩展了它:自动缓存、自动去重、支持
next: { revalidate, tags }。浏览器端的原生 fetch 没有这些。 -
Server Action 安全吗?会不会被恶意调用? Server Action 本质是 HTTP POST 端点,和 API 路由一样需要鉴权。Next.js 会自动验证
Origin头,但你仍要在 Action 内部做权限检查。 -
revalidatePath是立即生效吗? 它标记缓存失效,下一个请求触发重新生成。不是同步重建,而是「下一个访问者触发」。 -
Server Component 里能用 SWR / React Query 吗? 不能。这些是客户端库,Server Component 里没有浏览器环境。Server Component 的数据获取就是
await fetch/await prisma。 -
Server Action 能传文件吗? 可以,通过
FormData。Action 里用formData.get('file')拿到 File 对象,然后转 Buffer 或流处理。 -
为什么我的 fetch 每次都重新请求,没有缓存? 检查:① 是否加了
cache: 'no-store';② 是否在动态路由(force-dynamic)下;③ 是否部署在 Edge Runtime(Edge 默认不缓存)。另外,开发模式下缓存行为可能和线上不同。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
Server Component 里用 useEffect fetch | 服务端没有 useEffect | 直接 await fetch |
表单提交用 onSubmit={handleSubmit} | 需要写 API 路由 | 用 action={serverAction} |
revalidatePath 写成 revalidatePath('/posts', 'page') | 第二个参数不是字符串 | 正确:revalidatePath('/posts') 或 revalidatePath('/posts/[id]', 'page') |
| Server Action 里返回类实例 | 序列化失败 | 返回 plain object 或 void |
| 数据库查询放在 Client Component | 暴露连接串、无法直读 | 挪到 Server Component 或 API 路由 |
以为 revalidate = 60 是每 60 秒自动刷新 | 它是「最大缓存时间」,低流量时可能很久不刷新 | 配合 revalidatePath 或 revalidateTag 主动刷新 |
卡点急救
- Server Action 不执行,表单直接跳转:检查 Action 文件顶部是否有
'use server';检查表单action是否绑定的是函数引用,不是字符串。 - revalidatePath 不生效:确认路径格式。
revalidatePath('/posts')刷新/posts;revalidatePath('/posts/[id]')需要第二个参数'page'来刷新动态段。也检查是否有 CDN 层缓存。 - Prisma 开发环境报
Too many connections:用全局单例模式(上面的lib/db.ts写法),或设置connection_limit=1。 - fetch 缓存不生效:检查是否
cache: 'no-store'覆盖了默认行为;检查是否dynamic = 'force-dynamic'在页面级别强制了 SSR。 - Server Action 返回的数据在客户端拿不到:用
useActionStatehook,或在 Action 里redirect()。直接 return 的值只在渐进增强表单里可用。
面试常问
-
Next.js 的数据缓存有哪几层? 四层:① Request Memoization(单次渲染内相同 fetch 去重);② Data Cache(跨请求的 fetch 结果缓存,默认持久化);③ Full Route Cache(整页 HTML 缓存,SSG/ISR 用);④ Router Cache(客户端路由切换时的 RSC payload 缓存)。
-
Server Action 和 API 路由怎么选? Server Action 适合表单提交、数据变更,和页面耦合紧,不需要手动写路由;API 路由适合对外提供 REST/GraphQL 接口,给第三方调用。内部数据变更优先 Server Action,对外暴露接口用 API 路由。
-
cache: 'no-store'和revalidate: 0等价吗? 在 Data Cache 层面等价,都不缓存。但force-dynamic还会禁用 Full Route Cache,影响更大。no-store只影响当前 fetch,force-dynamic影响整个路由。 -
Server Component 里怎么传数据给 Client Component? 通过 props。Server Component 渲染时把数据作为 props 传给 Client Component,数据会被序列化(RSC payload),客户端 React 反序列化后渲染。注意 props 必须是可序列化的。
-
怎么实现「提交表单后刷新列表」? Server Action 里执行变更后,调用
revalidatePath('/list'),然后redirect('/list')。列表页的 fetch 缓存被标记失效,重新渲染时获取新数据。
自测三问
fetch(url, { next: { revalidate: 60 } })和export const revalidate = 60有什么区别?- Server Action 里能直接操作 DOM 吗?为什么?
- 一个页面同时有
export const revalidate = 60和某个 fetch 的cache: 'no-store',最终行为是什么?
小结一句话
Next.js 数据获取的核心是「在服务端完成」:Server Component 里 await fetch 直读数据,Server Action 里直接变更数据,配合多层缓存和精确失效策略,既快又实时。
延伸
- Next.js 官方 - Data Fetching
- Next.js 官方 - Server Actions
- 下一课:样式与静态资源——CSS Modules、Tailwind、全局样式、字体优化、图片优化、public 目录的完整用法。