- A+
领导:为什么每次项目部署后,有的用户要清缓存才能看到最新的页面
我:浏览器有默认的缓存策略,如果服务器在响应头中没有禁用缓存,那么浏览器每次请求页面会先看看缓存里面有没有,有的话从缓存取,造成还是取的旧页面。正常来说,用户只需要点击刷新按钮,刷新一下页面就好了,不必清除浏览器缓存刷新。
领导:为什么缓存这么严重,有的用户清除缓存刷新还是不行,关掉浏览器重新进来还是不行,要重启电脑才有效。
我:要重启电脑?这 。。。。。。用户都这样么,还是只有一小部分用户。
领导:不是所有的用户,有个别用户会出现这种情况
我:那可能得到用户电脑上看看了
每次需求投产后,因为有缓存问题导致用户看到的还是旧版内容,使用过程中出现了问题,联系我们才知道项目更新了,用户体验不好;
于是查找资料,寻找合适的方案,根据 评论区 的讨论,实践总结了下面 3 种前端部署后页面检测版本更新的方法
当检测到版本更新则及时通知用户,用户可以选择是否立即更新,并不会影响用户当前进行的业务;
下面以 vue 项目为例
1、轮询打包后的 index.html,比较生成的 js 文件的 hash
项目打包后,index.html 会包含打包后的 js 文件,这些文件的文件名包含的 hash 将会和上一次打包的不同,比较 hash 也就能判断是否有版本更新;
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()
得到的结果示例如下:
[ '<script src="/js/chunk-vendors.1234fff.js"></script>', '<script src="/js/app.1234fff.js"></script>', ]
改动了一点业务代码后,再次打包,上面 app.js 的 hash 就会发生变化
[ '<script src="/js/chunk-vendors.1234fff.js"></script>', '<script src="/js/app.12ed5ca.js"></script>', ]
比较两个的结果,如果结果不一样,则代表有版本更新。
2、HEAD 方法轮询响应头中的 etag
ETag
是资源的特定版本的标识符。当资源内容发生变化时,会生成新的 ETag
;
HEAD
方法请求资源的响应头信息,服务器不会返回响应体,可以节省带宽资源;
这里可以轮询打包后的 index.html,取两次响应头中的 eTag
比较,如果不同,说明版本更新了;前提是服务器没有禁用缓存。
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
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'
文件类似这样
// 示例 { "commitHash" : "234fjsdr322f32f322f32f3g32g23jglk32gjkl32lg3" }
项目改动后,提交改动的地方后,再次打包,会将最新的 commit hash
存入到 public/versionHash.json
// 示例 { "commitHash" : "234fjsdr322f3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" }
然后在页面中轮询 '/versionHash.json'
,比较 commit hash
,检测是否有更新
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')
捕获资源加载错误
window.addEventListener('error',(event) =>{ // 检测版本更新 // window.location.reload() },true)
2、路由切换发生错误时
和上面的 addEventListener('error')
捕获资源加载错误类似, vue-router
的 router.onError()
方法可以捕获到路由加载的错误。
路由切换时某些资源加载失败,会抛出 Loading chunk chunk-xxxx failed
,可以用正则匹配它并作相应处理;
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
事件;
document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { // 开始检测更新 } else { // 结束检测更新 } }); document.addEventListener('focus',() =>{ // 开始检测更新 })
关于禁用缓存
禁用 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 设置禁用缓存
// 配置 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
,优势是只需要取响应头中的字段,服务器不需要返回响应体,节约资源;