需求技術選型
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倉庫地址中:https://github.com/shuirongshuifu/react-examples
- 線上地址:https://ashuai.site/reactExamples/