React: Next.js 数据获取与 API
最后更新:2026-08-26
Tom 把博客切换到 App Router 后,又遇到了新问题:不同页面的数据更新频率差异很大——文章一个月改一次,产品价格一天改几次,用户头像每次登录都可能变。他需要针对每种数据特点选择不同的获取策略,同时还要给前端提供统一的 API 接口。Next.js 的 Server Component 数据获取和 Route Handler 正好帮他解决了这些痛點。
1. 你将学到
- Server Component 中直接
fetch数据的三种缓存策略(force-cache / no-store / revalidate) - Route Handler 创建 RESTful API 端点(GET/POST/PUT/DELETE)
- ISR 增量静态生成的工作原理与配置
- Middleware 中间件的请求拦截与路由守卫
- 环境变量在服务端和客户端的安全访问策略
- On-Demand ISR 按需刷新缓存的实现方法(revalidateTag / revalidatePath)
- Edge Runtime 中 Middleware 的能力边界和性能注意事项
2. 概念图解
Tom 梳理了 Next.js 中数据获取的完整流程:页面请求到达后,Next.js 根据数据获取策略决定是否缓存数据、是否重新验证、是否走 API 路由。理解这个流程帮助他针对不同数据特点选择正确的方法。
流程图中的决策节点 {缓存策略} 是本节的核心——它决定了数据获取的性能和实时性。force-cache 最快但不实时,no-store 最实时但性能最差,revalidate 和 tags 在两者之间提供了灵活的平衡方案。Route Handler 和 Middleware 作为补充机制,分别处理 API 访问和请求拦截。
flowchart TD
A[页面请求] --> B{Server Component}
B --> C[fetch 数据]
C --> D{缓存策略}
D -->|force-cache| E[读取缓存<br/>默认 SSG]
D -->|no-store| F[每次都请求<br/>实时 SSR]
D -->|revalidate: N| G[缓存 N 秒<br/>ISR 模式]
D -->|{next: {tags: [...]}}| H[按标签缓存<br/>On-Demand ISR]
E --> I[返回 HTML]
F --> I
G --> I
H --> I
I --> J[Route Handler<br/>/api/*]
J --> K[数据库/外部 API]
I --> L{需验证?}
L -->|是| M[Middleware<br/>鉴权/重定向]
L -->|否| N[直接渲染]
3. 一个真实场景
Tom 在博客迁移到 App Router 后发现了数据获取的新痛点。之前用 getStaticProps 和 getServerSideProps,虽然熟悉但不够灵活。比如文章列表需要每小时更新一次,但某一篇热门文章被编辑修改后需要立刻刷新——getStaticProps 做不到按页控制。
App Router 引入了全新的数据获取范式:Server Component 中直接用 fetch,通过 next.revalidate 和 next.tags 精确控制缓存策略。Tom 同时用 Route Handler 暴露 API 接口给评论区的前端组件调用,用 Middleware 保护仪表盘路由。下面是他为博客设计的数据获取架构。
他首先梳理了博客中所有数据源的特征:文章内容(每小时更新一次,可以接受缓存)、评论(实时更新,需要即时展示)、统计数据(每次访问都不同)、产品信息(每天更新几次,更新后需尽快展示)。然后针对每种数据类型选择了不同的获取策略。这个分析过程帮助他理解了 Next.js 数据获取设计的美妙之处——不是一刀切的"SSG 或 SSR",而是可以按页面、按请求、甚至按数据标签精确控制。
(1) Server Component 数据获取
Server Component 中可以直接 await fetch,这是 App Router 最强大的特性之一。不需要 useEffect、不需要 SWR/React Query、不需要额外的客户端状态管理库——直接在组件函数中请求数据,服务端渲染完成后连同 HTML 一起发给浏览器。
fetch 的第二个参数接受一个包含 next 配置的对象,用于控制缓存行为:{ cache: 'force-cache' } 等同于 SSG,数据只在构建时获取一次;{ cache: 'no-store' } 等同于 SSR,每次请求都重新获取;{ next: { revalidate: 60 } } 等同于 ISR,缓存 60 秒后重新验证。
更高级的用法是 On-Demand ISR:使用 next: { tags: ['posts'] } 给数据打标签,然后在 Route Handler 或管理员页面中调用 revalidateTag('posts') 主动刷新。这样文章编辑后可以立即重新生成,而无需等待 revalidate 时间到期。
三种缓存策略决策表
| 策略 | fetch 配置 | 行为 | 适用场景 |
|---|---|---|---|
| 静态 SSG | cache: 'force-cache' |
构建时获取一次,后续全走 CDN | 文章内容、关于页面 |
| 实时 SSR | cache: 'no-store' |
每次请求重新获取 | 用户数据、实时仪表盘 |
| 增量 ISR | next: { revalidate: 60 } |
缓存 60 秒后重新验证 | 产品列表、价格页面 |
| 按需刷新 | next: { tags: ['x'] } |
缓存 + revalidateTag() 触发刷新 |
CMS 内容编辑后即时更新 |
Tom 选择策略的原则:根据数据变动的频率和时效性要求来决定。文章内容(变动频率低)用 SSG,评论数量(变动频率高但可以接受延迟)用 ISR 每 30 秒更新,用户头像(要求即时更新)用 SSR。合理混合使用这几种策略,可以在性能和实时性之间找到最佳平衡点。
▶ 示例 1:Server Component 三种缓存策略
// app/posts/page.tsx - 策略一:force-cache(默认 SSG)
// 数据只在构建时获取一次,后续请求全走 CDN 缓存
async function PostsPage() {
const posts = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // 等同于 SSG,默认行为
}).then(r => r.json())
return (
<div>
<h1>文章列表</h1>
{posts.map((post: any) => (
<article key={post.id} style={{ marginBottom: 16 }}>
<h2>{post.title}</h2>
<p>{post.body.slice(0, 100)}...</p>
</article>
))}
</div>
)
}
export default PostsPage
// app/dashboard/stats/page.tsx - 策略二:no-store(实时 SSR)
// 每次请求都从 API 获取最新数据
export const dynamic = 'force-dynamic'
async function StatsPage() {
const stats = await fetch('https://api.example.com/dashboard/stats', {
cache: 'no-store' // 每次都请求最新数据
}).then(r => r.json())
return (
<div>
<h1>实时统计</h1>
<p>在线用户:{stats.onlineUsers}</p>
<p>今日 PV:{stats.pageViews}</p>
<p>API 调用次数:{stats.apiCalls}</p>
</div>
)
}
export default StatsPage
// app/products/page.tsx - 策略三:revalidate(ISR 增量更新)
// 缓存 60 秒,60 秒后首次访问触发重新生成
async function ProductsPage() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 60 } // ISR:60 秒后重新验证
}).then(r => r.json())
return (
<div>
<h1>产品列表</h1>
{products.map((p: any) => (
<div key={p.id} style={{ border: '1px solid #ddd', padding: 12, marginBottom: 8, borderRadius: 8 }}>
<h3>{p.name}</h3>
<p>价格:${p.price}</p>
<p>库存:{p.stock > 0 ? '有货' : '缺货'}</p>
</div>
))}
</div>
)
}
export default ProductsPage
// app/api/revalidate/route.ts - On-Demand ISR 按需刷新
// 管理员编辑文章后调用这个 API 立即刷新缓存
import { revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const body = await request.json()
const { tag } = body
// 验证 secret 防止滥用
const secret = request.headers.get('x-revalidate-secret')
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
revalidateTag(tag) // 按标签刷新缓存
return Response.json({ revalidated: true, tag })
}
(2) Route Handler API 路由
Route Handler 是 App Router 中创建 API 端点的方式。在 app/api/ 目录下创建 route.ts 文件,导出一个命名函数(GET、POST、PUT、DELETE、PATCH),函数名对应 HTTP 方法。Route Handler 支持 Server Component 的所有特性——可以访问数据库、使用环境变量、控制缓存策略。
Tom 用 Route Handler 替代了之前博客中的 Express 后端。文章 CRUD、用户认证、评论系统都通过 Route Handler 暴露 API。结合 On-Demand ISR,编辑文章后可以立即刷新缓存,用户不需要等待 revalidate 时间。
Route Handler 也支持动态路由,在 app/api/products/[id]/route.ts 中通过 params 参数获取路径参数,适合 RESTful 风格的 API 设计。每个 Route Handler 也可以配置自己的缓存策略——例如 GET 请求可以设置 { next: { revalidate: 60 } } 实现 API 级别的缓存。
Route Handler 核心用法速查
| 文件路径 | HTTP 方法 | URL 端点 | 用途 |
|---|---|---|---|
app/api/posts/route.ts |
GET | /api/posts |
获取文章列表 |
app/api/posts/route.ts |
POST | /api/posts |
创建新文章 |
app/api/posts/[id]/route.ts |
GET | /api/posts/1 |
获取单篇文章 |
app/api/posts/[id]/route.ts |
PUT | /api/posts/1 |
更新文章 |
app/api/posts/[id]/route.ts |
DELETE | /api/posts/1 |
删除文章 |
app/api/auth/login/route.ts |
POST | /api/auth/login |
用户登录 |
app/api/revalidate/route.ts |
POST | /api/revalidate |
刷新 ISR 缓存 |
▶ 示例 2:博客 CRUD 完整 API
// app/api/posts/route.ts - 文章列表 API(GET)和创建文章 API(POST)
import { revalidateTag } from 'next/cache'
// 模拟数据库
const posts = [
{ id: 1, title: 'Next.js 入门', body: '本文介绍 Next.js 的基础用法...', published: true },
{ id: 2, title: 'React 19 新特性', body: 'React 19 带来了哪些变化...', published: true },
]
export async function GET() {
// 只返回已发布的文章
const published = posts.filter(p => p.published)
return Response.json(published)
}
export async function POST(request: Request) {
try {
const body = await request.json()
const newPost = {
id: posts.length + 1,
title: body.title,
body: body.body,
published: body.published ?? false,
createdAt: new Date().toISOString()
}
posts.push(newPost)
// 刷新文章列表的 ISR 缓存
revalidateTag('posts')
return Response.json(newPost, { status: 201 })
} catch (error) {
return Response.json({ error: 'Invalid request body' }, { status: 400 })
}
}
// app/api/posts/[id]/route.ts - 单篇文章操作(GET / PUT / DELETE)
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = posts.find(p => p.id === Number(params.id))
if (!post) {
return Response.json({ error: 'Post not found' }, { status: 404 })
}
return Response.json(post)
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const index = posts.findIndex(p => p.id === Number(params.id))
if (index === -1) {
return Response.json({ error: 'Post not found' }, { status: 404 })
}
posts[index] = { ...posts[index], ...body, id: Number(params.id) }
revalidateTag('posts')
return Response.json(posts[index])
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
const index = posts.findIndex(p => p.id === Number(params.id))
if (index === -1) {
return Response.json({ error: 'Post not found' }, { status: 404 })
}
posts.splice(index, 1)
revalidateTag('posts')
return Response.json({ message: 'Deleted' })
}
(3) Middleware 中间件与请求拦截
Middleware 是 Next.js 中一个强大的请求拦截机制。它在每个请求到达页面之前执行,可以用来处理重定向、鉴权、国际化路由、A/B 测试等场景。Middleware 运行在 Edge Runtime 中,具有极低的延迟(毫秒级)。
Tom 用 Middleware 实现了三个功能:第一,未登录用户访问仪表盘时重定向到登录页;第二,根据浏览器语言自动重定向到对应的语言版本;第三,阻止爬虫访问 API 路由。
Middleware 通过 matcher 配置匹配需要拦截的路由,避免不必要的执行开销。注意 Middleware 中不能读取 req.body,也不能使用 Node.js 原生 API——它运行在 Edge 环境。
▶ 示例 3:完整的 Middleware 鉴权系统
// middleware.ts - 项目根目录,与 app/ 平级
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// === 功能1:路由守卫 ===
// 未登录用户访问保护路由 → 重定向到登录页
const token = request.cookies.get('session_token')?.value
const protectedPaths = ['/dashboard', '/admin', '/profile']
if (!token && protectedPaths.some(path => pathname.startsWith(path))) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('redirect', pathname)
return NextResponse.redirect(loginUrl)
}
// === 功能2:已登录用户访问登录页 → 重定向到仪表盘 ===
if (token && pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
// === 功能3:国际化路由 ===
// 根据 Accept-Language 头自动跳转语言版本
const supportedLocales = ['zh', 'en', 'ja', 'pt']
const defaultLocale = 'zh'
// 检查 URL 是否已包含语言前缀
const pathnameHasLocale = supportedLocales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (!pathnameHasLocale) {
const acceptLanguage = request.headers.get('accept-language') || ''
const preferredLocale = supportedLocales.find(locale =>
acceptLanguage.startsWith(locale)
) || defaultLocale
return NextResponse.redirect(new URL(`/${preferredLocale}${pathname}`, request.url))
}
// === 功能4:设置响应头 ===
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
return response
}
// 配置 middleware 匹配路径(必填,否则所有路由都会触发)
export const config = {
matcher: [
// 匹配所有需要保护的路由
'/dashboard/:path*',
'/admin/:path*',
'/profile/:path*',
'/login',
// 匹配国际化路由
'/((?!api|_next/static|_next/image|favicon.ico).*)',
]
}
Middleware 配置说明
| 配置项 | 说明 | 示例 |
|---|---|---|
matcher |
匹配需要执行 middleware 的路由 | ['/dashboard/:path*', '/login'] |
request.nextUrl |
当前请求的 URL 对象 | 用于读取 pathname、searchParams |
request.cookies |
读取/操作 Cookie | request.cookies.get('token') |
NextResponse.redirect() |
重定向到指定 URL | 用于登录校验 |
NextResponse.next() |
继续正常处理请求 | 放行合法请求 |
response.headers.set() |
设置响应头 | 安全相关的响应头 |
Middleware 执行顺序与注意事项
Middleware 在每个匹配的请求上都会执行,因此性能至关重要。Edge Runtime 设计为微秒级执行,避免在其中做数据库查询或复杂计算。多个 Middleware 的执行顺序按 matcher 配置的顺序排列。如果某个 Middleware 返回了 redirect() 或 rewrite(),后续的 Middleware 不会执行。
一个重要注意事项:Middleware 中的环境变量需要通过 NEXT_PUBLIC_ 前缀暴露吗?不需要——Middleware 运行在服务端环境中,可以直接访问所有环境变量。但要注意,process.env 在 Middleware 编译时就被替换为实际值,所以不能在运行时动态读取环境变量。
4. 环境变量安全策略
Tom 在使用 Route Handler 和 Middleware 时还遇到了一个关键问题:环境变量的安全访问。Next.js 中环境变量的访问规则取决于前缀——这在数据获取和 API 开发中非常重要。
环境变量访问规则
| 前缀 | 可访问位置 | 说明 |
|---|---|---|
| 无前缀 | Server Component、Route Handler、Middleware | 仅在服务端可用,不会暴露到浏览器 |
NEXT_PUBLIC_ |
所有位置(包括浏览器) | 会被编译打包到 JS 中,不能放敏感信息 |
NEXT_PRIVATE_ |
仅在服务端可用 | 新增的明确标识,与无前缀行为一致 |
Tom 的经验法则:数据库连接字符串、API Secret Key、JWT 密钥等敏感信息——不加前缀,只在 Server Component 和 Route Handler 中使用。Google Analytics ID、Public API 地址等需要在前端使用的变量——加 NEXT_PUBLIC_ 前缀。
▶ 示例 4:ISR 增量静态再生——博客文章页
// app/blog/[slug]/page.tsx - ISR: 每 60 秒重新生成
interface BlogPost {
title: string
content: string
author: string
publishedAt: string
}
async function getPost(slug: string): Promise<BlogPost> {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 60 },
})
if (!res.ok) throw new Error('Post not found')
return res.json()
}
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post: { slug: string }) => ({ slug: post.slug }))
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
return (
<article style={{ maxWidth: 700, margin: '0 auto', padding: 24 }}>
<h1>{post.title}</h1>
<p style={{ color: '#999', fontSize: 14 }}>
By {post.author} · {new Date(post.publishedAt).toLocaleDateString()}
</p>
<div style={{ lineHeight: 1.8, marginTop: 16 }}>{post.content}</div>
</article>
)
}
▶ 示例 5:Server Actions——表单提交与数据变更
// app/actions.ts - Server Actions
'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
if (!title || !content) return
await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content, author: 'Alice' }),
})
revalidatePath('/blog')
redirect('/blog')
}
export async function deletePost(slug: string) {
await fetch(`https://api.example.com/posts/${slug}`, { method: 'DELETE' })
revalidatePath('/blog')
}
// app/blog/new/page.tsx - 新建文章表单
function NewPostPage() {
return (
<div style={{ maxWidth: 600, margin: '0 auto', padding: 24 }}>
<h1>New Post</h1>
<form action={createPost} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<input name="title" placeholder="Title" required style={{ padding: 8, borderRadius: 4 }} />
<textarea name="content" placeholder="Content..." rows={8} required style={{ padding: 8, borderRadius: 4 }} />
<button type="submit" style={{ padding: 10, background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>
Publish
</button>
</form>
</div>
)
}
export default NewPostPage
❓ 常见问题
req.body、使用 Node.js 内置模块(fs、path)、访问数据库、使用 fetch 之外的外部请求。复杂的鉴权逻辑建议在 Route Handler 中完成。revalidate: 60 后,页面最多 60 秒后更新。On-Demand ISR 是事件驱动的刷新——调用 revalidateTag() 或 revalidatePath() 后立即更新,适合编辑内容后需要即时生效的场景。两者可以结合使用:设一个较长的 revalidate 时间作为兜底,同时通过 On-Demand ISR 在内容变化时立即刷新。Access-Control-Allow-Origin 等 CORS 头即可。你可以封装一个工具函数 corsHeaders() 返回统一的 CORS 响应头对象,在每个 Route Handler 中通过 Response.json(data, { headers: corsHeaders() }) 带上。如果前后端在同一域名下(Next.js 全栈部署通常如此),不需要处理 CORS。📖 小节
- Server Component 直接
await fetch获取数据,支持三种缓存策略:force-cache(SSG)、no-store(SSR)、next.revalidate(ISR) - Route Handler 在
app/api/目录下的route.ts文件中定义,导出的函数名对应 HTTP 方法(GET/POST/PUT/DELETE) - Route Handler 支持动态路由(
[id])、请求体解析、响应状态码控制、自定义缓存策略 - ISR 通过
next.revalidate配置定时重新验证,通过next.tags+revalidateTag()实现 On-Demand 按需刷新 - Middleware 在请求到达页面之前执行,运行在 Edge Runtime,适合路由守卫、国际化、重定向、安全头设置等场景
matcher配置控制 Middleware 的执行范围,避免不必要的性能开销,可以精确匹配或排除特定路由- On-Demand ISR 适合内容编辑后即时刷新的场景,通过
revalidateTag或revalidatePath触发,无需等待定时到期 - 数据获取策略的选择原则:根据数据变动频率和时效性要求决定 SSG/ISR/SSR 的混合使用方案
middleware.ts的matcher配置必须精确,避免匹配到_next/static、favicon.ico等静态资源revalidateTag和revalidatePath是实现 On-Demand ISR 的两个核心 API,在 Route Handler 或 Server Action 中调用- 本课所有的数据获取能力都是后几课(部署 CI/CD、组件库集成、综合项目)的技术基础
📝 作业
- 在项目中创建
app/api/products/route.ts,实现一个模拟商品 API——GET 返回商品列表,POST 创建新商品。在app/products/page.tsx中用 Server Component 的fetch调用这个 API 展示商品列表,并设置revalidate: 30实现 ISR。验证 30 秒内修改商品数据后页面不会更新,30 秒后刷新页面会看到新数据。 - 创建
middleware.ts,保护/dashboard/*路由——如果 Cookie 中没有session_token,则重定向到/login。同时为已登录用户访问/login时自动跳转到/dashboard。使用matcher精确配置只匹配需要的路由,避免 middleware 在_next/static等资源路径上执行。 - 实现一个 On-Demand ISR 刷新机制:在管理员页面中点击"刷新缓存"按钮,调用
POST /api/revalidate(带x-revalidate-secret请求头验证),通过revalidateTag('products')刷新商品页面的 ISR 缓存。验证刷新后页面是否立即更新,对比定时 ISR 和 On-Demand ISR 的刷新速度差异。