Article28 Sep 2026

React 纯前端实现版本更新检测

之前写过一篇纯前端实现版本更新检测,当时用的是 Vue.js,打包时生成一个 version.txt 文件,前端定时检查里面的版本号。

最近在 React 项目里又遇到了这个需求。页面经常一开就是很久,已经部署了新版,用户还在使用旧代码,所以加了一个更新提示。

这次项目使用 React 和 Vite。Vite 默认打包生成的脚本文件名带有 hash,例如 index-YDUW89JJ.js,可以直接拿它作为版本标记。前端定时请求首页,取出入口脚本地址,和当前页面的地址比较,有变化就提示刷新。这样也就不用另外生成版本文件了。Vite 文档里有打包后的文件名示例。

第一步

先安装用来显示提示的 Sonner。项目里已经有的话可以跳过。

Codebash
bun add sonner

第二步

编写检测函数,我把这一块放在 src/hooks/use-update-check.ts 文件里。

初始版本直接从当前页面的 document 中读取,代表浏览器正在运行的版本。不要用第一次请求首页的结果作为初始版本,否则旧页面在第一次检测前遇到部署,就可能把新版本当成基准,漏掉更新提示。

hasUpdate 的作用是检测到更新后不再继续请求,也避免重复弹出提示。

Codets
import { useEffect } from "react";
import { toast } from "sonner";

const CHECK_INTERVAL = 60 * 1000;

function getEntryScript(doc: Document): string | null {
  return doc
    .querySelector<HTMLScriptElement>('script[type="module"][src]')
    ?.getAttribute("src") ?? null;
}

export function useUpdateCheck() {
  useEffect(() => {
    // 开发环境下跳过检测
    if (import.meta.env.DEV) return;

    const currentScript = getEntryScript(document);
    if (!currentScript) return;

    let hasUpdate = false;
    let stopped = false;
    let timer: ReturnType<typeof setTimeout>;
    const controller = new AbortController();

    async function checkUpdate() {
      if (stopped || hasUpdate) return;

      try {
        const url = new URL(import.meta.env.BASE_URL, window.location.origin);
        url.searchParams.set("_", String(Date.now()));

        const resp = await fetch(url, {
          cache: "no-store",
          signal: controller.signal,
        });

        if (resp.ok) {
          const html = await resp.text();
          const doc = new DOMParser().parseFromString(html, "text/html");
          const latestScript = getEntryScript(doc);

          if (!stopped && latestScript && latestScript !== currentScript) {
            hasUpdate = true;
            toast("发现新版本", {
              description: "页面已更新,刷新后生效",
              duration: Infinity,
              position: "bottom-right",
              action: {
                label: "立即刷新",
                onClick: () => window.location.reload(),
              },
            });
          }
        }
      } catch {
        // 请求失败时不打扰用户,下次继续检测
      } finally {
        if (!stopped && !hasUpdate) {
          timer = setTimeout(checkUpdate, CHECK_INTERVAL);
        }
      }
    }

    // 延迟首次请求,避免和首屏加载挤在一起
    timer = setTimeout(checkUpdate, 3000);

    return () => {
      stopped = true;
      clearTimeout(timer);
      controller.abort();
    };
  }, []);
}

这里每次请求完成后,再等待一分钟发起下一次检测。组件卸载时清理定时器并取消请求,放在 useEffect 的清理函数里就可以了。

第三步

在 App.tsx 中调用这个 Hook,并放上显示 toast 的组件。

Codetsx
import { Toaster } from "sonner";
import { useUpdateCheck } from "./hooks/use-update-check";

export default function App() {
  useUpdateCheck();

  return (
    <>
      {/* 这里放原来的页面或路由 */}
      <Toaster />
    </>
  );
}

如果项目里已经有 <Toaster />,直接调用 useUpdateCheck() 就行,不需要再加一个。

检测到新版后,右下角会出现“发现新版本”的提示,一直保留到用户操作。点击“立即刷新”才会重新加载页面,避免自动刷新打断正在做的事情。

还有一点要注意:首页 HTML 要能拿到最新内容。代码里的时间参数和 cache: "no-store"用于避免浏览器缓存,如果 CDN 配置了忽略查询参数的强缓存,也需要调整首页的缓存规则。

这个办法检测的是入口脚本地址的变化。如果一次发布只改了 HTML 或独立的 CSS,入口脚本地址没有变,就不会提示。对需要让每次发布都触发提示的项目,还是用单独的构建版本号更合适。

About this article

Author
Lerry
Published
2026-09-28