📩 教你如何在 Next.js 中纯前端实现版本更新检测和提示

2,314 阅读7分钟

前言

有部分用户在使用前端站点时,习惯将网站标签持续挂在浏览器中。如果网站发布了新版本,前端代码的新版本将被部署到服务器上,但在用户不主动刷新页面的情况下,用户会一直保留在旧的版本,这可能会导致许多问题,例如网站功能的错误、性能问题或安全漏洞等。

因此当前端发布新版本时,我们需要让用户在网页端可以感知,并提醒用户刷新以获取最新版本。

实现方案

版本检测方式

首先用户在使用网站时,如果发布了新的版本,需要有一个方式通知前端更新。这一步有以下两种方式:

  1. websocket 消息推送
  2. 轮询请求

考虑到版本的发布一般不是一个非常高频的操作(根据实际业务决定),且 websocket 需要后端做单独支持,成本比较高,因此这里选择的是前端轮询发送请求的方式来检查是否已发布新版本。

前端设置 setInterval 轮训的方式检测新版本,setInterval 会周期性地调用函数,并在指定的时间间隔内重复执行该操作,因此会在一定程度上增加 CPU 和内存的使用量。但是,这个消耗通常是非常小的,每隔 60 秒检查一次版本,这种频率的消耗通常是可以忽略不计的。

版本信息存储及获取

每次轮询,前端会发送一个请求用于获取当前最新版本,因此前端需要维护一个接口用于获取版本信息,这里我使用的是直接将一个静态的 json 文件 version.json 放在 public 目录下,json 中存放版本信息,客户端通过请求 /version.jso 获取 json 数据。在每一次构建项目时,通过在 webpack 中新增一个构建流程,去输出一个包含最新版本信息的 json 文件。

关于版本信息,如果通过手动维护,或是使用发版时的版本号那么在测试环境和预览环境中都无法调试,因此这里选择将 git commit hash 值作为版本信息,当 git commit hash 值变更时,意味着有更新版的代码需要刷新获取。

可以通过 next-build-id 这个库为我们获取 commit hash,并将它设置为 Next.js 的 Build Id。然后在 webpack 打包时获取 Build Id ,并将它写入到 public 目录 version.json 文件中。

webpack: (config, { buildId }) => {
    fs.writeFileSync(
      'public/version.json',
      JSON.stringify({
        version: buildId,
      })
    );
    return config;
  },
  generateBuildId: () => nextBuildId({ dir: __dirname }),

关于 Next.js 中 Build Id 的作用,我在 Next.js 网站部署踩坑经历小记及前端站点部署技巧 这篇文章中有介绍,感兴趣的同学可以去看下,这里再补充一点。

对于Next.js项目而言,Build Id 的作用在以下几个方面:

  1. 缓存策略:在构建 Next.js 站点时,Build Id 会被包含在生成的文件名中。这样,在部署站点时,可以通过配置CDN等缓存服务来缓存静态资源文件,以提高站点的性能和加载速度。由于 Build Id 是每次 Build 版本的唯一标识符,因此每个版本的静态资源文件名都不同,从而避免了浏览器缓存旧版本的文件而导致站点出现问题。
  2. 版本控制:Build Id 还可以用于版本控制和部署的管理。通过记录每次构建的 Build Id,可以方便地追踪站点的版本,并可以回滚到先前的版本,以便进行修复或撤销更改。
  3. 错误分析:在网站出现错误或异常情况时,可以使用 Build Id 来确定错误发生的版本。这有助于开发人员更快地找到问题并进行修复。

实际上也不一定需要设置 Build Id,只要你能在 webpack 中获取到一个版本相关联的独特变量并设置为静态文件即可。这里使用 Build Id 只是恰好 Next.js 中存在这个配置,且能符合我们的需要所以直接拿来使用了。

最后实现一个启动轮询检查的函数:

import { Modal } from '@/components';
import { useDisclosure } from '@chakra-ui/react';
import { useEffect } from 'react';

const fetchVersion = async () => {
  const res = await fetch('/version.json', {
    headers: { 'Cache-Control': 'no-cache' },
  });

  const latestVersion: { version?: string } = await res.json();
  return { ok: res.ok, latestVersion };
};

