前言

在传统的多页Web应用中,每次用户访问页面时,都会从服务器获取最新的页面和资源,因此版本更新相对简单,用户总是能获取到最新的版本。然而,SPA在首次加载后,前端的静态资源会缓存在浏览器内存中,且在整个使用过程中通常不会自动重新加载。这种特性意味着如果应用有新的版本发布,用户可能仍在使用旧版本,无法立即获得最新的功能、修复或安全更新。

那么,在我们部署之后,如何提醒用户版本更新,并引导用户刷新页面呢?

手搓

比较构建文件的hash值

这里用轮询的方式请求index.html文件,从中解析里面所有的js文件,由于vue打包后每个js文件都有指纹标识,因此对比每次打包后的指纹,分析文件是否存在变动,如果有变动则提示用户更新!(该方案需要webpack/vite开启打包文件带上hash值)

jsHash
1
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
// utils.js
import { MessageBox } from 'element-ui';

const modelConfirm = (title, message, type='warning') => {
return new Promise((resolve, reject) => {
MessageBox.confirm(message, title, {
type
}).then(() => {
refreshPage();
}).catch(() => {
reject();
});
});
};


function refreshPage() {
// 清除http缓存(请求带时间戳的首页,防止缓存)
const timestamp = new Date().getTime();
window.location.href = window.location.pathname + '?t=' + timestamp;
location.reload();
// 清除 Web Storage
localStorage.clear();
sessionStorage.clear();
}


// 存储当前脚本标签的哈希值集合
let scriptHashes = new Set();
let timer = undefined;

async function fetchScriptHashes() {
// 请求带时间戳的首页,防止缓存
const html = await fetch('/?_timestamp=' + Date.now()).then((resp) => resp.text());
// 正则表达式匹配所有<script>标签
const scriptRegex = /<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/gi;
// 获取匹配到的所有<script>标签内容
const scripts = html.match(scriptRegex) ?? [];
// 将脚本标签内容存入集合并返回
return new Set(scripts);
}


/**
* 比较当前脚本标签的哈希值集合与新获取的集合,检测是否有更新
*/
async function compareScriptHashes() {
// 获取新的脚本标签哈希值集合
const newScriptHashes = await fetchScriptHashes();

if (scriptHashes.size === 0) {
// 初次运行时,存储当前脚本标签哈希值
scriptHashes = newScriptHashes;
} else if (
scriptHashes.size !== newScriptHashes.size ||
![...scriptHashes].every((hash) => newScriptHashes.has(hash))
) {
// 如果脚本标签数量或内容发生变化,则认为有更新
console.info('更新了', {
oldScript: [...scriptHashes],
newScript: [...newScriptHashes],
});
// 清除定时器
clearInterval(timer);
// 提示用户更新
modelConfirm('更新提示','检测到页面有内容更新,为了功能的正常使用,是否立即刷新?');
} else {
// 没有更新
console.info(`没更新${new Date().toLocaleString()}`, {
oldScript: [...scriptHashes],
});
}
}

// 每10秒检查一次是否有新的脚本标签更新
export function autoRefresh() {
timer = setInterval(compareScriptHashes, 10000);
}
1
2
3
4
5
// main.js
import { autoRefresh } from "@/utils/index.js";
if(process.env.NODE_ENV === 'production'){
autoRefresh();
}

效果:

image-20250727134458379

比较Etag或last-modified

利用HTTP协议的缓存机制,比较Etag或last-modified前后是否一致。(经测试,即使代码内容不做任何修改, ETag 和 Last-Modified 的值每次打包完都不一样;而js的hash值如果内容不做任何修改,hash值也不会修改)

1
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
// utils.js
import { MessageBox } from 'element-ui';

const modelConfirm = (title, message, type = 'warning') => {
return new Promise((resolve, reject) => {
MessageBox.confirm(message, title, {
type,
})
.then(() => {
refreshPage();
})
.catch(() => {
reject();
});
});
};

function refreshPage() {
// 清除http缓存(请求带时间戳的首页,防止缓存)
const timestamp = new Date().getTime();
window.location.href = window.location.pathname + '?t=' + timestamp;
location.reload();
// 清除 Web Storage
localStorage.clear();
sessionStorage.clear();
}

let versionTag = null; // 版本标识
let timer = undefined;

async function fetchVersionTag() {
// 请求带时间戳的首页,防止缓存
const response = await fetch('/?_timestamp=' + Date.now(), {
cache: 'no-cache',
});
return response.headers.get('etag') || response.headers.get('last-modified');
}

/**
* 比较当前的 ETag 或 Last-Modified 值与最新获取的值
*/
async function compareVersionTag() {
// 获取新的脚本标签哈希值集合
const newVersionTag = await fetchVersionTag();

if (versionTag === null) {
// 初次运行时,存储当前的 ETag 或 Last-Modified 值
versionTag = newVersionTag;
} else if (versionTag !== newVersionTag) {
// 如果 ETag 或 Last-Modified 发生变化,则认为有更新
console.info('更新了', {
oldVersionTag: versionTag,
newVersionTag: newVersionTag,
});
// 清除定时器
clearInterval(timer);
// 提示用户更新
modelConfirm(
'更新提示',
'检测到页面有内容更新,为了功能的正常使用,是否立即刷新?',
);
} else {
// 没有更新
console.info(`没更新${new Date().toLocaleString()}`, {
oldVersionTag: versionTag,
});
}
}

// 每10秒检查一次是否有新的 ETag 或 Last-Modified 值
export function autoRefresh() {
timer = setInterval(compareVersionTag, 10000);
}
1
2
3
4
5
// main.js
import { autoRefresh } from "@/utils/index.js";
if(process.env.NODE_ENV === 'production'){
autoRefresh();
}

参考文献

第三方库

version-polling

1
2
# 本地项目安装
npm install version-polling --save
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// main.js
import { createVersionPolling } from 'version-polling';
import { MessageBox } from 'element-ui';

createVersionPolling({
silent: process.env.NODE_ENV === 'development', // 开发环境下不检测
onUpdate: (self) => {
MessageBox.confirm('检测到网页有更新, 是否刷新页面加载最新版本?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(() => {
self.onRefresh();
})
.catch(() => {
self.onCancel();
});
},
});

plugin-web-update-notification

看啥呢?(我没尝试,上面的够用了 (懒…))