版本更新与缓存清除

0 阅读5分钟

update-version.js

#!/usr/bin/env node

/**
 * @FileDesc: 部署前自动更新版本号脚本
 * @Usage: 在package.json的build脚本中调用
 * 例如: "build": "node scripts/update-version.js && vite build"
 */
#!/usr/bin/env node

/**
 * @FileDesc: 部署前自动更新版本号脚本
 * @Usage: 在package.json的build脚本中调用
 * 例如: "build": "node scripts/update-version.js && vite build"
 */

import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
import { resolve, join } from "path"
import { fileURLToPath } from "url"

// 获取当前文件的目录
const __filename = fileURLToPath(import.meta.url)
const __dirname = resolve(__filename, "..")

try {
    // 获取项目根目录
    const projectRoot = resolve(__dirname, "../")
    const packageJsonPath = join(projectRoot, "package.json")
    const versionJsonPath = join(projectRoot, "public/version.json")

    // 确保 public 目录存在
    const publicDir = join(projectRoot, "public")
    if (!existsSync(publicDir)) {
        mkdirSync(publicDir, { recursive: true })
    }

    // 从 package.json 获取版本号
    const packageJsonContent = readFileSync(packageJsonPath, "utf-8")
    const packageJson = JSON.parse(packageJsonContent)
    const version = packageJson.version || "1.0.0"

    // 获取当前时间(ISO 8601格式)
    const buildTime = new Date().toISOString()

    // 创建 version.json 内容
    const versionInfo = {
        version,
        buildTime
    }

    // 写入 version.json 文件
    writeFileSync(versionJsonPath, JSON.stringify(versionInfo, null, 2))

    console.log("✅ 版本文件已更新:")
    console.log(`   版本号: ${version}`)
    console.log(`   构建时间: ${buildTime}`)
} catch (error) {
    console.error("❌ 版本文件更新失败:", error.message)
    process.exit(1)
}

version.ts

/*
 * @FileDesc: 版本管理和更新检测工具
 */

import { Toast } from "antd-mobile"
import { Dialog } from "antd-mobile"

/** 声明全局变量 */
declare const __BUILD_TIME__: string | undefined

/** 版本检查相关常量 */
export const VERSION_CHECK_CONFIG = {
    // 版本检查的localStorage key - 存储 buildTime,用于检测更新
    STORAGE_KEY_VERSION: "APP_BUILD_TIME",
    // 版本检查间隔(毫秒),5分钟检查一次
    CHECK_INTERVAL: 5 * 60 * 1000,
    // 版本API接口
    VERSION_URL: `${import.meta.env.VITE_BASE_PATH || "/"}version.json`
}

/** 版本信息接口 */
export interface IVersionInfo {
    version: string
    buildTime: string
}

/**
 * 获取当前应用版本
 */
export const getCurrentVersion = (): string => {
    const buildTime = typeof __BUILD_TIME__ !== "undefined" ? __BUILD_TIME__ : ""
    return buildTime || localStorage.getItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION) || "0.0.0"
}

/**
 * 获取远端版本信息
 */
export const fetchRemoteVersion = async (): Promise<IVersionInfo | null> => {
    try {
        const response = await fetch(VERSION_CHECK_CONFIG.VERSION_URL, {
            cache: "no-cache"
        })

        if (!response.ok) {
            return null
        }

        const data: IVersionInfo = await response.json()
        return data
    } catch (error) {
        console.error("获取远端版本失败:", error)
        return null
    }
}

/**
 * 清除所有缓存和本地数据(除了版本信息)
 */
export const clearAllCaches = async () => {
    try {
        // ⚠️ 关键:保存版本信息,防止清除后丢失
        const versionBuildTime = localStorage.getItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION)

        // 清除Service Worker缓存
        if ("caches" in window) {
            const cacheNames = await caches.keys()
            await Promise.all(cacheNames.map(cacheName => caches.delete(cacheName)))
        }

        // 清除localStorage和sessionStorage
        localStorage.clear()
        sessionStorage.clear()

        // 如果存在IndexedDB,也清除掉
        if ("indexedDB" in window) {
            const databases = await indexedDB.databases()
            databases.forEach(db => {
                if (db.name) {
                    indexedDB.deleteDatabase(db.name)
                }
            })
        }

        // 恢复版本信息,防止重复检测
        if (versionBuildTime) {
            localStorage.setItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION, versionBuildTime)
        }
    } catch (error) {
        console.error("清除缓存失败:", error)
    }
}

/**
 * 清除登录状态
 */
export const clearLoginState = async () => {
    try {
        // 清除与登录相关的localStorage数据
        const loginRelatedKeys = ["xczzH5Token", "applyStatus"]

        loginRelatedKeys.forEach(key => {
            localStorage.removeItem(key)
            sessionStorage.removeItem(key)
        })
    } catch (error) {
        console.error("清除登录状态失败:", error)
    }
}

/**
 * 处理应用更新
 */