const VersionCheck = () => {
  const { isOpen, onClose, onOpen } = useDisclosure();
  
  useEffect(() => {
    fetchVersion().then(({ ok, latestVersion }) => {
      if (ok) {
        localStorage.setItem('latestVersion', latestVersion.version || '');
      }
    });

    const checkVersion = async () => {
      const { ok, latestVersion } = await fetchVersion();
      try {
        if (ok) {
          const currentVersion = localStorage.getItem('latestVersion');
          if (latestVersion.version !== currentVersion) {
            onOpen();
          }
          localStorage.setItem('latestVersion', latestVersion.version || '');
        }
      } catch (err) {
        console.error('Error checking version:', err);
      }
    };
    const interval = setInterval(checkVersion, 60000);

    return () => {
      clearInterval(interval);
    };
  }, []);

  return (
    <Modal
      title="New Version Available"
      showCloseIcon={false}
      showCancel
      isOpen={isOpen}
      onClose={onClose}
      onOk={() => window.location.reload()}
      okText="刷新"
    >
      网站已发布新版本,请刷新后继续使用
    </Modal>
  );
};

export default VersionCheck;
  1. fetchVersion:该函数从位于 /version.json 位置的 JSON 文件中获取版本信息,返回一个对象包含ok状态和最新版本号。
  2. VersionCheck:该函数渲染一个模态框组件,当网站有新版本时,弹出该模态框通知用户。它使用了useEffect 钩子来执行 fetchVersion 函数,将最新版本号保存在 localStorage 对象中,并使用useDisclosure 钩子来切换模态框的状态。最后,它设置了一个 1 分钟的时间间隔来检查是否有新版本,如果有则打开模态框。

当用户在模态框中点击“刷新”按钮时,浏览器将重新加载页面以获取最新版本的网站。

注意点

由于版本信息存储在 localstroage 中,当用户首次进入网站后会判断用户是否存在 latestVersion key,如果不存在就添加 latestVersion,然后在后续启动轮询判断当前网站是否为最新版本。

但如果用户已经存在 latestVersion ,且在上一次关闭标签后,前端发布了新版本,那么当用户再次进入网站时,由于 localstorage 中存储的还是旧版本的信息,因此会弹出一个需要更新版本的信息,这是不符合用户操作逻辑的,并且用户当前用户已经是最新版本的站点了。

为了解决这个问题,我在 useEffect 的顶部增加了一个操作:

fetchVersion().then(({ ok, latestVersion }) => {
  if (ok) {
    localStorage.setItem('latestVersion', latestVersion.version || '');
  }
});

用户每次进入网站时,都会先将最新版本的信息存储在 localstorage 中,之后再启动轮询判断,这样就不会出现用户刚刚进入页面就弹出提示的问题了。

nextBuildId 的实现原理

next-build-id 是一个用于为Next.js项目生成唯一构建ID的库。

在Next.js项目中,每次执行next build命令都会生成一个唯一的构建ID,用于标识该构建的版本。next-build-id 库的主要功能就是生成这个唯一的构建 ID。

  1. next-build-id库首先通过调用 child_process 模块的 execSync 方法执行 git rev-parse HEAD 命令获取当前Git仓库的最新提交ID。
  2. 它会读取 Next.js项目的 next.config.js 文件中的配置项 generateBuildId,如果该配置项被定义为一个函数,则会调用该函数并将当前的 Git 提交 ID 作为参数传递进去,以生成一个自定义的构建 ID。

最后,next-build-id 库会将生成的构建 ID 写入到Next.js项目的 .next/BUILD_ID 文件中,以便在每次执行 next build 命令时使用。

总结

虽然功能是实现了,目前使用刷新来获取新包的方式,对于用户的操作影响太大了。尤其是在填写一些复杂表单时打断用户的体验不好。是否可以实现用户无感增量更新?后续我会再调研一下能不能使用 ISR 或者其他策略提升一下版本更新时的用户体验。

如果文章对你有帮助,除了收藏外,不妨为我点个赞👍,respcet!

本文正在参加「金石计划」