纯前端实现web项目版本发布后用户侧自动刷新

背景

在 Web 系统进行版本迭代更新后,对于处于旧版本的客户端(即在版本发布时用户在线但未刷新页面的情况),无法自动感知到有新版本,需要用户手动刷新页面方可加载最新的版本资源。

技术方案设计

实现目标

仅针对旧版本的客户端:

  • 当版本发布时,主动展示版本更新提示,用户点击提示能够自动刷新页面。
  • 在 SPA 路由跳转时,自动刷新页面。

实现方案设计

解决方案为在前端项目本次版本发布时,将一个最新的版本号 JSON 描述文件存储至服务端(放置于腾讯云 Cos 中)。同时,在前端项目打包构建过程中,将版本号注入到代码里。客户端通过定时轮询的方式,获取服务端的 JSON 文件并与本地的版本号进行对比。若版本号不一致,则提示用户主动刷新。此外,在路由跳转时,判断若需要版本更新,则自动刷新页面

### gen-app-version-vite-plugin 插件实现

通过vite插件的transformIndexHtml钩子,在html中,注入版本信息

js 复制代码
import fs from 'fs'
import path from 'path'

const currWorkingDir = process.cwd()

function generateVersionPlugin() {

  const getVersionData = () => {
    const packageJsonPath = path.join(currWorkingDir, 'package.json')
    const packageJsonData = JSON.parse(fs.readFileSync(packageJsonPath))
    const version = packageJsonData.version
    const versionData = {
      version: version,
      name: packageJsonData.name,
    }
    return versionData
  }

  return {
    name: 'generate-app-version-plugin',
    transformIndexHtml(htmlContent) {
      const versionData = getVersionData()
      const headEndIndex = htmlContent.indexOf('</head>')
      // 在<head>标签结束位置插入全局变量
      const newHtmlContent = htmlContent.slice(0, headEndIndex) + `<script> 
       Object.defineProperty(window, '__yt_build_info__', { value: ${JSON.stringify(versionData)}, writable: false })
       </script>` + htmlContent.slice(headEndIndex)
      return newHtmlContent
    }
  }
}

export default generateVersionPlugin

定时拉取version文件任务实现

项目入口文件中,引入checkAppVersion函数执行即可

js 复制代码
import { Alert } from 'antd'
import ReloadOutlined from '@ant-design/icons/ReloadOutlined'
import ReactDOM from 'react-dom'
const key = '__yt_build_info__'

function compareVersions(version1: string, version2: string) {
  const v1Parts = version1.split('.').map(Number)
  const v2Parts = version2.split('.').map(Number)
  for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
    if (v1Parts[i] === undefined) {
      v1Parts[i] = 0
    }
    if (v2Parts[i] === undefined) {
      v2Parts[i] = 0
    }
    if (v1Parts[i] > v2Parts[i]) {
      return 1
    } else if (v1Parts[i] < v2Parts[i]) {
      return -1
    }
  }
  return 0
}

interface RespData {
  name: 'client' | 'admin'
  version: string
}

const getLocalVersionInfo = () => {
  const win = window as any
  const localBuildInfo = win?.[key] || {}
  return localBuildInfo
}

const containerkey = 'checkVersion'
function renderAlert() {
  const div = document.createElement('div')
  div.id = containerkey
  div.style.display = 'inline-flex'
  div.style.position = 'fixed'
  div.style.left = '50%'
  div.style.top = '-50px'
  div.style.zIndex = '0'
  div.style.transform = 'translate(-50%, 0)'
  div.style.transition = 'top 1s'  // 使用过渡效果

  const existingDiv = document.getElementById(containerkey)
  if (!existingDiv) {
    document.body.appendChild(div)
    setTimeout(() => {
      div.style.top = '20px'
    }, 0)

    ReactDOM.render(
      <Alert
        // type='warn'
        message={<div
          onClick={() => {
            location.reload()
          }}
          style={{
            cursor: 'pointer',
            padding: '0 20px',
            color: 'green'
          }}
        > 当前系统功能有更新迭代,点击此处将刷新页面加载到最新版本  <ReloadOutlined /></div>}
        banner
        // closable
        onClose={() => {
          setTimeout(() => {
            unmountAlert()
          }, 200)
        }}
      />,
      div
    )
  }
}

function unmountAlert() {
  const div = document.getElementById(containerkey)
  if (div) {
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
  }
}

function generateRandomNumber(min: number, max: number) {
  return Math.floor(Math.random() * (max - min + 1)) + min
}

export const checkAppVersion = (opt: { viteModeEnv: string }) => {
  const { viteModeEnv } = opt

  function checkAppVersion() {
    let url = ''
    if (viteModeEnv === 'prod' || viteModeEnv === 'test') {
      url = location.origin + '/version.json'
    }
    if (viteModeEnv === 'dev') {
      url = 'https://you-domain.com' + '/version.json'
    }

    const localBuildInfo = getLocalVersionInfo()
    if (!localBuildInfo.version) {
      return
    }

    fetch(url)
      .then(response => response.json())
      .then((serverVersionInfo: RespData) => {
        if (!serverVersionInfo) return
        const diffVal = compareVersions(serverVersionInfo.version, localBuildInfo.version)
        if (diffVal == 0) {
          setTimeout(() => {
            checkAppVersion()
          }, generateRandomNumber(3, 6) * 1000)
          return
        }
        if (diffVal > 0 || diffVal < 0) {
          renderAlert()
        }
      })
      .catch(error => {
        console.error('获取数据时出错:', error)
      })
  }
  checkAppVersion()
}

实现效果

相关推荐
大猫会长16 小时前
tailwindcss中,自定义多个背景渐变色
前端·html
xj75730653316 小时前
《python web开发 测试驱动方法》
开发语言·前端·python
IT=>小脑虎16 小时前
2026年 Vue3 零基础小白入门知识点【基础完整版 · 通俗易懂 条理清晰】
前端·vue.js·状态模式
IT_陈寒16 小时前
Python 3.12性能优化实战:5个让你的代码提速30%的新特性
前端·人工智能·后端
赛博切图仔17 小时前
「从零到一」我用 Node BFF 手撸一个 Vue3 SSR 项目(附源码)
前端·javascript·vue.js
爱写程序的小高17 小时前
npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree
前端·npm·node.js
loonggg17 小时前
竖屏,其实是程序员的一个集体误解
前端·后端·程序员
程序员爱钓鱼17 小时前
Node.js 编程实战:测试与调试 - 单元测试与集成测试
前端·后端·node.js
码界奇点17 小时前
基于Vue.js与Element UI的后台管理系统设计与实现
前端·vue.js·ui·毕业设计·源代码管理
时光少年17 小时前
Android KeyEvent传递与焦点拦截
前端