前端检测更新,自动刷新网页
# 前言
最近在开发过程中遇到了一个颇为头疼的问题,前端发布了新版本,但是用户侧没有感知。对于长时间停留在网页上的用户来说,他们操作的还是旧版本。这就会导致很多问题,发布了新功能用户不知道,修复了bug用户也不知道。只有当他刷新网页或者下一次重新打开网页才能加载到最新的系统。因此现在需要做一个自动检测更新并提示用户刷新页面的功能
# 调研
提示
在开发之前,首先在网上搜罗了一圈,总结如下: 大众所采用的方案大致分为两种
- 用户侧主动触发
- 网页自动触发
下面就来详细了解一下这两种方式
# 用户主动触发
用户侧主动触发指的是,用户切换了路由或者某些操作触发了接口调用时触发版本比对,具体操作:前端和服务端统一维护一个版本号,服务端在接口的响应体或者响应头里带上这个版本号,前端在每次请求接口的时候都去比对这个版本号,如果发现本版号有变化就提示用户
# 网页自动触发
网页自动触发大体上就是两种方案:
- 服务端主动推送更新
- 服务端通过
ws
或者sse
主动告知客户端有版本变动,客户端做出处理
- 服务端通过
- 客户端轮询
- 客户端轮询服务器上的文件或者服务端的接口,通过比对差异做出相应的处理
上述的方案网上有很多示例,可自行啃食,本文主要讲述通过轮询的方式
# 正文
通过轮询的方式也有许多种实现方案,比较典型的有请求服务器html
文件比对内容,亦或者不比对内容比对响应头上的缓存字段。由于比对这些都需要直接对服务器发起请求,轮询频率*用户基数会造成服务器产生一些不必要的开销。所以我决定生成一个版本文件用来记录当前版本信息,这个文件可以部署到服务器上
也可以放在oss
或者cdn
上,比较自由。如果服务器吃不消就放在oss
上这样不影响我们的功能也不会对服务器造成什么影响。
# 生成version.js文件
由于项目的构建工具是webpack
,所以这里需要写一个webpack
插件用来生成version
信息,用vite
的项目也是同样的道理。
插件的代码也非常简单,在构建完毕输出产物之前生成一个version.js
文件,并把它加入到构建产物中
# 插件代码
/**
* 生成构建指纹 用来比对服务器上文件是否更新
*/
module.exports = {
UploadPagePlugin: class UploadPagePlugin {
constructor(props) {
this.active = props || false
console.log(props)
}
apply(compiler) {
const _this = this
console.log(compiler.options.mode, '插件获取到的环境')
if (_this.active) {
// 使用 emit 钩子,这个钩子会在生成资源到 output 目录之前执行
compiler.hooks.emit.tapAsync('UploadPagePlugin', (compilation, callback) => {
try {
const sourceString = `export const version = ${new Date().getTime()}`
compilation.assets['version.js'] = {
source: () => sourceString,
size: () => sourceString.length
}
callback()
} catch (error) {
console.error('UploadPagePlugin error:', error)
callback()
}
})
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 插件使用
// vue.config.js
const plugin = require('./plugins/UploadPagePlugin.js')
module.exports = {
chainWebpack(config){
config.plugin('UploadPagePlugin').use(plugin.UploadPagePlugin, [process.env.NODE_ENV === 'staging'])
}
}
2
3
4
5
6
7
8
9
插件比较简单,就是生成了一个文件,这里开放了个参数给使用者传递,来确认是否激活插件的功能。在使用的代码里我判断了只有测试环境才激活插件,这里的条件可自行更改。
接下来就是需要写一段轮询代码了
# 轮询
轮询的代码可简单可复杂,实现主要的功能点就行了。这里我写的比较复杂一点,支持配置化,对后续扩展比较友好
const axios = require('axios')
class VersionChecker {
constructor(options = {}) {
this.options = Object.assign(
{
versionUrl: '/version.js', // 版本文件路径
checkInterval: 5 * 60 * 1000, // 默认5分钟检查一次
onNewVersion: null // 发现新版本时的回调函数
},
options
)
this.currentVersion = null // 当前版本
this.checkTimer = null // 轮询的定时器
this.isFirstCheck = true // 是否是第一次查询版本
}
// 开始轮询检查
startChecking() {
// 立即执行一次检查
this.checkVersion()
// 设置定时检查
this.checkTimer = setInterval(() => {
this.checkVersion()
}, this.options.checkInterval)
// 当页面从隐藏变为可见时,也触发检查
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
this.checkVersion()
}
})
}
// 停止轮询检查
stopChecking() {
if (this.checkTimer) {
clearInterval(this.checkTimer)
this.checkTimer = null
}
}
// 检查版本
async checkVersion() {
try {
// 添加时间戳防止缓存
const timestamp = new Date().getTime()
const response = await axios.get(`${this.options.versionUrl}?t=${timestamp}`)
// 从响应中提取版本号
let versionData = response.data
// 如果返回的是字符串(如export const version = 1234567890),需要提取数字部分
if (typeof versionData === 'string') {
const match = versionData.match(/version\s*=\s*(\d+)/)
if (match && match[1]) {
versionData = match[1]
}
}
// 首次检查,记录当前版本
if (this.isFirstCheck) {
this.currentVersion = versionData
this.isFirstCheck = false
console.log('初始版本:', this.currentVersion)
return
}
// 比较版本
if (versionData !== this.currentVersion) {
console.log('检测到新版本:', versionData, '当前版本:', this.currentVersion)
// 如果设置了回调,则调用
if (typeof this.options.onNewVersion === 'function') {
this.options.onNewVersion(versionData, this.currentVersion)
}
// 更新当前版本
this.currentVersion = versionData
// 发现新版本后结束轮询
this.stopChecking()
}
} catch (error) {
console.error('检查版本失败:', error)
}
}
}
export default VersionChecker
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
这里我写了一个VersionChecker
类,支持外部传入请求路径,轮询时间,以及发现新版本后的回调。整体的逻辑也很简单,就是
首次执行代码的时候会去请求一次远端的version
文件,然后记录下来里面的内容,后续每个一段时间去请求这个version
文件,并比对二者内容是否一致
然后调用实例化VersionChecker
的时候传入的回调函数。
// main.js
// 导入版本检测器
import VersionChecker from '@/utils/versionChecker'
import { MessageBox } from 'bi-eleme'
// 创建版本检测器实例
const versionChecker = new VersionChecker({
versionUrl: '/version.js', // 版本文件的URL
checkInterval: 5 * 60 * 1000, // 5分钟检查一次
onNewVersion: () => {
// 当检测到新版本时,提示用户刷新页面
MessageBox.confirm(
'系统已更新到新版本,请刷新页面以获取最新功能和修复。',
'发现新版本',
{
confirmButtonText: '立即刷新',
cancelButtonText: '稍后刷新',
type: 'warning'
}
).then(() => {
// 用户点击确认,刷新页面
window.location.reload(true)
}).catch(() => {
// 用户点击取消,不做操作
})
}
})
// 启动版本检测
versionChecker.startChecking()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
至此我们的功能就已经完善了。
# 总结
实现此功能的方案有很多种,但是没有完美的方案,每一种都有弊端,例如通过用户主动触发这种方式是有滞后性的,如果用户长时间停留在网页上 不做任何操作是没法感知道版本有更新的。轮询的方式又会对性能造成一定的损耗,大家可自行选择合适的方案食用