背景
在 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()
}