前端轮子(1)--前端部署后-判断页面是否为最新

前端轮子系列:

  1. 前端部署后-判断页面是否为最新
  2. 如何优雅diy响应数据
  3. 如何优雅进行webview调试
  4. 如何实现测试环境的自动登录

项目部署后:通知更新、强制刷新、自测访问的页面为最新代码(是否发版成功)

重点:自测、提测、修改bug 后,确认访问的为最新页面

实现思路

目的:生成新的版本标识,通过对比标识 ==> 当前为最新 还是 旧页面

  1. 生成版本号:如:1.1.10 ,或者时间戳(202512201416)
    • 自动化脚本生成
    • 手动控制
  2. 对比版本号:相等=> 当前为最新;不等 => 当前为旧页面
  3. 通过对比结果 对应处理产品逻辑 完事儿~

具体实现

方案一:生成时间戳,注入变量,控制台打印

在vue-cli项目中

通过vue.config.js 入变量

然后在main.js中打印变量

javascript 复制代码
// main.js
console.log('__BUILD_TIME__', __BUILD_TIME__)
javascript 复制代码
// vue.config.js
const _date = new Date()
const BUILD_TIME = _date.toLocaleString ? _date.toLocaleString() : _date.getTime()

module.exports = {
  configureWebpack: {
    plugins: [
      new webpack.DefinePlugin({
        __BUILD_TIME__: JSON.stringify(BUILD_TIME)
      })
    ]
  },
}

在vite项目中

通过vite.config.js 注入 BUILD_TIME 变量

然后在main.js中打印 BUILD_TIME 变量

javascript 复制代码
// main.js
console.log('__BUILD_TIME__', __BUILD_TIME__)
javascript 复制代码
// vite.config.js
const _date = new Date()
const BUILD_TIME = _date.toLocaleString ? _date.toLocaleString() : _date.getTime()
export default defineConfig(() => {
  return {
    define: {
      __BUILD_TIME__: JSON.stringify(BUILD_TIME)
    },
  }
})

这种方案,实现了在控制台打印。但是对于生产环境,无法做到版本号的对比

为啥? 因为上线 一般会去掉日志的打印, 所以咱通过版本号来~

方案二:记录版本号,轮询对比版本号

  1. 编写生成版本号的脚本,生成版本号文件
  2. build结束,调用脚本去生成版本号文件
  3. 轮询对比版本号文件,如果版本号不一致,做相应操作
javascript 复制代码
// dist/version.json
{
  "buildTime": "2025-12-20 14:16:00"
}

prebuild : build前执行的脚本
postbuild:build结束后执行的脚本,用于生成版本号文件

