在构建一个现代博客系统时,性能与开发体验往往是一对矛盾。
传统动态网站:
用户访问
↓
服务器查询数据库
↓
生成页面
↓
返回 HTML虽然数据实时,但是每一次访问都需要服务器计算。
而纯静态网站:
next build
↓
生成 HTML
↓
部署 CDN拥有极快的访问速度,但是每次内容更新都需要重新构建整个项目。
对于博客、文档站这类内容驱动型网站来说,我们希望同时拥有:
这也是我最终选择:
Next.js ISR + Webhook + GitHub Actions
的原因。
最开始,我设计了一套看起来很合理的流程:
Pages CMS
↓
GitHub Commit
↓
GitHub Webhook
↓
Vercel API Route
↓
syncPosts()
↓
读取 content/posts
↓
更新数据库
↓
revalidateTag()理论上:
CMS 修改文章后:
但是实际运行后发现:
无论怎么修改文章,Webhook 触发之后始终返回:
no content changed刚开始我以为是同步逻辑的问题。
但是进一步排查后发现:
真正的问题在于:
Vercel Runtime 并不是 Git 仓库。
很多人第一次使用 Serverless 平台时容易产生一个误区:
GitHub 更新了代码,Vercel Function 里面的文件也会自动更新。
实际上并不是这样。
Vercel 的部署模型:
GitHub Repository
↓
Build
↓
Immutable Deployment
↓
Serverless Function一次部署生成的是一个不可变快照。
例如:
第一次部署:
GitHub main
content/posts
├── hello-world.mdx
└── nextjs.mdx
↓
Vercel Build
↓
Deployment A
↓
Function Runtime此时:
Function 中携带的是:
hello-world.mdx
nextjs.mdx之后:
GitHub:
content/posts
├── hello-world.mdx
├── nextjs.mdx
└── new-post.mdx发生变化。
但是:
GitHub Repository
↓
新的 commit不会自动修改:
Deployment A
↓
Function Runtime所以:
syncPosts()读取到的依然是:
旧文章列表最终自然得到:
no content changed发现问题之后,我重新调整了架构。
新的流程:
Pages CMS
↓
GitHub Commit
↓
GitHub Actions
↓
checkout 最新代码
↓
执行同步脚本
↓
更新数据库
↓
调用 Next.js API
↓
revalidateTag()
↓
ISR 更新缓存核心变化:
GitHub Actions 负责获取最新代码。
因为 Actions 才是真正拥有 Git 仓库工作区的环境。
很多人会认为:
GitHub Actions 只适合重新构建项目。
其实并不是。
Actions 本质上就是一个 CI Runner。
它可以:
因此:
这里不需要:
GitHub Push
↓
Vercel Build而是:
GitHub Push
↓
Actions
↓
同步数据
↓
触发 ISR避免了:
ISR:
Incremental Static Regeneration
中文:
增量静态再生。
它的目标:
在保持静态页面性能的同时,让页面可以动态更新。
服务端渲染:
用户请求
↓
服务器查询数据
↓
生成 HTML
↓
返回浏览器优点:
缺点:
静态生成:
next build
↓
读取数据
↓
生成 HTML访问:
用户
↓
CDN
↓
HTML优点:
但是:
内容更新:
修改文章
↓
重新 build
↓
重新部署对于博客并不方便。
ISR 可以理解为:
让静态页面拥有重新生成能力。
流程:
第一次访问:
用户请求
↓
生成页面
↓
缓存之后:
用户请求
↓
读取缓存当内容发生变化:
触发重新验证
↓
旧缓存失效
↓
下一次请求重新生成整个过程:
不需要重新部署整个网站。
在新版 Next.js 中,我使用:
Cache Components 模式
代替传统:
export const revalidate = 3600方式。
例如:
import { cacheLife, cacheTag } from 'next/cache'
export async function getPosts() {
'use cache'
cacheTag('posts')
cacheLife('weeks')
return await findPosts()
}'use cache'表示:
这个函数返回值应该被缓存。
cacheTag('posts')为缓存添加标签。
例如:
posts
├── 首页文章列表
├── RSS
└── 文章归档之后:
revalidateTag('posts')即可刷新所有相关缓存。
cacheLife('weeks')表示:
缓存生命周期。
博客内容通常不会每分钟变化,所以可以设置较长缓存。
但是:
我们仍然需要一种主动刷新机制。
这就是 Webhook。
Webhook 是一种:
事件驱动通知机制。
传统 API:
你的程序
↓
主动请求
↓
第三方服务例如:
每分钟询问:
有没有新文章?这种叫:
Polling(轮询)
缺点:
Webhook:
内容变化
↓
主动发送 HTTP 请求
↓
通知目标系统例如:
CMS
文章更新
↓
Webhook
↓
你的服务最终架构:
Pages CMS
↓
GitHub Commit
↓
GitHub Actions
↓
syncPosts()
↓
数据库更新
↓
POST /api/revalidate
↓
revalidateTag()
↓
ISR Cache 更新Route Handler:
app/api/revalidate/posts/route.ts
示例:
import { revalidateTag } from 'next/cache'
export async function POST(req: Request) {
const auth = req.headers.get('authorization')
if (auth !== `Bearer ${process.env.WEBHOOK_SECRET}`) {
return new Response('Unauthorized', {
status: 401,
})
}
revalidateTag('posts', 'max')
return Response.json({
success: true,
})
}Webhook 本质上是公开接口。
如果没有保护:
任何人都可以:
POST /api/revalidate/posts导致:
因此需要:
Authorization:
Bearer xxx进行验证。
Webhook 本身只负责通知:
有内容变化。
它不会告诉系统:
哪篇文章变化。
因此:
收到通知后,需要执行同步逻辑:
syncPosts()例如:
返回:
;[
{
type: 'update',
slug: 'nextjs-isr',
},
{
type: 'delete',
slug: 'old-post',
},
]然后:
根据变化精准刷新缓存。
博客通常包含:
如果每次:
revalidatePath('/')会导致大量页面重新计算。
所以使用:
Tag Cache。
例如:
posts
↓
文章列表
post-nextjs-isr
↓
单篇文章更新:
revalidateTag('post-nextjs-isr')只影响对应内容。
┌──────────────┐
│ Pages CMS │
└──────┬───────┘
│
│ Git Commit
↓
┌──────────────┐
│ GitHub Action│
└──────┬───────┘
│
│ syncPosts()
↓
┌──────────────┐
│ Database │
└──────┬───────┘
│
│ Revalidate API
↓
┌──────────────┐
│ Next.js │
└──────┬───────┘
│
revalidateTag()
↓
┌──────────────┐
│ ISR Cache │
└──────┬───────┘
↓
最新博客页面这次踩坑让我重新理解了 Serverless 平台的运行机制。
ISR 解决:
如何让静态页面拥有动态更新能力。
Webhook 解决:
如何让系统知道什么时候需要更新。
GitHub Actions 解决:
如何获取最新代码并执行同步任务。
最终:
形成了一套:
自动化、高性能、低成本的现代博客架构。
对于个人博客、文档站、知识库这类内容驱动型网站来说:
ISR + Webhook + CI Workflow
是一套非常优雅的解决方案。
它让我可以像使用动态网站一样维护博客,同时享受到静态网站接近 CDN 的访问速度。