实践总结 3 种前端部署后页面检测版本的方法
領(lǐng)導(dǎo):為什么每次項(xiàng)目部署后,有的用戶要清緩存才能看到最新的頁(yè)面
我:瀏覽器有默認(rèn)的緩存策略,如果服務(wù)器在響應(yīng)頭中沒(méi)有禁用緩存,那么瀏覽器每次請(qǐng)求頁(yè)面會(huì)先看看緩存里面有沒(méi)有,有的話從緩存取,造成還是取的舊頁(yè)面。正常來(lái)說(shuō),用戶只需要點(diǎn)擊刷新按鈕,刷新一下頁(yè)面就好了,不必清除瀏覽器緩存刷新。
領(lǐng)導(dǎo):為什么緩存這么嚴(yán)重,有的用戶清除緩存刷新還是不行,關(guān)掉瀏覽器重新進(jìn)來(lái)還是不行,要重啟電腦才有效。
我:要重啟電腦?這 。。。。。。用戶都這樣么,還是只有一小部分用戶。
領(lǐng)導(dǎo):不是所有的用戶,有個(gè)別用戶會(huì)出現(xiàn)這種情況
我:那可能得到用戶電腦上看看了
每次需求投產(chǎn)后,因?yàn)橛芯彺鎲?wèn)題導(dǎo)致用戶看到的還是舊版內(nèi)容,使用過(guò)程中出現(xiàn)了問(wèn)題,聯(lián)系我們才知道項(xiàng)目更新了,用戶體驗(yàn)不好;
于是查找資料,尋找合適的方案,根據(jù) 評(píng)論區(qū) 的討論,實(shí)踐總結(jié)了下面 3 種前端部署后頁(yè)面檢測(cè)版本更新的方法
當(dāng)檢測(cè)到版本更新則及時(shí)通知用戶,用戶可以選擇是否立即更新,并不會(huì)影響用戶當(dāng)前進(jìn)行的業(yè)務(wù);
下面以 vue 項(xiàng)目為例
1、輪詢打包后的 index.html,比較生成的 js 文件的 hash
項(xiàng)目打包后,index.html 會(huì)包含打包后的 js 文件,這些文件的文件名包含的 hash 將會(huì)和上一次打包的不同,比較 hash 也就能判斷是否有版本更新;
let firstV = [] //記錄初始獲得的 script 文件字符串
let currentv = [] //記錄當(dāng)前獲得的 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 內(nèi)容,匹配 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)
// 當(dāng)前 script hash 和初始的不同時(shí),說(shuō)明已經(jīng)更新
if(!isEqual(firstV, currentv)) {
console.log('已更新')
}
},3000)
}
// 文檔可見(jiàn)時(shí)檢測(cè)版本是否更新
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
checkIfNewVersion();
} else {
window.checkVersionInterval && clearInterval(window.checkVersionInterval)
}
});
getHtml() 得到的結(jié)果示例如下:
[
'<script src="/js/chunk-vendors.1234fff.js"></script>',
'<script src="/js/app.1234fff.js"></script>',
]
改動(dòng)了一點(diǎn)業(yè)務(wù)代碼后,再次打包,上面 app.js 的 hash 就會(huì)發(fā)生變化
[
'<script src="/js/chunk-vendors.1234fff.js"></script>',
'<script src="/js/app.12ed5ca.js"></script>',
]
比較兩個(gè)的結(jié)果,如果結(jié)果不一樣,則代表有版本更新。
2、HEAD 方*詢響應(yīng)頭中的 etag
ETag 是資源的特定版本的標(biāo)識(shí)符。當(dāng)資源內(nèi)容發(fā)生變化時(shí),會(huì)生成新的 ETag;HEAD?方法請(qǐng)求資源的響應(yīng)頭信息,服務(wù)器不會(huì)返回響應(yīng)體,可以節(jié)省帶寬資源;
這里可以輪詢打包后的 index.html,取兩次響應(yīng)頭中的 eTag 比較,如果不同,說(shuō)明版本更新了;前提是服務(wù)器沒(méi)有禁用緩存。
let firstEtag = `` //記錄第一次進(jìn)來(lái)請(qǐng)求獲得的 etag
let currentEtag = `` //記錄當(dāng)前的 etag,會(huì)不斷的刷新
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() =>{
// 每隔一定時(shí)間請(qǐng)求最新的 etag
currentEtag = await getEtag()
// 當(dāng)前最新的 currentEtag 和初始 firstEtag 進(jìn)行比較,不同則說(shuō)明資源更新了;
if(firstEtag && currentEtag && firstEtag!==currentEtag){
console.log('已更新')
}
},3000)
}
// 文檔可見(jiàn)時(shí)檢測(cè)版本是否更新
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
checkEtag();
} else {
window.checkEtagInterval && clearInterval(window.checkEtagInterval)
}
});
3、監(jiān)聽(tīng) git commit hash 變化
項(xiàng)目改動(dòng)提交 git 時(shí)會(huì)生成唯一的 hash 字符串,將最近提交的 commit hash 作為版本號(hào)保存在一個(gè) json 文件中;通過(guò)輪詢 json 文件,檢測(cè)里面的版本號(hào)是否和上次不同,不同則表示有版本更新;
監(jiān)聽(tīng) git commit hash 變化的好處是只要投產(chǎn)的版本有 git 提交記錄,而不管靜態(tài)文件變化還是代碼變化,都能檢測(cè)到版本更新;
在 vue.config.js 中引入 git-revision-webpack-plugin,該插件可獲取到項(xiàng)目本地 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 文件中;
項(xiàng)目打包會(huì)執(zhí)行上面的代碼,生成后的 'versionHash.json' 文件類似這樣
// 示例
{ "commitHash" : "234fjsdr322f32f322f32f3g32g23jglk32gjkl32lg3" }
項(xiàng)目改動(dòng)后,提交改動(dòng)的地方后,再次打包,會(huì)將最新的 commit hash 存入到 public/versionHash.json
// 示例
{ "commitHash" : "234fjsdr322f3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" }
然后在頁(yè)面中輪詢 '/versionHash.json',比較 commit hash ,檢測(cè)是否有更新
let firstCommitHash = ``
let currentCommitHash = ``
async function getCommitHash() {
// 避免瀏覽器緩存加上時(shí)間戳參數(shù)
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('已更新')
// 作相應(yīng)處理
}
}, 3000)
}
關(guān)于檢測(cè)版本更新的時(shí)機(jī)
檢測(cè)時(shí)機(jī),我覺(jué)得有三種比較合適,可以靈活搭配上面的方法使用
- 資源加載錯(cuò)誤時(shí)(常常發(fā)生在切換菜單時(shí)),檢測(cè)版本更新
- 路由切換發(fā)生錯(cuò)誤時(shí)(也發(fā)生在切換菜單時(shí)或者當(dāng)前頁(yè)面引用其他路由時(shí)),檢測(cè)版本更新
- 監(jiān)聽(tīng)
visibilitychange + focus事件
1、資源加載錯(cuò)誤時(shí)
前端部署后,某些資源已經(jīng)更新,當(dāng)切換菜單時(shí),可能會(huì)出現(xiàn)資源加載失敗的錯(cuò)誤(404)。此時(shí)可以使用 addEventListener('error') 捕獲資源加載錯(cuò)誤
window.addEventListener('error',(event) =>{
// 檢測(cè)版本更新
// window.location.reload()
},true)
2、路由切換發(fā)生錯(cuò)誤時(shí)
和上面的 addEventListener('error') 捕獲資源加載錯(cuò)誤類似, vue-router 的 router.onError() 方法可以捕獲到路由加載的錯(cuò)誤。
路由切換時(shí)某些資源加載失敗,會(huì)拋出 Loading chunk chunk-xxxx failed,可以用正則匹配它并作相應(yīng)處理;
router.onError((error) =>{
let reg = /Loading.*?failed/g
if(reg.test(error)){
// 檢測(cè)版本更新
// window.location.reload()
}
})
3、監(jiān)聽(tīng) visibilitychange + focus 事件
visibilitychange:當(dāng)其選項(xiàng)卡的內(nèi)容變得可見(jiàn)或被隱藏時(shí),會(huì)在 document 上觸發(fā)?visibilitychange?事件。
當(dāng)用戶導(dǎo)航到新頁(yè)面、切換標(biāo)簽頁(yè)、關(guān)閉標(biāo)簽頁(yè)、最小化或關(guān)閉瀏覽器,或者在移動(dòng)設(shè)備上從瀏覽器切換到不同的應(yīng)用程序時(shí),該事件就會(huì)觸發(fā),其?
visibilityState?為?hidden
在 pc 端,從瀏覽器切換到其他應(yīng)用程序并不會(huì)觸發(fā) visibilitychange 事件,所以加以 focus 輔佐;當(dāng)鼠標(biāo)點(diǎn)擊過(guò)當(dāng)前頁(yè)面(必須 focus 過(guò)),此時(shí)切換到其他應(yīng)用會(huì)觸發(fā)頁(yè)面的 blur 實(shí)踐;再次切回到瀏覽器則會(huì)觸發(fā) focus 事件;
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
// 開(kāi)始檢測(cè)更新
} else {
// 結(jié)束檢測(cè)更新
}
});
document.addEventListener('focus',() =>{
// 開(kāi)始檢測(cè)更新
})
關(guān)于禁用緩存
禁用 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 設(shè)置了 "max-age" 或者 "s-max-age" 指令,那么?`Expires`?頭會(huì)被忽略。-->
<meta http-equiv="Expires" content="0">
如果只在 html 中設(shè)置這個(gè)的話,只在 IE 中有效;若要在其他瀏覽器中生效,則需要對(duì)服務(wù)器設(shè)置禁用緩存;
nginx 設(shè)置禁用緩存
// 配置 html 和 htm 文件不緩存
location / {
root html;
index index.html index.htm;
add_header Cache-Control "no-cache,no-store,must-revalidate";
}
總結(jié)
本文總結(jié)了 3 種前端部署后頁(yè)面檢測(cè)版本更新的方法;
- 輪詢打包后的 index.html,比較生成的 js 文件的 hash
- HEAD 方*詢響應(yīng)頭中的
etag - 監(jiān)聽(tīng)
git commit hash變化
3 種都有用武之地,看具體場(chǎng)景和需求;
監(jiān)聽(tīng) git commit hash 變化優(yōu)勢(shì)是可以檢測(cè)到靜態(tài)資源的變化;
HEAD 方*詢響應(yīng)頭中的 etag,優(yōu)勢(shì)是只需要取響應(yīng)頭中的字段,服務(wù)器不需要返回響應(yīng)體,節(jié)約資源;
總結(jié)
以上是生活随笔為你收集整理的实践总结 3 种前端部署后页面检测版本的方法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【技术推荐】我愿称之为开源界最好用的行为
- 下一篇: 实时数据流无忧:用 SpringBoot