前言
这篇博客我们将会讨论一下如何实现一个轻量级的站点更新提示。
实现效果
在站点存在新版本可用,而用户正在访问旧版本时,弹出一个侵入程度较低的更新提示,点击后刷新页面获取最新版本的站点。
Demo
博客有新版本
可点击更新按钮来体验效果,右上角的重置按钮将会使组件重新进行初始化渲染。
架构
先用一个简单的架构图来展示当前的更新模式。
主要分为三部分:
- GitHub 通过 Web-hook 推送最新部署 ID 至服务器。
- 服务器获取到最新部署 ID 后存储到 Redis 中,并通过 SSE 分发给连接中的客户端。
- 客户端对比服务器下发 ID 与本地所持有的部署 ID ,如不匹配则弹出更新提示。
接下来就分步骤简要拆解一下整体流程。
构建 ID
首先,需要一个标识来区分每个部署,既然代码由 Git 进行版本管理,那最符合直觉的方式当然就是直接使用 git SHA(Hash) 来作为唯一 ID。
类似于保存已经编辑的文件,提交会记录对分支中一个或多个文件的更改。 Git 将为每个提交分配唯一的 ID,称为 SHA 或哈希,用于识别:
- 具体的更改
- 进行更改的时间
- 更改创建者
Next.js 的构建 ID
在 next.js 中,我们可以通过
next-build-id
这个库来获取当前的 build id(也就是当前的 git SHA)。By default, it will use the latest git commit hash from the local git repository
next-build-id 实际上是通过获取本地 git 存储库来获取当前的 git SHA,因此在客户端页面上我们无法通过这个库来获取build id,只可以在构建的时候或者在服务器组件中获取。
在这个博客站点中,在服务器组件中获取 build id 后再将其传递给客户端组件,用以后续的对比。
import buildId from 'next-build-id'
import UpdaterInit from './updater'
export default async function UpdaterServer() {
const BuildId = await buildId()
return <UpdaterInit buildId={BuildId} />
}
GitHub 推送构建 ID
当前博客部署在 Vercel 上,每次代码更新后都会触发 Vercel 的构建部署任务,而任务的状态实际上反映在仓库的 Deployment 中,因此我们可以通过监听 Deployment 的状态来获取最新构建部署成功的 ID。
而监听的方式也十分简单,通过 GitHub 的 Webhooks 功能,可以在每次 Deployment 状态有变更时将相关状态与数据推送到设置的地址中。
Webhooks documentation - GitHub Docs
在 Which events would you like to trigger this webhook? 设置中,只勾选
Let me select individual events - Deployment statuses
设置后,每当部署状态有变更时,数据将会推送到设置的 Webhooks 地址中。
博客后端
在服务端,需要设计两个 API:
- 接收 GitHub 的部署推送数据以获取部署状态与部署 ID API
- 提供客户端获取与订阅版本号更新 API
解析推送
解析 GitHub 的部署推送数据十分简单,但首先需检查 Header 中的
x-github-event
是否为
deployment_status
在检查完成后,如
state
为success
则便可以提取
deployment.sha
作为部署 ID
从推送 json 中解析部署ID
在获取到构建ID后,将其存入到 Redis 缓存中,并通知订阅者。
获取构建 ID API
在获取 API 的实现上则较为简单,提供直接获取最新构建ID与订阅的功能即可,可以将这两个功能在同一个 API 中实现,通过 SSE 参数来区分是否为订阅请求。
func (c *ControllerV1) GetDeploy(ctx context.Context, req *v1.GetDeployReq) (res *v1.GetDeployRes, err error) {
if !req.SSE {
res, err = getBuildId(ctx)
if err != nil {
return
}
} else {
request := g.RequestFromCtx(ctx)
request.Response.Header().Set("Content-Type", "text/event-stream")
request.Response.Header().Set("Cache-Control", "no-cache")
request.Response.Header().Set("Connection", "keep-alive")
notifier := g_functions.NotifierManagerInstance.GetOrSetCreateNotifier(g_consts.BlogBuildIdCacheKey)
id, ch := notifier.Subscribe()
defer notifier.Unsubscribe(id)
return nil, c.sendBuildIdSSE(ctx, ch, request)
}
return
}
客户端比对
在 Next.js 中,通过构建一个更新器组件来实现更新的功能。
首先,在初始化组件时,首先通过 API 获取博客服务端记录的最新构建 ID,获取后将其作为订阅数据的 fallbackData ,这样子即可保证:
获取到最新数据的同时也可以随时接收到新数据的推送。
获取到数据后,只需对比获取到的构建 ID 与客户端的构建 ID 是否一致即可得知是否为最新版本。
如版本不一致,则弹出更新提示。
在更新提示的显示动画上,使用 framer-motion 动画库中的
<AnimatePresence/>
与<m.div/>
来定义初始化与消失动画。而在更新逻辑上,增加 canvas-confetti 来使得更新体验(刷新页面)有趣一些。
灵感来源
在使用 Clerk 与 V0 的过程中,在存在更新未保存的状态下,都采用了这种 banner 提示的方式来呈现。
这种良好的用户体验让我也想拙劣地模仿一番,于是便有了这个简易的更新提示。