---
title: "React 纯前端实现版本更新检测"
url: "https://lerry.me/post/2026/09/check-update-in-react"
date: 2026-09-28T13:24:00.000Z
updated: 2026-09-28T11:28:31.650Z
tags:
  - "前端"
  - "React"
  - "Vite"
language: zh-CN
---

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

之前写过一篇[纯前端实现版本更新检测](https://lerry.me/post/2022/05/check-update-in-frontend)，当时用的是 Vue.js，打包时生成一个 `version.txt` 文件，前端定时检查里面的版本号。

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

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

### 第一步

先安装用来显示提示的 [Sonner](https://github.com/emilkowalski/sonner)。项目里已经有的话可以跳过。

```bash
bun add sonner
```

### 第二步

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

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

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

```ts
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` 的清理函数](https://react.dev/reference/react/useEffect)里就可以了。

### 第三步

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

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

export default function App() {
  useUpdateCheck();

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

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

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

还有一点要注意：首页 HTML 要能拿到最新内容。代码里的时间参数和 [`cache: "no-store"`](https://developer.mozilla.org/en-US/docs/Web/API/Request/cache)用于避免浏览器缓存，如果 CDN 配置了忽略查询参数的强缓存，也需要调整首页的缓存规则。

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