export const handleAppUpdate = async (newBuildTime?: string) => {
    // 显示更新提示对话框
    Dialog.show({
        title: "发现新版本",
        content: "应用已更新,请重新登录以获取最新功能",
        closeOnAction: true,
        actions: [
            {
                key: "confirm",
                text: "重新登录",
                onClick: async () => {
                    const toastHandler = Toast.show({
                        content: "正在清除缓存...",
                        duration: 0
                    })

                    // 先更新版本号,再清除缓存
                    if (newBuildTime) {
                        localStorage.setItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION, newBuildTime)
                    }

                    // 清除所有缓存和登录状态
                    await clearAllCaches()
                    await clearLoginState()

                    if (toastHandler) {
                        toastHandler.close()
                    }

                    // 重定向到登录页
                    window.location.href = `${import.meta.env.VITE_BASE_PATH || "/"}login`
                }
            }
        ]
    })
}

/**
 * 检查应用版本并处理更新
 * ⚠️ 关键改进:
 * 1. 添加去重标志,防止重复弹窗
 * 2. 添加详细日志追踪
 * 3. 添加异常处理
 */
export const checkAndHandleUpdate = async () => {
    try {
        // 获取本地记录的构建时间
        const storedBuildTime = localStorage.getItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION)

        // 获取远端版本
        const remoteVersion = await fetchRemoteVersion()

        if (!remoteVersion) {
            console.warn("⚠️ 无法获取远端版本信息")
            return
        }

        // 添加详细日志
        console.log("📊 版本检查详情:", {
            "远端 buildTime": remoteVersion.buildTime,
            "本地 buildTime": storedBuildTime || "未记录(首次访问)",
            版本号: remoteVersion.version,
            需要更新: !!storedBuildTime && storedBuildTime !== remoteVersion.buildTime
        })

        if (storedBuildTime && storedBuildTime !== remoteVersion.buildTime) {
            console.log(`🔄 检测到版本更新!`)
            console.log(`   旧版本: ${storedBuildTime}`)
            console.log(`   新版本: ${remoteVersion.buildTime}`)

            // 传递新的 buildTime,在用户确认后更新
            await handleAppUpdate(remoteVersion.buildTime)
        } else if (!storedBuildTime) {
            // 第一次访问,记录当前版本
            console.log(`📝 首次访问,记录 buildTime: ${remoteVersion.buildTime}`)
            localStorage.setItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION, remoteVersion.buildTime)
        } else {
            console.log("✅ 版本无更新,应用为最新版本")
        }
    } catch (error) {
        console.error("❌ 版本检查出错:", error)
    }
}

/**
 * 启动版本检查(定时检查)
 */
export const startVersionCheck = () => {
    console.log("🚀 版本检查已启动")
    console.log(`📋 检查间隔: ${VERSION_CHECK_CONFIG.CHECK_INTERVAL / 1000} 秒`)
    console.log(`🔗 版本 API: ${VERSION_CHECK_CONFIG.VERSION_URL}`)

    // 立即检查一次
    checkAndHandleUpdate()

    // 定时检查
    const intervalId = setInterval(() => {
        console.log(`⏰ [${new Date().toLocaleTimeString()}] 执行定时版本检查...`)
        checkAndHandleUpdate()
    }, VERSION_CHECK_CONFIG.CHECK_INTERVAL)

    // 暴露到全局作用域,方便调试
    if (typeof window !== "undefined") {
        ;(window as any).__versionCheckUtils__ = {
            // 手动检查版本
            checkNow: () => {
                console.log("🔍 手动触发版本检查...")
                checkAndHandleUpdate()
            },
            // 清空版本记录,下次访问会重新记录
            resetVersion: () => {
                localStorage.removeItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION)
                console.log("✅ 版本记录已清空,刷新页面后会重新记录")
            },
            // 查看当前版本信息
            getVersionInfo: () => {
                return {
                    "本地 buildTime": localStorage.getItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION),
                    "存储 key": VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION,
                    "API 地址": VERSION_CHECK_CONFIG.VERSION_URL,
                    "检查间隔(秒)": VERSION_CHECK_CONFIG.CHECK_INTERVAL / 1000
                }
            },
            // 获取远端版本
            getRemoteVersion: async () => {
                const version = await fetchRemoteVersion()
                console.log("🌐 远端版本:", version)
                return version
            },
            // 测试清除缓存
            testClearCache: async () => {
                console.log("🧹 测试清除缓存...")
                await clearAllCaches()
                console.log("✅ 缓存清除完成")
            },
            // 间隔 ID
            intervalId
        }

        console.log("💡 调试工具已加载,可在控制台使用 window.__versionCheckUtils__")
        console.log("   - checkNow()        手动检查版本")
        console.log("   - resetVersion()    清空版本记录")
        console.log("   - getVersionInfo()  查看版本信息")
        console.log("   - getRemoteVersion()获取远端版本")
        console.log("   - testClearCache()  测试清除缓存")
    }
}

版本更新与缓存清除


🎯 核心功能

