领导:为什么每次项目部署后,有的用户要清缓存才能看到最新的页面
我:浏览器有默认的缓存策略,如果服务器在响应头中没有禁用缓存,那么浏览器每次请求页面会先看看缓存里面有没有,有的话从缓存取,造成还是取的旧页面。正常来说,用户只需要点击刷新按钮,刷新一下页面就好了,不必清除浏览器缓存刷新。
领导:为什么缓存这么严重,有的用户清除缓存刷新还是不行,关掉浏览器重新进来还是不行,要重启电脑才有效。
我:要重启电脑?这 。。。。。。用户都这样么,还是只有一小部分用户。
领导:不是所有的用户,有个别用户会出现这种情况
我:那可能得到用户电脑上看看了
每次需求投产后,因为有缓存问题导致用户看到的还是旧版内容,使用过程中出现了问题,联系我们才知道项目更新了,用户体验不好;
于是查找资料,寻找合适的方案,根据 评论区 的讨论,实践总结了下面 3 种前端部署后页面检测版本更新的方法
当检测到版本更新则及时通知用户,用户可以选择是否立即更新,并不会影响用户当前进行的业务;
下面以 vue 项目为例
1、轮询打包后的 index.html,比较生成的 js 文件的 hash
项目打包后,index.html 会包含打包后的 js 文件,这些文件的文件名包含的 hash 将会和上一次打包的不同,比较 hash 也就能判断是否有版本更新;
js
let firstV = [] //记录初始获得的 script 文件字符串
let currentv = [] //记录当前获得的 script 文件字符串
// 获得的文件字符串类似这样 `<script src="/js/chunk-vendors.1234fff.js"></script>`
async function getHtml() {
let res = await axios.get('/index.html?date=' + Date.now())
if (res.status == '200') {
let text = res.data
if (text) {
// 解析 html 内容,匹配 script 字符串
let reg = /<script([^>]+)><\/script>/ig
return text.match(reg)
}
}
return []
}
function isEqual(a, b) {
return a.length = Array.from(new Set(a.concat(b))).length
}
export async function checkIfNewVersion() {
firstV = await getHtml()
window.checkVersionInterval && clearInterval(window.checkVersionInterval)
window.checkVersionInterval = setInterval(async () =>{
currentV = await getHtml()
console.log(firstV,currentv)
// 当前 script hash 和初始的不同时,说明已经更新
if(!isEqual(firstV, currentv)) {
console.log('已更新')
}
},3000)
}
// 文档可见时检测版本是否更新
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
checkIfNewVersion();
} else {
window.checkVersionInterval && clearInterval(window.checkVersionInterval)
}
});
getHtml()
得到的结果示例如下:
js
[
'<script src="/js/chunk-vendors.1234fff.js"></script>',
'<script src="/js/app.1234fff.js"></script>',
]
改动了一点业务代码后,再次打包,上面 app.js 的 hash 就会发生变化
js
[
'<script src="/js/chunk-vendors.1234fff.js"></script>',
'<script src="/js/app.12ed5ca.js"></script>',
]
比较两个的结果,如果结果不一样,则代表有版本更新。
2、HEAD 方法轮询响应头中的 etag
ETag
是资源的特定版本的标识符。当资源内容发生变化时,会生成新的 ETag
;
HEAD
方法请求资源的响应头信息,服务器不会返回响应体,可以节省带宽资源;
这里可以轮询打包后的 index.html,取两次响应头中的 eTag
比较,如果不同,说明版本更新了;前提是服务器没有禁用缓存。
js
let firstEtag = `` //记录第一次进来请求获得的 etag
let currentEtag = `` //记录当前的 etag,会不断的刷新
async function getEtag(){
let res = await axios.head('/index.html')
if(res.status == '200'){
if(res.headers && res.headers.etag){
return res.headers.etag
}
}
return ''
}
export async function checkEtag() {
firstEtag = await getEtag()
window.checkEtagInterval && clearInterval(window.checkEtagInterval)
window.checkEtagInterval = setInterval(async() =>{
// 每隔一定时间请求最新的 etag
currentEtag = await getEtag()
// 当前最新的 currentEtag 和初始 firstEtag 进行比较,不同则说明资源更新了;
if(firstEtag && currentEtag && firstEtag!==currentEtag){
console.log('已更新')
}
},3000)
}
// 文档可见时检测版本是否更新
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
checkEtag();
} else {
window.checkEtagInterval && clearInterval(window.checkEtagInterval)
}
});
3、监听 git commit hash
变化
项目改动提交 git 时会生成唯一的 hash 字符串,将最近提交的 commit hash
作为版本号保存在一个 json 文件中;通过轮询 json 文件,检测里面的版本号是否和上次不同,不同则表示有版本更新;
监听 git commit hash
变化的好处是只要投产的版本有 git 提交记录,而不管静态文件变化还是代码变化,都能检测到版本更新;
在 vue.config.js 中引入 git-revision-webpack-plugin
,该插件可获取到项目本地 git 的最新提交 commit hash
js
const GitRevisionPlugin = require('git-revision-webpack-plugin')
const gitRevision = new GitRevisionPlugin()
const { writeFile , existsSync } = require('fs')
if(existsSync('./public')){
fs.writeFile(
'./public/version.json',
`{"commitHash":${JSON.stringify(gitRevision.commithash())}`,
(error) =>{}
)
}
上面代码使用 gitRevision.commithash()
获取 commit hash
,将其存入到 public/versionHash.json
文件中;
项目打包会执行上面的代码,生成后的 'versionHash.json'
文件类似这样
js
// 示例
{ "commitHash" : "234fjsdr322f32f322f32f3g32g23jglk32gjkl32lg3" }
项目改动后,提交改动的地方后,再次打包,会将最新的 commit hash
存入到 public/versionHash.json
js
// 示例
{ "commitHash" : "234fjsdr322f3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" }
然后在页面中轮询 '/versionHash.json'
,比较 commit hash
,检测是否有更新
js
let firstCommitHash = ``
let currentCommitHash = ``
async function getCommitHash() {
// 避免浏览器缓存加上时间戳参数
let res = await axios.get('/versionHash.json?date=' + Date.now())
if (res.status == '200') {
if (res.data && res.data.commitHash) {
return res.data.commitHash
}
}
return ''
}
export async function checkCommitHash() {
firstCommitHash = await getCommitHash()
window.checkCommitHash && clearInterval(window.checkCommitHash)
window.checkCommitHash = setInterval(async () => {
// 轮询 versionHash.json 文件
currentCommitHash = await getCommitHash()
if (firstCommitHash && currentCommitHash && firstCommitHash !== currentCommitHash) {
console.log('已更新')
// 作相应处理
}
}, 3000)
}
关于检测版本更新的时机
检测时机,我觉得有三种比较合适,可以灵活搭配上面的方法使用
- 资源加载错误时(常常发生在切换菜单时),检测版本更新
- 路由切换发生错误时(也发生在切换菜单时或者当前页面引用其他路由时),检测版本更新
- 监听
visibilitychange + focus
事件
1、资源加载错误时
前端部署后,某些资源已经更新,当切换菜单时,可能会出现资源加载失败的错误(404)。此时可以使用 addEventListener('error')
捕获资源加载错误
js
window.addEventListener('error',(event) =>{
// 检测版本更新
// window.location.reload()
},true)
2、路由切换发生错误时
和上面的 addEventListener('error')
捕获资源加载错误类似, vue-router
的 router.onError()
方法可以捕获到路由加载的错误。
路由切换时某些资源加载失败,会抛出 Loading chunk chunk-xxxx failed
,可以用正则匹配它并作相应处理;
js
router.onError((error) =>{
let reg = /Loading.*?failed/g
if(reg.test(error)){
// 检测版本更新
// window.location.reload()
}
})
3、监听 visibilitychange + focus
事件
visibilitychange
:当其选项卡的内容变得可见或被隐藏时,会在 document 上触发 visibilitychange
事件。
当用户导航到新页面、切换标签页、关闭标签页、最小化或关闭浏览器,或者在移动设备上从浏览器切换到不同的应用程序时,该事件就会触发,其
visibilityState
为hidden
在 pc 端,从浏览器切换到其他应用程序并不会触发 visibilitychange
事件,所以加以 focus
辅佐;当鼠标点击过当前页面 (必须 focus 过),此时切换到其他应用会触发页面的 blur
实践;再次切回到浏览器则会触发 focus
事件;
js
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
// 开始检测更新
} else {
// 结束检测更新
}
});
document.addEventListener('focus',() =>{
// 开始检测更新
})
关于禁用缓存
禁用 html 缓存
html
<!-- HTTP/1.1 -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<!-- HTTP/1.0; 与 Cache-Control: no-cache 效果一致 -->
<meta http-equiv="Pragma" content="no-cache">
<!-- 如果在 Cache-Control 设置了 "max-age" 或者 "s-max-age" 指令,那么 `Expires` 头会被忽略。-->
<meta http-equiv="Expires" content="0">
如果只在 html 中设置这个的话,只在 IE 中有效;若要在其他浏览器中生效,则需要对服务器设置禁用缓存;
nginx 设置禁用缓存
js
// 配置 html 和 htm 文件不缓存
location / {
root html;
index index.html index.htm;
add_header Cache-Control "no-cache,no-store,must-revalidate";
}
总结
本文总结了 3 种前端部署后页面检测版本更新的方法;
- 轮询打包后的 index.html,比较生成的 js 文件的 hash
- HEAD 方法轮询响应头中的
etag
- 监听
git commit hash
变化
3 种都有用武之地,看具体场景和需求;
监听 git commit hash
变化优势是可以检测到静态资源的变化;
HEAD 方法轮询响应头中的 etag
,优势是只需要取响应头中的字段,服务器不需要返回响应体,节约资源;