之前写过一篇纯前端实现版本更新检测,当时用的是 Vue.js,打包时生成一个 version.txt 文件,前端定时检查里面的版本号。
最近在 React 项目里又遇到了这个需求。页面经常一开就是很久,已经部署了新版,用户还在使用旧代码,所以加了一个更新提示。
这次项目使用 React 和 Vite。Vite 默认打包生成的脚本文件名带有 hash,例如 index-YDUW89JJ.js,可以直接拿它作为版本标记。前端定时请求首页,取出入口脚本地址,和当前页面的地址比较,有变化就提示刷新。这样也就不用另外生成版本文件了。Vite 文档里有打包后的文件名示例。
第一步
先安装用来显示提示的 Sonner。项目里已经有的话可以跳过。
bun add sonner第二步
编写检测函数,我把这一块放在 src/hooks/use-update-check.ts 文件里。
初始版本直接从当前页面的 document 中读取,代表浏览器正在运行的版本。不要用第一次请求首页的结果作为初始版本,否则旧页面在第一次检测前遇到部署,就可能把新版本当成基准,漏掉更新提示。
hasUpdate 的作用是检测到更新后不再继续请求,也避免重复弹出提示。
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 的组件。
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
- License
- CC BY-NC-ND 4.0