Next.js 增量静态再生 (ISR) 🔄
🎯 概述
增量静态再生(Incremental Static Regeneration,ISR)允许你在构建后更新静态页面,无需重新构建整个网站,结合了静态生成和服务端渲染的优势。
📦 什么是 ISR?
ISR 工作原理
- 首次请求返回缓存的静态页面
- 后台触发页面重新生成
- 新页面生成后替换旧缓存
- 后续请求获得更新的页面
ISR 优势
- ⚡ 静态页面的快速响应
- 🔄 内容可以定期更新
- 💰 降低服务器负载
- 🎯 按需更新特定页面
- 📈 支持大规模网站
🚀 基本用法
App Router ISR
tsx
// app/posts/[id]/page.tsx
async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
next: { revalidate: 60 } // 每60秒重新验证
})
return res.json()
}
export default async function Post({ params }: { params: { id: string } }) {
const post = await getPost(params.id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}Pages Router ISR
tsx
// pages/posts/[id].tsx
export async function getStaticProps({ params }) {
const post = await fetchPost(params.id)
return {
props: { post },
revalidate: 60, // 每60秒重新生成
}
}
export async function getStaticPaths() {
return {
paths: [],
fallback: 'blocking',
}
}
export default function Post({ post }) {
return <article>{post.title}</article>
}📝 重新验证策略
基于时间的重新验证
tsx
// 每10秒重新验证
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 10 }
})
return res.json()
}
// 每小时重新验证
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
})
return res.json()
}
// 每天重新验证
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 86400 }
})
return res.json()
}按需重新验证
tsx
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret')
// 验证密钥
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ message: '无效的密钥' }, { status: 401 })
}
const path = request.nextUrl.searchParams.get('path')
const tag = request.nextUrl.searchParams.get('tag')
try {
if (path) {
revalidatePath(path)
return NextResponse.json({ revalidated: true, path })
}
if (tag) {
revalidateTag(tag)
return NextResponse.json({ revalidated: true, tag })
}
return NextResponse.json({ message: '缺少参数' }, { status: 400 })
} catch (error) {
return NextResponse.json({ message: '重新验证失败' }, { status: 500 })
}
}触发重新验证:
bash
# 重新验证特定路径
curl -X POST "http://localhost:3000/api/revalidate?secret=MY_SECRET&path=/posts/1"
# 重新验证带标签的内容
curl -X POST "http://localhost:3000/api/revalidate?secret=MY_SECRET&tag=posts"🎨 缓存标签
使用标签
tsx
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: {
revalidate: 3600,
tags: ['posts'] // 添加标签
}
})
return res.json()
}
export default async function Posts() {
const posts = await getPosts()
return <div>{/* ... */}</div>
}重新验证标签
tsx
// app/api/posts/route.ts
import { revalidateTag } from 'next/cache'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const post = await request.json()
// 创建新文章
await createPost(post)
// 重新验证所有带 'posts' 标签的页面
revalidateTag('posts')
return NextResponse.json({ success: true })
}🔄 路径重新验证
重新验证特定路径
tsx
// app/api/posts/[id]/route.ts
import { revalidatePath } from 'next/cache'
import { NextResponse } from 'next/server'
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const data = await request.json()
// 更新文章
await updatePost(params.id, data)
// 重新验证文章页面
revalidatePath(`/posts/${params.id}`)
// 也重新验证文章列表
revalidatePath('/posts')
return NextResponse.json({ success: true })
}重新验证布局
tsx
import { revalidatePath } from 'next/cache'
// 重新验证特定布局下的所有页面
revalidatePath('/blog', 'layout')
// 重新验证特定页面
revalidatePath('/blog/post-1', 'page')📊 实战示例
博客系统
tsx
// app/blog/[slug]/page.tsx
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: {
revalidate: 3600, // 1小时
tags: ['posts', `post-${slug}`]
}
})
return res.json()
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
return (
<article>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
// app/api/posts/[slug]/route.ts
import { revalidateTag, revalidatePath } from 'next/cache'
export async function PUT(request, { params }) {
const data = await request.json()
// 更新文章
await updatePost(params.slug, data)
// 重新验证这篇文章
revalidateTag(`post-${params.slug}`)
// 重新验证文章列表
revalidatePath('/blog')
return Response.json({ success: true })
}电商产品页面
tsx
// app/products/[id]/page.tsx
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: {
revalidate: 300, // 5分钟
tags: ['products', `product-${id}`]
}
})
return res.json()
}
async function getReviews(productId: string) {
const res = await fetch(`https://api.example.com/products/${productId}/reviews`, {
next: {
revalidate: 60, // 1分钟
tags: [`reviews-${productId}`]
}
})
return res.json()
}
export default async function Product({ params }) {
const [product, reviews] = await Promise.all([
getProduct(params.id),
getReviews(params.id),
])
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}元</p>
<div>
{reviews.map(review => (
<div key={review.id}>{review.comment}</div>
))}
</div>
</div>
)
}
// app/api/reviews/route.ts
import { revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const review = await request.json()
// 创建评论
await createReview(review)
// 只重新验证该产品的评论
revalidateTag(`reviews-${review.productId}`)
return Response.json({ success: true })
}新闻网站
tsx
// app/news/page.tsx
async function getNews() {
const res = await fetch('https://api.example.com/news', {
next: {
revalidate: 60, // 1分钟
tags: ['news']
}
})
return res.json()
}
export default async function News() {
const articles = await getNews()
return (
<div>
{articles.map(article => (
<article key={article.id}>
<h2>{article.title}</h2>
<p>{article.summary}</p>
</article>
))}
</div>
)
}
// Webhook 接收新文章通知
// app/api/webhooks/news/route.ts
import { revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const { secret, article } = await request.json()
if (secret !== process.env.WEBHOOK_SECRET) {
return Response.json({ error: '未授权' }, { status: 401 })
}
// 重新验证新闻页面
revalidateTag('news')
return Response.json({ revalidated: true })
}📚 最佳实践
1. 选择合适的重新验证时间
tsx
// 静态内容 - 长时间
const staticContent = await fetch('url', {
next: { revalidate: 86400 } // 24小时
})
// 半静态内容 - 中等时间
const semiStatic = await fetch('url', {
next: { revalidate: 3600 } // 1小时
})
// 动态内容 - 短时间
const dynamic = await fetch('url', {
next: { revalidate: 60 } // 1分钟
})
// 实时内容 - 不缓存
const realtime = await fetch('url', {
cache: 'no-store'
})2. 使用标签组织缓存
tsx
// 按类型标记
async function getPosts() {
const res = await fetch('url', {
next: { tags: ['posts', 'content'] }
})
return res.json()
}
// 按ID标记
async function getPost(id: string) {
const res = await fetch(`url/${id}`, {
next: { tags: ['posts', `post-${id}`] }
})
return res.json()
}
// 重新验证时可以选择性更新
revalidateTag('posts') // 更新所有文章
revalidateTag('post-123') // 只更新特定文章3. 错误处理
tsx
async function getData() {
try {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
if (!res.ok) {
throw new Error('获取失败')
}
return res.json()
} catch (error) {
console.error('数据获取错误:', error)
// 返回缓存的数据或默认值
return getCachedData()
}
}4. 监控和日志
tsx
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
export async function POST(request: Request) {
const { path } = await request.json()
console.log(`[ISR] 重新验证路径: ${path}`)
try {
revalidatePath(path)
console.log(`[ISR] 成功重新验证: ${path}`)
return Response.json({ success: true })
} catch (error) {
console.error(`[ISR] 重新验证失败: ${path}`, error)
return Response.json({ error: '重新验证失败' }, { status: 500 })
}
}🔗 相关资源
下一步:学习 Next.js 客户端渲染 (CSR)。