前端轮子(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

结语

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

因为收藏===会了

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

相关推荐
xiaoxue..2 小时前
列表转树结构:从扁平列表到层级森林
前端·javascript·算法·面试
小oo呆2 小时前
【自然语言处理与大模型】LangChainV1.0入门指南:核心组件Agent
前端·javascript·easyui
Hao_Harrision3 小时前
50天50个小项目 (React19 + Tailwindcss V4) ✨ | DrawingApp(画板组件)
前端·react.js·typescript·tailwindcss·vite7
dly_blog3 小时前
Vite 原理与 Vue 项目实践
前端·javascript·vue.js
计算机毕设VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue汽车销售系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·汽车·课程设计
仅此,3 小时前
前端接收了id字段,发送给后端就变了
java·前端·javascript·spring·typescript
Lovely Ruby3 小时前
[前端] 封装一下 echart 6,发布到 npm
前端·npm·node.js
BD_Marathon3 小时前
NPM_常见命令
前端·npm·node.js
绿鸳3 小时前
12.17面试题
前端