以React+Vite为例实现web项目版本发布后,通知用户刷新页面获取最新资源

464 阅读7分钟

需求技术选型

1. 纯前端实现——前端轮询方案

  • 原理:前端定时(如每一分钟)发送请求(如请求version.json文件),对比本地存储的版本号与服务器返回的版本号,若不一致则提示更新。
  • 优点:实现简单(无需后端复杂逻辑,仅需一个静态版本文件),兼容性极好(所有浏览器支持)。
  • 缺点:实时性差有延迟(依赖轮询间隔)。
  • 适用场景:小型项目、对实时性要求低(如非高频更新的工具类网站)、快速迭代验证需求。

实际上,这种技术方案,对于带宽和服务器资源而言,算不上什么,因为服务器的运行是以毫秒、微秒、甚至是纳秒为单位运行,比起长连接而言资源消耗,九牛一毛

2. 纯前端实现——Service Worker 方案

  • 原理:前端通过 Service Worker 的生命周期(检测编写的sw.js脚本变化)感知版本更新(因sw.js内容随版本变化),无需后端主动推送。
  • 优点:无额外网络请求(依赖浏览器对 Service Worker 的自动更新检测),实时性较好(页面加载时即检测),可结合缓存策略优化资源加载,是 PWA 的标准能力。
  • 缺点:依赖 Service Worker 支持(IE 不支持,但现代浏览器均支持),需处理缓存清理、版本冲突等细节,逻辑稍复杂。
  • 适用场景:PWA 应用、需要离线能力的 Web 应用、希望减少网络请求的场景。

Service Worker这个在浏览器的调试面板里面:F12打开控制台 ---> Application ---> Service workers

如下图,笔者截图示例

111.png

注意,Service Worker只能在localhost或者https环境下才能正常运行。篇幅有限,后续笔者会单独发文讲解

3. 后端配合实现——WebSocket 方案

  • 原理:前端与后端建立全双工 WebSocket 连接,后端在检测到版本更新(如部署完成)后,主动向所有连接的客户端推送更新通知,前端接收后提示用户。
  • 优点:实时性极高(后端触发后立即推送),支持双向通信(未来可扩展其他实时交互需求)。
  • 缺点:实现复杂度高(后端需维护 WebSocket 连接、处理断线重连、广播消息等),协议与 HTTP 不同(需单独部署支持),资源消耗高于 SSE。
  • 适用场景:对实时性要求极高(如即时通讯、协作工具同步更新)、已有 WebSocket 基础设施(可复用连接)的项目。

4. 后端配合实现——SSE(EventSource API)方案

  • 原理:前端通过 EventSource 与后端建立单向持久 HTTP 连接,后端在版本更新时,通过该连接向前端推送通知(遵循 SSE 协议格式)。
  • 优点:轻量(基于 HTTP,无需额外协议),后端实现简单(无需维护全双工连接),客户端自动重连,适合单向推送场景。
  • 缺点:仅支持服务器→客户端单向通信,数据只能是文本格式,部分旧浏览器(如 IE)不支持。
  • 适用场景:仅需后端主动推送更新通知(无双向通信需求)、希望降低后端复杂度的场景(比 WebSocket 更轻量)。

对于前端而言,sse方案,代码最少,只要如下代码流程即可

// 建立SSE连接
const eventSource = new EventSource('/api/sse');

// 监听后端发送的消息
eventSource.onmessage = (event) => {
  console.log('收到推送:', event.data);
  alert('有船新版本哦,可以刷新页面获取哦...')
};

// 监听连接错误
eventSource.onerror = (error) => {
  console.error('SSE连接错误:', error);
};

// 监听连接成功
eventSource.onopen = () => {
  console.log('SSE连接已建立');
};

篇幅限制,sse的详细介绍,后续笔者也会专门发文,敬请期待

选型建议

  • 若项目简单、更新频率低:优先前端轮询(低成本实现)。
  • 若项目是 PWA 或需优化性能:优先Service Worker(利用浏览器原生能力,减少冗余请求)。
  • 若需实时性极高(如分钟级内必须通知用户):选WebSocket(全双工,实时性最强)。
  • 若只需单向推送且想简化后端:选SSE(轻量,适合纯通知场景)。

笔者个人经验,部署生产环境,前端轮询或者sse是性价比高一些的方案

代码流程控制

  • 生产打包构建的时候,在vite中编写一个Plugin去维护一个版本号JSON文件
  • JSON文件记录了当前的版本号
  • 版本号的来源是package.json中的版本号(每次发布新版本,我们都会修改与之对应的版本号)
  • 然后,编写一个VersionUpdateCheck.jsx的组件
  • 在这个组件中使用setInterval轮询请求生产环境的这个版本号JSON文件
  • 读取里面的版本号,并与本地对比
  • 若不一致,则说明有更新,就弹框通知提示用户有新版本了

编写versionPlugin插件

import versionPlugin from './src/plugins/vite-plugin-version';

export default defineConfig({
  base: '/reactExamples/',
  plugins: [react(), versionPlugin()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'), // 将 @ 指向 src 目录
    },
  },
  ......
}

插件内容

// vite-plugin-version.js
import fs from 'fs';
import path from 'path';

/**
 * Vite插件:生成版本信息文件
 * 在构建开始时读取package.json中的版本号,并生成包含版本和构建时间的JSON文件
 */
