返回博客列表
ISRNext.js博客Webhooks10 分钟

探秘博客构建的秘诀:ISR 增量静态再生与自动化内容更新

在构建一个现代博客系统时,性能与开发体验往往是一对矛盾。

传统动态网站:

用户访问
 ↓
服务器查询数据库
 ↓
生成页面
 ↓
返回 HTML

虽然数据实时,但是每一次访问都需要服务器计算。

而纯静态网站:

next build
 ↓
生成 HTML
 ↓
部署 CDN

拥有极快的访问速度,但是每次内容更新都需要重新构建整个项目。

对于博客、文档站这类内容驱动型网站来说,我们希望同时拥有:

  • 静态页面的极致性能;
  • 动态内容的快速更新;
  • 无需手动重新部署。

这也是我最终选择:

Next.js ISR + Webhook + GitHub Actions

的原因。

一次失败的 Webhook 架构

最开始,我设计了一套看起来很合理的流程:

Pages CMS

    ↓

GitHub Commit

    ↓

GitHub Webhook

    ↓

Vercel API Route

    ↓

syncPosts()

    ↓

读取 content/posts

    ↓

更新数据库

    ↓

revalidateTag()

理论上:

CMS 修改文章后:

  1. 提交 Markdown 文件;
  2. GitHub 通知 Vercel;
  3. Vercel 同步最新文章;
  4. 刷新 ISR 缓存。

但是实际运行后发现:

无论怎么修改文章,Webhook 触发之后始终返回:

no content changed

刚开始我以为是同步逻辑的问题。

但是进一步排查后发现:

真正的问题在于:

Vercel Runtime 并不是 Git 仓库。

Vercel Deployment 的核心机制

很多人第一次使用 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 为什么适合这个场景?

很多人会认为:

GitHub Actions 只适合重新构建项目。

其实并不是。

Actions 本质上就是一个 CI Runner。

它可以:

  • 拉取代码;
  • 执行脚本;
  • 调用 API;
  • 操作数据库。

因此:

这里不需要:

GitHub Push

↓

Vercel Build

而是:

GitHub Push

↓

Actions

↓

同步数据

↓

触发 ISR

避免了:

  • 不必要的重新部署;
  • 浪费构建时间;
  • 增加部署成本。

什么是 ISR?

ISR:

Incremental Static Regeneration

中文:

增量静态再生。

它的目标:

在保持静态页面性能的同时,让页面可以动态更新。

SSR:每次请求生成页面

服务端渲染:

用户请求

 ↓

服务器查询数据

 ↓

生成 HTML

 ↓

返回浏览器

优点:

  • 数据实时。

缺点:

  • 每次访问都有计算成本。

SSG:构建时生成页面

静态生成:

next build

 ↓

读取数据

 ↓

生成 HTML

访问:

用户

 ↓

CDN

 ↓

HTML

优点:

  • 极快;
  • 几乎没有服务器压力。

但是:

内容更新:

修改文章

 ↓

重新 build

 ↓

重新部署

对于博客并不方便。

ISR:静态页面的局部更新

ISR 可以理解为:

让静态页面拥有重新生成能力。

流程:

第一次访问:

用户请求

 ↓

生成页面

 ↓

缓存

之后:

用户请求

 ↓

读取缓存

当内容发生变化:

触发重新验证

 ↓

旧缓存失效

 ↓

下一次请求重新生成

整个过程:

不需要重新部署整个网站。

Next.js 中使用 ISR

在新版 Next.js 中,我使用:

Cache Components 模式

代替传统:

ts
export const revalidate = 3600

方式。

例如:

ts
import { cacheLife, cacheTag } from 'next/cache'

export async function getPosts() {
  'use cache'

  cacheTag('posts')

  cacheLife('weeks')

  return await findPosts()
}

use cache

ts
'use cache'

表示:

这个函数返回值应该被缓存。

cacheTag

ts
cacheTag('posts')

为缓存添加标签。

例如:

