Next.js 面试题详细答案 - Q10
Q10: 如何实现按需重新验证(On-demand Revalidation)?即如何在 API Route 或特定事件后手动清除特定路径的缓存?(revalidatePath, revalidateTag)
按需重新验证概述
按需重新验证允许在特定事件(如数据更新、用户操作)后手动清除缓存,确保用户看到最新数据。
1. revalidatePath - 路径重新验证
基本用法
// app/api/posts/route.js
import { revalidatePath } from 'next/cache'
export async function POST(request) {
const newPost = await request.json()
// 创建新文章
const post = await createPost(newPost)
// 重新验证博客列表页
revalidatePath('/blog')
return Response.json(post)
}
// 重新验证特定页面
export async function PUT(request) {
const { id, slug, ...updates } = await request.json()
const post = await updatePost(id, updates)
// 重新验证文章详情页
revalidatePath(`/blog/${slug}`)
return Response.json(post)
}
重新验证类型
// 重新验证页面
revalidatePath('/blog') // 重新验证 /blog 页面
// 重新验证布局
revalidatePath('/blog', 'layout') // 重新验证 /blog 布局
// 重新验证页面(默认)
revalidatePath('/blog', 'page') // 重新验证 /blog 页面
// 重新验证所有相关路径
revalidatePath('/blog/[slug]', 'page') // 重新验证所有 /blog/[slug] 页面
实际应用示例
// app/api/products/route.js
import { revalidatePath } from 'next/cache'
// 创建产品
export async function POST(request) {
const productData = await request.json()
try {
const product = await createProduct(productData)
// 重新验证产品相关页面
revalidatePath('/products') // 产品列表页
revalidatePath('/') // 首页(可能显示最新产品)
return Response.json(product)
} catch (error) {
return Response.json({ error: 'Failed to create product' }, { status: 500 })
}
}
// 更新产品
export async function PUT(request) {
const { id, ...updates } = await request.json()
try {
const product = await updateProduct(id, updates)
// 重新验证相关页面
revalidatePath('/products') // 产品列表
revalidatePath(`/products/${product.slug}`) // 产品详情
revalidatePath('/') // 首页
return Response.json(product)
} catch (error) {
return Response.json({ error: 'Failed to update product' }, { status: 500 })
}
}
// 删除产品
export async function DELETE(request) {
const { id } = await request.json()
try {
const product = await deleteProduct(id)
// 重新验证相关页面
revalidatePath('/products')
revalidatePath(`/products/${product.slug}`)
revalidatePath('/')
return Response.json({ success: true })
} catch (error) {
return Response.json({ error: 'Failed to delete product' }, { status: 500 })
}
}
2. revalidateTag - 标签重新验证
基本用法
// app/api/posts/route.js
import { revalidateTag } from 'next/cache'
export async function POST(request) {
const newPost = await request.json()
// 创建新文章
const post = await createPost(newPost)
// 重新验证所有带有 'posts' 标签的缓存
revalidateTag('posts')
return Response.json(post)
}
// 重新验证多个标签
export async function PUT(request) {
const { id, ...updates } = await request.json()
const post = await updatePost(id, updates)
// 重新验证多个标签
revalidateTag('posts')
revalidateTag(`post-${id}`)
revalidateTag('featured-posts')
return Response.json(post)
}
标签缓存策略
// 使用标签缓存数据
async function BlogPost({ slug }) {
const post = await fetch(`https://api.example.com/posts/${slug}`, {
next: {
revalidate: 3600,
tags: ['posts', `post-${slug}`]
},
})
return <article>{post.content}</article>
}
async function BlogList() {
const posts = await fetch('https://api.example.com/posts', {
next: {
revalidate: 1800,
tags: ['posts']
},
})
return <PostList posts={posts} />
}
// API 中重新验证标签
export async function POST(request) {
const newPost = await request.json()
const post = await createPost(newPost)
// 重新验证所有相关标签
revalidateTag('posts') // 重新验证所有文章
revalidateTag('featured-posts') // 重新验证精选文章
return Response.json(post)
}
3. 组合使用 revalidatePath 和 revalidateTag
// app/api/posts/route.js
import { revalidatePath, revalidateTag } from 'next/cache'
export async function POST(request) {
const newPost = await request.json()
try {
const post = await createPost(newPost)
// 同时使用路径和标签重新验证
revalidatePath('/blog') // 重新验证博客列表页
revalidatePath('/') // 重新验证首页
revalidateTag('posts') // 重新验证所有文章缓存
revalidateTag('featured-posts') // 重新验证精选文章
return Response.json(post)
} catch (error) {
return Response.json({ error: 'Failed to create post' }, { status: 500 })
}
}
export async function PUT(request) {
const { id, slug, ...updates } = await request.json()
try {
const post = await updatePost(id, updates)
// 重新验证相关路径和标签
revalidatePath('/blog')
revalidatePath(`/blog/${slug}`)
revalidateTag('posts')
revalidateTag(`post-${id}`)
return Response.json(post)
} catch (error) {
return Response.json({ error: 'Failed to update post' }, { status: 500 })
}
}
4. 外部事件触发重新验证
Webhook 处理
// app/api/webhooks/content/route.js
import { revalidateTag } from 'next/cache'
export async function POST(request) {
const { type, data } = await request.json()
// 验证 webhook 签名
const signature = request.headers.get('x-webhook-signature')
if (!verifyWebhookSignature(signature, data)) {
return Response.json({ error: 'Invalid signature' }, { status: 401 })
}
try {
switch (type) {
case 'post.created':
revalidateTag('posts')
revalidateTag('featured-posts')
break
case 'post.updated':
revalidateTag('posts')
revalidateTag(`post-${data.id}`)
break
case 'post.deleted':
revalidateTag('posts')
revalidateTag(`post-${data.id}`)
break
case 'user.updated':
revalidateTag('users')
revalidateTag(`user-${data.id}`)
break
default:
console.log('Unknown webhook type:', type)
}
return Response.json({ success: true })
} catch (error) {
return Response.json({ error: 'Failed to revalidate' }, { status: 500 })
}
}
定时任务触发
// app/api/cron/revalidate/route.js
import { revalidateTag } from 'next/cache'
export async function GET(request) {
// 验证 cron 请求
const authHeader = request.headers.get('authorization')
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
// 重新验证所有缓存
revalidateTag('posts')
revalidateTag('products')
revalidateTag('analytics')
return Response.json({
success: true,
message: 'Cache revalidated successfully'
})
} catch (error) {
return Response.json({ error: 'Failed to revalidate' }, { status: 500 })
}
}
5. 条件重新验证
// app/api/posts/route.js
import { revalidatePath, revalidateTag } from 'next/cache'
export async function PUT(request) {
const { id, slug, ...updates } = await request.json()
try {
const post = await updatePost(id, updates)
// 根据更新内容决定重新验证策略
if (updates.featured) {
// 如果更新了精选状态,重新验证首页和精选页面
revalidatePath('/')
revalidatePath('/featured')
revalidateTag('featured-posts')
}
if (updates.published) {
// 如果更新了发布状态,重新验证博客列表
revalidatePath('/blog')
revalidateTag('posts')
}
// 总是重新验证文章详情页
revalidatePath(`/blog/${slug}`)
revalidateTag(`post-${id}`)
return Response.json(post)
} catch (error) {
return Response.json({ error: 'Failed to update post' }, { status: 500 })
}
}
6. 批量重新验证
// app/api/bulk-revalidate/route.js
import { revalidateTag } from 'next/cache'
export async function POST(request) {
const { tags, paths } = await request.json()
try {
// 批量重新验证标签
if (tags && Array.isArray(tags)) {
for (const tag of tags) {
revalidateTag(tag)
}
}
// 批量重新验证路径
if (paths && Array.isArray(paths)) {
for (const path of paths) {
revalidatePath(path)
}
}
return Response.json({
success: true,
revalidated: { tags, paths }
})
} catch (error) {
return Response.json({ error: 'Failed to revalidate' }, { status: 500 })
}
}
7. 错误处理和监控
// app/api/posts/route.js
import { revalidatePath, revalidateTag } from 'next/cache'
export async function POST(request) {
const newPost = await request.json()
try {
const post = await createPost(newPost)
// 重新验证缓存
try {
revalidatePath('/blog')
revalidateTag('posts')
// 记录重新验证成功
console.log('Cache revalidated successfully')
} catch (revalidateError) {
// 记录重新验证失败,但不影响主流程
console.error('Cache revalidation failed:', revalidateError)
}
return Response.json(post)
} catch (error) {
return Response.json({ error: 'Failed to create post' }, { status: 500 })
}
}
8. 实际应用场景
电商系统
// app/api/products/route.js
import { revalidatePath, revalidateTag } from 'next/cache'
export async function POST(request) {
const productData = await request.json()
try {
const product = await createProduct(productData)
// 重新验证相关页面
revalidatePath('/products')
revalidatePath('/')
revalidateTag('products')
revalidateTag('featured-products')
return Response.json(product)
} catch (error) {
return Response.json({ error: 'Failed to create product' }, { status: 500 })
}
}
export async function PUT(request) {
const { id, ...updates } = await request.json()
try {
const product = await updateProduct(id, updates)
// 根据更新内容重新验证
if (updates.price || updates.stock) {
revalidatePath(`/products/${product.slug}`)
revalidateTag(`product-${id}`)
}
if (updates.featured) {
revalidatePath('/')
revalidateTag('featured-products')
}
revalidatePath('/products')
revalidateTag('products')
return Response.json(product)
} catch (error) {
return Response.json({ error: 'Failed to update product' }, { status: 500 })
}
}
最佳实践
1. 合理使用重新验证
// 只在必要时重新验证
export async function POST(request) {
const newPost = await request.json()
const post = await createPost(newPost)
// 只重新验证真正受影响的页面
if (post.published) {
revalidatePath('/blog')
revalidateTag('posts')
}
return Response.json(post)
}
2. 错误处理
// 重新验证失败不影响主流程
export async function PUT(request) {
const { id, ...updates } = await request.json()
try {
const post = await updatePost(id, updates)
// 重新验证缓存
try {
revalidatePath(`/blog/${post.slug}`)
revalidateTag(`post-${id}`)
} catch (revalidateError) {
console.error('Cache revalidation failed:', revalidateError)
}
return Response.json(post)
} catch (error) {
return Response.json({ error: 'Failed to update post' }, { status: 500 })
}
}
总结
按需重新验证的关键点:
- revalidatePath:重新验证特定路径的缓存
- revalidateTag:重新验证特定标签的缓存
- 组合使用:路径和标签可以同时使用
- 外部触发:支持 webhook、定时任务等外部事件
- 条件重新验证:根据更新内容决定重新验证策略
- 错误处理:重新验证失败不应影响主流程
这些功能让开发者能够精确控制缓存失效,确保用户始终看到最新数据。