实践总结 3 种前端部署后页面检测版本的方法

发布时间 2024-01-11 12:48:44作者: xingba-coder

领导:为什么每次项目部署后,有的用户要清缓存才能看到最新的页面

我:浏览器有默认的缓存策略,如果服务器在响应头中没有禁用缓存,那么浏览器每次请求页面会先看看缓存里面有没有,有的话从缓存取,造成还是取的旧页面。正常来说,用户只需要点击刷新按钮,刷新一下页面就好了,不必清除浏览器缓存刷新。

领导:为什么缓存这么严重,有的用户清除缓存刷新还是不行,关掉浏览器重新进来还是不行,要重启电脑才有效。

我:要重启电脑?这 。。。。。。用户都这样么,还是只有一小部分用户。

领导:不是所有的用户,有个别用户会出现这种情况

我:那可能得到用户电脑上看看了

每次需求投产后,因为有缓存问题导致用户看到的还是旧版内容,使用过程中出现了问题,联系我们才知道项目更新了,用户体验不好;

于是查找资料,寻找合适的方案,根据 评论区 的讨论,实践总结了下面 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 方法请求资源的响应头信息,服务器不会返回响应体,可以节省带宽资源;

1.png

这里可以轮询打包后的 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-routerrouter.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,优势是只需要取响应头中的字段,服务器不需要返回响应体,节约资源;