1. 自动版本检测

  • 应用启动时自动检查 /version.json
  • 每5分钟定时检查一次
  • 比对构建时间(buildTime)检测新版本

2. 缓存清除

  • Service Worker 缓存
  • localStorage / sessionStorage
  • IndexedDB 数据库
  • 所有登录相关数据

3. 强制重新登录

  • 检测到新版本时显示"发现新版本"提示
  • 点击"重新登录"清除缓存并跳转到登录页
  • 用户重新登录后获取最新版本代码

📁 文件结构

src/
├── utils/
│   └── version.ts                    # 核心版本检查工具
├── hooks/
│   └── update/
│       └── useVersionCheck.ts        # 版本检查 Hook
├── stores/
│   └── userInfo.ts                   # 已集成 logout 方法(清除缓存)
└── App.tsx                           # 已集成 useVersionCheck Hook

public/
└── version.json                      # 版本信息文件(自动生成)

scripts/
├── update-version.js                 # 自动更新版本脚本
├── update-version.sh                 # Linux/Mac 版本脚本
└── update-version.bat                # Windows 版本脚本

package.json                          # build 脚本

🚀 使用方式

1. 开发环境

pnpm start

2. 构建项目

# 自动更新版本并构建
pnpm build

# 测试云构建
pnpm build:testcloud

# 生产云构建
pnpm build:prodcloud

3. 手动更新版本(可选)

pnpm update-version

🔄 工作流程

1. 执行 pnpm build
   ↓
2. 运行 update-version.js 脚本
   ↓
3. 读取 package.json 中的版本号
   ↓
4. 生成当前构建时间(ISO 8601)
   ↓
5. 生成/更新 public/version.json
   ↓
6. Vite 构建应用
   ↓
7. 部署到服务器
   ↓
8. 用户访问应用
   ↓
9. useVersionCheck Hook 启动
   ↓
10. 定时检查 /version.json
    ↓
11. 版本不同 → 显示更新提示 → 清除缓存 → 重新登录

🛠️ 部署配置

Nginx 配置(重要)

必须禁用 version.json 的缓存,否则更新检测无法生效:

# 版本检查文件 - 禁用缓存
location /version.json {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
}

# 其他资源 - 长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# HTML 文件
location ~* \.html$ {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
}

📊 版本信息文件格式

public/version.json 内容示例:

{
    "version": "1.4.3",
    "buildTime": "2026-01-27T10:30:00Z"
}

自动生成说明

  • version:从 package.json 中读取
  • buildTime:构建时的当前时间(自动生成)

🧪 测试版本更新

本地测试步骤

  1. 修改版本号

    # 编辑 package.json,改变 version 字段
    # 例如:1.4.3 -> 1.4.4
    
  2. 重新构建

    pnpm build
    pnpm preview
    
  3. 验证功能

    • 打开应用
    • 打开浏览器开发者工具
    • 编辑 public/version.json,改变 buildTime
    • 刷新页面
    • 应该会弹出"发现新版本"提示

🔍 调试

查看版本信息

在浏览器控制台执行:

// 获取远端版本
fetch("/version.json")
    .then(r => r.json())
    .then(data => console.log("远端版本:", data))

// 检查本地版本记录
console.log("本地版本:", localStorage.getItem("APP_VERSION"))

查看缓存

// Service Worker 缓存
caches.keys().then(names => console.log("缓存列表:", names))

// localStorage
console.log("localStorage:", localStorage)

// sessionStorage
console.log("sessionStorage:", sessionStorage)

手动清除缓存

import { clearAllCaches, clearLoginState } from "@/utils/version"

// 清除所有缓存
await clearAllCaches()
console.log("缓存已清除")

// 清除登录状态
await clearLoginState()
console.log("登录状态已清除")

⚙️ 配置调整

修改检查间隔

编辑 src/utils/version.ts

export const VERSION_CHECK_CONFIG = {
    // 改为 10 分钟检查一次
    CHECK_INTERVAL: 10 * 60 * 1000
    // ...其他配置
}

禁用版本检查

src/App.tsx 中注释掉:

// useVersionCheck() // 注释掉这行

自定义更新提示

编辑 src/utils/version.ts 中的 handleAppUpdate() 函数


📚 更多资源

详细部署指南请查看 DEPLOYMENT_GUIDE.md


💡 关键要点

一定要做的事:

  1. 在 Nginx/Apache 中禁用 version.json 的缓存
  2. 确保 Service Worker 正常注册
  3. 每次部署前运行 pnpm build(会自动更新版本)

不要做的事:

  1. 手动修改 public/version.json(会被覆盖)
  2. 启用 version.json 的缓存
  3. 在部署前忘记构建项目

📞 常见问题

Q: 用户更新后仍看到旧版本? A: 检查 Nginx 缓存配置,确保禁用了 version.json 的缓存

Q: 版本文件未生成? A: 检查是否有写入 public 目录的权限

Q: 如何强制所有用户更新? A: 修改 version.json 中的 buildTime 字段