返回首页

前端部署后的版本检测与自动更新

布莱克2026-08-18 22:33已编辑
Tip:文章封面与内容无关,作者旅游时拍摄,因为没什么值得把四季都错过!

问题现象:

SPA 应用部署后,已打开页面的用户不会自动加载新版本

部署前(用户当前运行版本 v1)
├── index.html          # 引用 _nuxt/abc123.js
└── _nuxt/
    └── abc123.js

部署后(服务器版本 v2)
├── index.html          # 引用 _nuxt/def789.js
└── _nuxt/
    ├── abc123.js       # 已删除
    └── def789.js


技术方案对比

方案实时性实现成本服务器开销适用场景
WebSocket 推送毫秒级高需维护长连接实时协作、IM
SSE毫秒级中需服务端支持通知、监控面板
轮询0-5min低极低内容型网站、后台管理

我的方案选择是轮询,成本较低,且五分钟的延迟对用户体验影响较小

┌─────────────────────────────────────────────────────────────────┐
│                        构建阶段                                 │
│  ┌─────────────┐    ┌─────────────────────────────────────┐     │
│  │ 生成版本号   │ →  │ 写入 build-version.json + 注入到     │    │
│  │ (Git SHA)   │    │ 运行时配置 (appBuildVersion)         │    │
│  └─────────────┘    └─────────────────────────────────────┘     │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                        部署阶段                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │ Nginx 配置: 静态资源长缓存 + JSON 不缓存 + 备份清理       │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                        运行阶段                                 │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐       │
│  │ 立即检测一次  │ →  │ 每 5 分钟轮询 │ →  │ 版本不一致   │       │
│  │              │    │              │    │ 弹窗通知刷新  │       │
│  └──────────────┘    └──────────────┘    └──────────────┘       │
│                              ↑                                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │ visibilitychange 控制轮询启停 + 切回时立即检测            │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

模块职责

模块职责
版本号生成每次构建生成唯一标识
构建注入写入 build-version.json,注入运行时配置
Nginx 路由静态资源服务 + 缓存策略控制
前端检测插件轮询、对比版本、通知、生命周期控制

版本号生成策略

//优先git命令 获取当前代码仓库最新提交的短哈希值
//时间戳兜底
function getAppBuildVersion() {
  try {
    return execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim()
  } catch {
    return String(Date.now())
  }
}

构建时写入version.json

以Nuxt项目为例,不同项目写法不同  build-version.json 必须放在 public 目录下

hooks: {
  compiled(nitro) {
    const publicDir = nitro.options.output.publicDir
    mkdirSync(publicDir, { recursive: true })
    writeFileSync(
      join(publicDir, 'build-version.json'),
      JSON.stringify({ version: appBuildVersion }, null, 2)
    )
  }
}

Nginx配置:

# JSON 文件:禁止缓存(核心配置)
location ~* \.json$ {
    root /var/nuxt-blog/.output/public;
    try_files $uri =404;
    access_log off;
    add_header Cache-Control "no-cache, no-store, must-revalidate";
}

核心检测逻辑:

const check = async () => {
  if (!localVersion) return  // ← 本地没有版本号,无法对比,直接退出
  
  try {
    // 1. 请求服务器版本文件
    const res = await fetch(`${VERSION_URL}?t=${Date.now()}`, {
      cache: 'no-store'  // ← 强制不走浏览器缓存
    })
    
    if (!res.ok) return   // ← 请求失败(404/500等),静默退出
    
    const data = await res.json()  // ← 解析 JSON
    
    // 2. 对比版本号
    if (data?.version && data.version !== localVersion) {
      notify()  // ← 不一致 → 弹窗!
    }
  } catch {
    // 捕获异常,静默处理,不打断用户阅读
  }
}

正常用户访问流程:

时间点 ①  用户首次打开页面(版本 v1)
          服务器上有 build-version.json → { version: "v1" }
          构建时注入到页面的 localVersion = "v1"

时间点 ②  用户发起 fetch(/build-version.json)
          服务器返回 { version: "v1" }

          对比:localVersion ("v1") === json.version ("v1")
          ✅ 一致 → 不弹窗
          
时间点 ③  你部署了新版本 v2
          服务器上的 build-version.json 变成了 { version: "v2" }
          ⚠️ 但用户浏览器里还跑着旧页面(没刷新)

时间点 ④  用户页面里的 localVersion 仍然是构建时注入的旧值 "v1"
          (因为页面本身没刷新,JS 上下文没重建)

时间点 ⑤  用户发起 fetch(/build-version.json)
          服务器返回 { version: "v2" }

          对比:localVersion ("v1") !== json.version ("v2")
          🔔 不一致 → 弹窗!

核心流程编排:

const start = () => {
  // 1. 立即执行一次检测
  throttledCheck()
  
  // 2. 启动定时轮询
  startPolling()
  
  // 3. 监听页面可见性变化
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') {
      startPolling()      // ← 用户回来了,恢复轮询
      throttledCheck()    // ← 立即检测一次
      return
    }
    stopPolling()         // ← 用户切到后台,停止轮询
  })
  
  // 4. 用户聚焦页面时触发检测
  window.addEventListener('focus', throttledCheck)
}
行为触发作用
切到后台stopPolling()节省 CPU/电量
切回前台startPolling() + throttledCheck()恢复检测,立即查一次
点击页面throttledCheck()用户活跃时多一次检测



assistant