javascript 复制代码
// package.json
{
  "scripts": {
    "postbuild": "node scripts/generate-version-dist.mjs"
  }
}
javascript 复制代码
// scripts/generate-version-dist.mjs
import { writeFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const root = path.resolve(__dirname, '..')


async function main() {
  const _date = new Date()
  const BUILD_TIME = _date.toLocaleString ? _date.toLocaleString() : _date.getTime()

  const outPath = path.join(root, 'dist', 'version.json')
  await writeFile(outPath, JSON.stringify({ BUILD_TIME }, null, 2) + '\n', 'utf-8')

}

main().catch((e) => {
  console.error('[generate-version] failed', e)
  process.exit(1)
})

轮询对比版本号

  1. 轮询对比版本号文件,如果版本号不一致,做相应操作
  2. 页面隐藏、切后台时,停止轮询,页面关闭时,停止轮询
  3. 页面显示、切前台时,开始轮询
javascript 复制代码
// main.js
import { createVersionPoller } from './utils/versionPoller'
createVersionPoller({
  versionUrl: '/version.json',
  storageKey: '__VERSION_CHECK__CURRENT_BUILD_TIME__',
  intervalMs: 15_000,
  maxDelayMs: 60_000,
  onResult: ({ remoteVersion, remoteBuildTime }) => {
    // 轮询到json文件 成功,额外处理逻辑
  },
  onError: (e) => {
    // 轮询失败,额外处理逻辑
  },
  onUpdate: () => {
    // 版本号不一致,需要更新,额外处理逻辑
  }
}).start()
javascript 复制代码
/**
 * 轮询 /version.json,对比 buildTime 判断是否需要更新(setTimeout + 错误指数退让)
 *
 * 行为说明:
 * - start() 会自动注册监听:visibilitychange / beforeunload / unload
 * - stop() 会清理定时器并卸载监听
 * - 请求失败会指数退让(delay *= 2,最大不超过 maxDelayMs);成功后恢复 intervalMs
 * - currentBuildTime 会写入 localStorage(storageKey)
 *
 * @param {Object} options
 * @param {string} [options.versionUrl='/version.json'] 版本文件地址
 * @param {string} [options.storageKey='__VERSION_CHECK__CURRENT_BUILD_TIME__'] localStorage key
 * @param {number} [options.intervalMs=15000] 正常轮询间隔(毫秒)
 * @param {number} [options.maxDelayMs=60000] 退让最大间隔(毫秒)
 * @param {(info:{localBuildTime:string,remoteBuildTime:string,remoteVersion:string,data:any})=>void} [options.onResult] 每次成功拉取后的回调
 * @param {(error:any)=>void} [options.onError] 拉取失败回调
 * @param {(info:{localBuildTime:string,remoteBuildTime:string,remoteVersion:string,data:any})=>void} [options.onUpdate] 发现新 buildTime 时回调(默认会 stop)
 */
export function createVersionPoller({
  versionUrl = '/version.json',
  storageKey = '__VERSION_CHECK__CURRENT_BUILD_TIME__',
  intervalMs = 15000,
  maxDelayMs = 60000,
  onResult,
  onError,
  onUpdate
} = {}) {
  let timer = null
  let stopped = true
  let delayMs = intervalMs
  let bound = false

  const readLocal = () => window.localStorage.getItem(storageKey) || ''
  const writeLocal = (v) => v && window.localStorage.setItem(storageKey, String(v))

  function clear() {
    if (!timer) return
    window.clearTimeout(timer)
    timer = null
  }

  function isVisible() {
    return document.visibilityState !== 'hidden'
  }

  async function fetchRemote() {
    const sep = versionUrl.includes('?') ? '&' : '?'
    const url = `${versionUrl}${sep}t=${Date.now()}`
    const res = await fetch(url, { cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } })
    if (!res.ok) throw new Error(`fetch ${versionUrl} failed: ${res.status}`)
    return await res.json()
  }

  async function tick() {
    if (stopped || !isVisible()) return

    const localBuildTime = readLocal() || ''

    try {
      const data = await fetchRemote()
      const remoteVersion = String(data?.version || '')
      const remoteBuildTime = String(data?.BUILD_TIME || '')

      delayMs = intervalMs
      onResult?.({ localBuildTime, remoteBuildTime, remoteVersion, data })

      console.log( remoteBuildTime , '=>', localBuildTime)

      if (remoteBuildTime && remoteBuildTime !== localBuildTime) {
        // 记录最新 buildTime,避免重复提示同一个更新
        writeLocal(remoteBuildTime)
        onUpdate?.({ localBuildTime, remoteBuildTime, remoteVersion, data })
      }
    } catch (e) {
      delayMs = Math.min(maxDelayMs, Math.max(500, delayMs * 2))
      onError?.(e)
    }

    timer = window.setTimeout(tick, delayMs)
  }

  function onVisibilityChange() {
    if (stopped) return
    if (!isVisible()) return clear()
    clear()
    timer = window.setTimeout(tick, 0)
  }

  function ensureBound() {
    if (bound) return
    bound = true
    document.addEventListener('visibilitychange', onVisibilityChange)
    window.addEventListener('beforeunload', stop)
    window.addEventListener('unload', stop)
  }

  function ensureUnbound() {
    if (!bound) return
    bound = false
    document.removeEventListener('visibilitychange', onVisibilityChange)
    window.removeEventListener('beforeunload', stop)
    window.removeEventListener('unload', stop)
  }

  function start() {
    if (!stopped) return
    stopped = false
    delayMs = intervalMs
    ensureBound()
    if (isVisible()) timer = window.setTimeout(tick, 0)
  }

  function stop() {
    stopped = true
    clear()
    ensureUnbound()
  }

  return { start, stop }
}

注意点

  • 轮询版本文件,拼 时间戳、设置请求头 避免命中缓存
  • 轮询版本文件,时间间隔不要太短,耗费网络,虽然已经做了轮询指数退让
  • 生成的版本文件 生成到dist下 注意访问路径
  • 版本号需要上传到git,需要手动执行脚本&commit + push

源码

xiaoyi1255

结语

如果本文对你有收获,麻烦动动发财的小手,点点关注、点点赞!!!👻👻👻

因为收藏===会了

如果有不对、更好的方式实现、可以优化的地方欢迎在评论区指出,谢谢👾👾👾

相关推荐
Pedantic1 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘1 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆2 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师3 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆3 小时前
VSCode自动格式化三要素
前端
爱勇宝3 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen4 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518136 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode6 小时前
Redis 在生产项目的使用
前端·后端