纯前端实现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()
}

实现效果

相关推荐
2401_857026237 分钟前
拖动未来:WebKit 完美融合拖放API的交互艺术
前端·交互·webkit
星辰中的维纳斯1 小时前
vue新手入门教程(项目创建+组件导入+VueRouter)
前端·javascript·vue.js
miao_zz1 小时前
react native中依赖@react-native-clipboard/clipboard库实现文本复制以及在app中获取复制的文本内容
android·react native·react.js
嫣嫣细语1 小时前
css实现鼠标禁用(鼠标滑过显示红色禁止符号)
前端·css
Days20501 小时前
web前端主要包括哪些技术
前端
XF鸭2 小时前
HTML-CSS 入门介绍
服务器·前端·javascript
forwardMyLife2 小时前
element-plus 的form表单组件之el-radio(单选按钮组件)
前端·javascript·vue.js
fs哆哆3 小时前
ExcelVBA运用Excel的【条件格式】(二)
linux·运维·服务器·前端·excel
安冬的码畜日常3 小时前
【CSS in Depth 2精译】2.5 无单位的数值与行高
前端·css
ilisi_3 小时前
导航栏样式,盒子模型
前端·javascript·css