动态

详情 返回 返回

以React+Vite為例實現web項目版本發佈後,通知用户刷新頁面獲取最新資源 - 动态 详情

需求技術選型

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代碼和線上地址

user avatar yelloxing 头像 beibiaobaidehaigui 头像 dlonng 头像 shuyixiaobututou 头像 nihaoanihao 头像 dirackeeko 头像 fkcaikengren 头像 xboxyan 头像 tong_6816038415d24 头像 meirenlidexiaomaju 头像 shoushoudeniupai 头像 beiniaonanyou 头像
点赞 22 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.