需求技术选型
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
如下图,笔者截图示例
注意,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";
}
效果图
功能笔者已经发布上线,大家可以手动修改这个版本号,等一分钟就能看到更新提示效果
完整github代码和线上地址
- 完整代码在github仓库地址中:github.com/shuirongshu…
- 线上地址:ashuai.site/reactExampl…