export default function versionPlugin() {
    return {
        name: 'version-plugin',
        /* Vite 构建开始时的钩子函数 */
        buildStart() {
            // 只在生产环境才去生成版本文件
            if (process.env.NODE_ENV !== 'production') {
                return;
            }

            // 1. 读取package.json文件对象里面的version版本号
            const pkgPath = path.resolve(process.cwd(), 'package.json');
            const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));

            // 2. 准备新版本信息和新版本构建时间
            const versionInfo = {
                // 使用这次发布的package.json版本号,如从0.0.0到0.0.1
                version: pkg.version,
                buildTime: new Date().toLocaleString('zh-CN')
            };

            // 3. 写入public目录
            const publicDir = path.resolve(process.cwd(), 'public');
            // 不存在则创建
            if (!fs.existsSync(publicDir)) fs.mkdirSync(publicDir);

            // 4. 将版本信息写入version.json文件
            fs.writeFileSync(
                path.join(publicDir, 'version.json'),
                JSON.stringify(versionInfo, null, 2) // 缩进2个空格
            );

            console.log(`😁😁😁 版本文件已生成: v${pkg.version}`);
        }
    };
}

注意,版本号源自于package.json文件

{
  "name": "react-examples",
  "private": true,
  "version": "0.0.1", // 源头版本号
  "type": "module",
  "scripts": { ... },
  "dependencies": { ... },
  "devDependencies": { ... }
}

编写VersionUpdateCheck.jsx的组件

import React, { useState, useEffect } from 'react';
import { Modal, Button, Space, Alert } from 'antd';
import { ExclamationCircleOutlined, ReloadOutlined } from '@ant-design/icons';

const isProduction = process.env.NODE_ENV === 'production';

/**
 * 版本更新检查组件
 * 功能:定期检查服务器上的版本信息,当检测到新版本时提示用户更新
 * 更新机制:通过刷新页面加载最新资源
 */
const VersionUpdateCheck = () => {
  const [showUpdate, setShowUpdate] = useState(false); // 控制更新提示弹窗的显示/隐藏
  const [latestVersion, setLatestVersion] = useState(''); // 存储从服务器获取的最新版本号

  useEffect(() => {
    if (!isProduction) {
      console.info('开发环境,跳过版本检查');
      return;
    }

    /**
     * 版本检查函数流程:
     * 1. 从服务器获取 version.json 文件(带时间戳避免缓存)
     * 2. 比较当前存储版本与服务器版本
     * 3. 如果版本不同,显示更新提示
     */
    const checkVersion = async () => {
      try {
        // 获取版本信息(添加时间戳参数防止浏览器缓存)
        const response = await fetch(`/reactExamples/version.json?t=${Date.now()}`);

        // 解析JSON数据,提取版本号
        const { version } = await response.json();

        // 从 localStorage 获取当前存储的版本号
        const currentVersion = localStorage.getItem('app-version');

        // 首次访问处理:若本地没有存储版本,则初始化存一份
        if (!currentVersion) {
          localStorage.setItem('app-version', version);
        }
        // 若本地有版本,再比较一下本地和服务器版本,二者不一致,说明有新版本
        else if (currentVersion !== version) {
          setLatestVersion(version); // 更新最新版本号
          setShowUpdate(true); // 打开更新提示弹窗
          // 更新本地版本号确保不会一直出现弹框提示
          localStorage.setItem('app-version', version);
          console.info(`检测到新版本: ${version} (当前: ${currentVersion})`);
        }
      } catch (error) {
        console.error('版本检查失败:', error);
      }
    };

    // 组件挂载后立即执行一次版本检查
    checkVersion();

    // 每1分钟检查一次版本更新
    const timer = setInterval(checkVersion, 1 * 60 * 1000);

    // 组件卸载时时候清除定时器
    return () => {
      clearInterval(timer);
      console.info('清除版本检查定时器');
    };
  }, []); // 空依赖数组确保只didMount运行一次

  const handleUpdate = () => {
    // 刷新页面,强制浏览器重新加载所有资源
    window.location.reload();
  };

  const handleKnow = () => {
    // 关闭弹窗
    setShowUpdate(false);
  };

  return (
    <Modal
      title={
        <Space>
          <ExclamationCircleOutlined style={{ color: '#faad14' }} />
          <strong>系统更新提示</strong>
        </Space>
      }
      open={showUpdate}
      onCancel={handleKnow}
      footer={
        <Space>
          <Button onClick={handleKnow}>
            我知道了,后续手动刷新页面
          </Button>
          <Button
            type="primary"
            icon={<ReloadOutlined />}
            onClick={handleUpdate}
          >
            立即更新
          </Button>
        </Space>
      }
      width={500}
      closable={false}
      maskClosable={false}
      centered
    >
      <Alert
        message="发现新版本"
        description={
          <div>
            <p>系统已发布新版本 {latestVersion},是否立即更新获取最新功能?</p>
            <p style={{ fontSize: '12px', color: '#666', marginTop: '8px' }}>
              • 更新将刷新当前页面,请确保已保存所有重要数据<br />
              • 更新过程只需几秒钟
            </p>
          </div>
        }
        type="warning"
        showIcon
        style={{ marginBottom: '16px' }}
      />
    </Modal>
  );
};

export default VersionUpdateCheck;

打包构建的version.json文件

{
  "version": "0.0.2",
  "buildTime": "2025/10/7 20:39:01"
}

nginx禁用此文件的缓存

为了以防万一此文件被缓存,我们通过nginx做对应控制,不过基本上我们通过时间戳的方式足以应对静态文件资源缓存了

// 获取版本信息(添加时间戳参数防止浏览器缓存) const response = await fetch("/reactExamples/version.json?t=${Date.now()}")

# 针对于version.json版本文件,设置禁止缓存(优先级更高)
location = /reactExamples/version.json {  # 使用 = 表示精确匹配
    alias /var/www/html/reactExamples/version.json;  # 直接指定文件路径
    # 禁止缓存
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
}

效果图

功能笔者已经发布上线,大家可以手动修改这个版本号,等一分钟就能看到更新提示效果

222.png

完整github代码和线上地址