背景
在单页应用(SPA)项目中,路由的变化并不会导致前端资源的重新加载。然而,当服务端进行了更新,例如接口的字段名或结构发生变化,而前端的静态资源没有同步更新时,可能会导致报错。对于那些长时间保持电脑开机且浏览器页面保持打开状态的用户来说,版本升级提示显得尤为重要。
前言
为了避免因版本不匹配而引发的报错,提供及时、准确的版本更新提示变得至关重要。
1. 如何比较版本?
-
监听响应头中的 Etag 的变化
-
监听 git commit hash 的变化
2. 何时比较版本?
- 使用
setInterval
轮询固定时间段进行比较 - 监听
visibilitychange
事件 - 监听
focus
事件
大部分情况使用setInterval
设置一个合理的时间段即可,但当tab被切换时,浏览器的资源分配可能会受到限制,例如减少CPU和网络的使用,setInterval
的执行可能会受到影响。
因此还需监听visibilitychange
事件,当其选项卡的内容变得可见或被隐藏时,会在 document 上触发 visibilitychange
事件。
但在 pc 端,从浏览器切换到其他应用程序并不会触发 visibilitychange
事件,所以加以 focus
辅佐;当鼠标点击过当前页面 (必须 focus 过),此时切换到其他应用会触发页面的 blur
事件;再次切回到浏览器则会触发 focus
事件;
使用setInterval
+visibilitychange
+focus
便可解决绝大部分应用更新提示情况。如果更新频繁,对版本提示实时性要求较高的,还可以监听路由的变化,在路由切换时,获取版本的信息进行对比。
监听响应头中的 Etag 的变化
框架:React+Antd
关键点:
-
getETag:获取静态资源的Etag或last-modified
-
getHash:如果localStorage中没有版本信息,说明是新用户,直接存储版本信息。如果本地存储的版本信息与获取到的Etag不同,则弹出更新提示弹窗
-
使用
setInterval
+visibilitychange
+focus
调用getHash
js
import { useCallback, useEffect, useRef } from 'react';
import { notification, Button } from 'antd';
const getETag = async () => {
const response = await fetch(window.location.origin, {
cache: 'no-cache',
});
return response.headers.get('etag') || response.headers.get('last-modified');
};
const useVersion = () => {
const timer = useRef();
//防止弹出多个弹窗
const uploadNotificationShow = useRef(false);
const close = useCallback(() => {
uploadNotificationShow.current = false;
}, []);
const onRefresh = useCallback(
(new_hash) => {
close();
// 更新localStorage版本号信息
window.localStorage.setItem('vs', new_hash);
// 刷新页面
window.location.reload();
},
[close],
);
const openNotification = useCallback(
(new_hash) => {
uploadNotificationShow.current = true;
const btn = (
<Button type="primary" size="small" onClick={() => onRefresh(new_hash)}>
确认更新
</Button>
);
notification.open({
message: '版本更新提示',
description: '检测到系统当前版本已更新,请刷新后使用。',
btn,
duration: 0,
onClose: close,
});
},
[close, onRefresh],
);
const getHash = useCallback(() => {
if (!uploadNotificationShow.current) {
// 在 js 中请求首页地址,这样不会刷新界面,也不会跨域
getETag().then((res) => {
const new_hash = res;
const old_hash = localStorage.getItem('vs');
if (!old_hash && new_hash) {
// 如果本地没有,则存储版本信息
window.localStorage.setItem('vs', new_hash);
} else if (new_hash && new_hash !== old_hash) {
// 本地已有版本信息,但是和新版不同:版本更新,弹出提示
openNotification(new_hash);
}
});
}
}, [openNotification]);
/* 初始时检查,之后1h时检查一次 */
useEffect(() => {
getHash();
timer.current = setInterval(getHash, 60 * 60 * 1000);
return () => {
clearInterval(timer.current);
};
}, [getHash]);
useEffect(() => {
/* 切换浏览器tab时 */
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
getHash();
}
});
/* 当鼠标点击过当前页面,此时切换到其他应用会触发页面的blur;
再次切回到浏览器则会触发focus事件 */
document.addEventListener('focus', getHash, true);
}, [getHash]);
};
export default useVersion;
监听 git commit hash 的变化
框架:Vite+Vue3
1. 将版本号写入环境变量中
新建scripts/release.js
关键点:
-
执行
git rev-parse HEAD
获取最新git commit hash -
使用
fs.writeFileSync
将版本号写入到.env.production文件中
js
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filenameNew = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filenameNew);
//执行git命令,获取当前 HEAD 指针所指向的提交的完整哈希值,并通过substring取前十个数字或字母
const hash = execSync('git rev-parse HEAD', { cwd: path.resolve(__dirname, '../') })
.toString()
.trim()
.substring(0, 10);
//版本号:时间戳+git的hash值
const version = Date.now() + '_' + hash;
//.env.production文件路径
const envFile = path.join(__dirname, '../.env.production');
//读取目标文件,并通过正则判断.env.production文件中是否有VITE_APP_VERSION开头的环境变量
try {
const data = fs.readFileSync(envFile, {
encoding: 'utf-8',
});
const reg = /VITE_APP_VERSION=\d+_[\w-_+:]{7,14}/g;
const releaseStr = `VITE_APP_VERSION=${version}`;
let newData = '';
if (reg.test(data)) {
newData = data.replace(reg, releaseStr);
fs.writeFileSync(envFile, newData);
} else {
newData = `${data}\n${releaseStr}`;
fs.writeFileSync(envFile, newData);
}
console.log(`插入release版本信息到env完成,版本号:${version}`);
} catch (e) {
console.error(e);
}
2. 配置启动命令
修改package.json
js
"build": "node ./scripts/release.js && vite build",
运行 yarn build,可以在.env.production文件中看到环境变量VITE_APP_VERSION已成功写入
3. 轮询+visibilitychange + focus检测
关键点:
-
import.meta.env.VITE_APP_VERSION:获取新版本的hash值
-
getHash:如果localStorage中没有版本信息,说明是新用户,直接存储版本信息。如果本地存储的版本信息与新版本的hash值不同,则弹出更新提示弹窗
-
使用
setInterval
+visibilitychange
+focus
调用getHash
js
import { onBeforeUnmount, onMounted, ref } from 'vue';
const useCheckUpdate = () => {
const timer = ref();
const new_hash = import.meta.env.VITE_APP_VERSION; //获取新版本的hash值
let uploadNotificationShow = false; //防止弹出多个框
const getHash = () => {
if (!uploadNotificationShow && new_hash) {
const old_hash = localStorage.getItem('vs');
if (!old_hash) {
// 如果本地没有,则存储版本信息
window.localStorage.setItem('vs', new_hash);
} else if (new_hash !== old_hash) {
uploadNotificationShow = true;
// 本地已有版本信息,但是和新版不同:版本更新,弹出提示
if (window.confirm('检测到系统当前版本已更新,请刷新浏览器后使用。')) {
uploadNotificationShow = false;
// 更新localStorage版本号信息
window.localStorage.setItem('vs', new_hash);
// 刷新页面
window.location.reload();
} else {
uploadNotificationShow = false;
}
}
}
};
onMounted(() => {
getHash();
timer.value = setInterval(getHash, 60 * 60 * 1000);
/* 切换浏览器tab时 */
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
getHash();
}
});
/* 当鼠标点击过当前页面,此时切换到其他应用会触发页面的blur;
再次切回到浏览器则会触发focus事件 */
document.addEventListener('focus', getHash, true);
});
onBeforeUnmount(() => {
clearInterval(timer.value);
});
};
export default useCheckUpdate;
结尾
文章中只是介绍了大概的实现方式与思路,还有些细节可根据自己的实际情况实现。例如在开发环境下,不要弹出版本更新提示弹窗等功能。
如果有其他更好的方式实现版本更新提示,可以在评论区留言,大家积极探讨。
最后,创造不易,欢迎大家点赞支持!!!