BackIcon为站点增加更新提示

2024年11月4日

Openai logomark

Hamster1963

前言

这篇博客我们将会讨论一下如何实现一个轻量级的站点更新提示。

实现效果

在站点存在新版本可用,而用户正在访问旧版本时,弹出一个侵入程度较低的更新提示,点击后刷新页面获取最新版本的站点。

Demo

博客有新版本

可点击更新按钮来体验效果,右上角的重置按钮将会使组件重新进行初始化渲染。

架构

先用一个简单的架构图来展示当前的更新模式。

Frame 16.png

主要分为三部分:

  1. GitHub 通过 Web-hook 推送最新部署 ID 至服务器。
  2. 服务器获取到最新部署 ID 后存储到 Redis 中,并通过 SSE 分发给连接中的客户端。
  3. 客户端对比服务器下发 ID 与本地所持有的部署 ID ,如不匹配则弹出更新提示。

接下来就分步骤简要拆解一下整体流程。

构建 ID

首先,需要一个标识来区分每个部署,既然代码由 Git 进行版本管理,那最符合直觉的方式当然就是直接使用 git SHA(Hash) 来作为唯一 ID。

类似于保存已经编辑的文件,提交会记录对分支中一个或多个文件的更改。 Git 将为每个提交分配唯一的 ID,称为 SHA 或哈希,用于识别:

  • 具体的更改
  • 进行更改的时间
  • 更改创建者

Next.js 的构建 ID

在 next.js 中,我们可以通过

next-build-id

这个库来获取当前的 build id(也就是当前的 git SHA)。

npm: next-build-id

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。

CleanShot 2024-11-03 at 01.49.39@2x.png

而监听的方式也十分简单,通过 GitHub 的 Webhooks 功能,可以在每次 Deployment 状态有变更时将相关状态与数据推送到设置的地址中。

Webhooks documentation - GitHub Docs

Which events would you like to trigger this webhook? 设置中,只勾选

Let me select individual events - Deployment statuses

CleanShot 2024-11-03 at 01.52.10@2x.png

设置后,每当部署状态有变更时,数据将会推送到设置的 Webhooks 地址中。

博客后端

在服务端,需要设计两个 API:

  1. 接收 GitHub 的部署推送数据以获取部署状态与部署 ID API
  2. 提供客户端获取与订阅版本号更新 API

解析推送

解析 GitHub 的部署推送数据十分简单,但首先需检查 Header 中的

x-github-event

是否为

deployment_status

在检查完成后,如

state

success

则便可以提取

deployment.sha

作为部署 ID

alt: 从推送 json 中解析部署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 ,这样子即可保证:

获取到最新数据的同时也可以随时接收到新数据的推送。

api.go-2.png

获取到数据后,只需对比获取到的构建 ID 与客户端的构建 ID 是否一致即可得知是否为最新版本。

如版本不一致,则弹出更新提示。

api.go-3.png

在更新提示的显示动画上,使用 framer-motion 动画库中的

<AnimatePresence/>

<m.div/>

来定义初始化与消失动画。

而在更新逻辑上,增加 canvas-confetti 来使得更新体验(刷新页面)有趣一些。

灵感来源

在使用 Clerk 与 V0 的过程中,在存在更新未保存的状态下,都采用了这种 banner 提示的方式来呈现。

CleanShot 2024-11-03 at 23.35.26@2x.png

CleanShot 2024-11-03 at 23.37.04@2x.png

这种良好的用户体验让我也想拙劣地模仿一番,于是便有了这个简易的更新提示。