posts

 ├── 首页文章列表

 ├── RSS

 └── 文章归档

之后:

ts
revalidateTag('posts')

即可刷新所有相关缓存。

cacheLife

ts
cacheLife('weeks')

表示:

缓存生命周期。

博客内容通常不会每分钟变化,所以可以设置较长缓存。

但是:

我们仍然需要一种主动刷新机制。

这就是 Webhook。

什么是 Webhook?

Webhook 是一种:

事件驱动通知机制。

传统 API:

你的程序

 ↓

主动请求

 ↓

第三方服务

例如:

每分钟询问:

有没有新文章?

这种叫:

Polling(轮询)

缺点:

  • 浪费请求;
  • 有延迟。

Webhook:

内容变化

 ↓

主动发送 HTTP 请求

 ↓

通知目标系统

例如:

CMS

文章更新

 ↓

Webhook

 ↓

你的服务

我的博客如何使用 Webhook?

最终架构:

Pages CMS

    ↓

GitHub Commit

    ↓

GitHub Actions

    ↓

syncPosts()

    ↓

数据库更新

    ↓

POST /api/revalidate

    ↓

revalidateTag()

    ↓

ISR Cache 更新

Next.js 接收刷新请求

Route Handler:

app/api/revalidate/posts/route.ts

示例:

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 本质上是公开接口。

如果没有保护:

任何人都可以:

bash
POST /api/revalidate/posts

导致:

  • 无限刷新缓存;
  • 消耗服务器资源;
  • 影响网站稳定性。

因此需要:

Authorization:

Bearer xxx

进行验证。

为什么需要同步文章?

Webhook 本身只负责通知:

有内容变化。

它不会告诉系统:

哪篇文章变化。

因此:

收到通知后,需要执行同步逻辑:

ts
syncPosts()

例如:

返回:

ts
;[
  {
    type: 'update',
    slug: 'nextjs-isr',
  },
  {
    type: 'delete',
    slug: 'old-post',
  },
]

然后:

根据变化精准刷新缓存。

精准失效,而不是全部刷新

博客通常包含:

  • 首页列表;
  • 标签页面;
  • 分类页面;
  • 单篇文章;
  • RSS。

如果每次:

ts
revalidatePath('/')

会导致大量页面重新计算。

所以使用:

Tag Cache。

例如:

posts

 ↓

文章列表


post-nextjs-isr

 ↓

单篇文章

更新:

ts
revalidateTag('post-nextjs-isr')

只影响对应内容。

最终架构

┌──────────────┐
                 │  Pages CMS   │
                 └──────┬───────┘
                        │
                        │ Git Commit
                        ↓

                 ┌──────────────┐
                 │ GitHub Action│
                 └──────┬───────┘
                        │
                        │ syncPosts()
                        ↓

                 ┌──────────────┐
                 │  Database    │
                 └──────┬───────┘
                        │
                        │ Revalidate API
                        ↓

                 ┌──────────────┐
                 │   Next.js    │
                 └──────┬───────┘
                        │

                  revalidateTag()

                        ↓

                 ┌──────────────┐
                 │ ISR Cache    │
                 └──────┬───────┘

                        ↓

                 最新博客页面

总结

这次踩坑让我重新理解了 Serverless 平台的运行机制。

ISR 解决:

如何让静态页面拥有动态更新能力。

Webhook 解决:

如何让系统知道什么时候需要更新。

GitHub Actions 解决:

如何获取最新代码并执行同步任务。

最终:

  • CMS 负责内容管理;
  • GitHub 保存内容版本;
  • Actions 执行同步任务;
  • Next.js 管理缓存;
  • ISR 提供高速访问。

形成了一套:

自动化、高性能、低成本的现代博客架构。

对于个人博客、文档站、知识库这类内容驱动型网站来说:

ISR + Webhook + CI Workflow

是一套非常优雅的解决方案。

它让我可以像使用动态网站一样维护博客,同时享受到静态网站接近 CDN 的访问速度。

加载评论中...