Q10: 如何实现按需重新验证(On-demand Revalidation)?即如何在 API Route 或特定事件后手动清除特定路径的缓存?(revali

27 阅读4分钟

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 })
  }
}

总结

按需重新验证的关键点:

  1. revalidatePath:重新验证特定路径的缓存
  2. revalidateTag:重新验证特定标签的缓存
  3. 组合使用:路径和标签可以同时使用
  4. 外部触发:支持 webhook、定时任务等外部事件
  5. 条件重新验证:根据更新内容决定重新验证策略
  6. 错误处理:重新验证失败不应影响主流程

这些功能让开发者能够精确控制缓存失效,确保用户始终看到最新数据。