前端重新部署如何使用WebWorker優(yōu)雅地通知用戶刷新網(wǎng)頁?
本文對(duì)部署后通知用戶刷新網(wǎng)頁感興趣的小伙伴閱讀。
歡迎關(guān)注 前端早茶 ,與廣東靚仔攜手共同進(jìn)階~
文章轉(zhuǎn)載于:Zayn
https://juejin.cn/post/7329280514628534313
前言
周五晚上組里說前端有bug,正在吃宵夜的我眉頭一緊,立即打開了釘釘(手賤...),看了一下這不是前幾天剛解決的嗎,果然,使用刷新大法就解決,原因不過是用戶一直停留在頁面上,新的版本發(fā)布后,沒有刷新拿不到新的資源。現(xiàn)在大部分的前端系統(tǒng)都是SPA,用戶在使用中對(duì)系統(tǒng)更新無感知,切換菜單等并不能獲取最新資源,如果前端是覆蓋性部署,切換菜單請(qǐng)求舊資源,這個(gè)舊資源已經(jīng)被覆蓋(hash打包的文件),還會(huì)出現(xiàn)一直無響應(yīng)的情況。那么,當(dāng)前端部署更新后,提示一直停留在系統(tǒng)中的用戶刷新系統(tǒng)很有必要。解決方案
-
在public文件夾下加入manifest.json文件,記錄版本信息
-
前端打包的時(shí)候向manifest.json寫入當(dāng)前時(shí)間戳信息
-
在入口JS引入檢查更新的邏輯,有更新則提示更新
- 路由守衛(wèi)router.beforeResolve(Vue-Router為例),檢查更新,對(duì)比manifest.json文件的響應(yīng)頭Etag判斷是否有更新
- 通過Worker輪詢,檢查更新,對(duì)比manifest.json文件的響應(yīng)頭Etag判斷是否有更新。當(dāng)然你如果不在乎這點(diǎn)點(diǎn)開銷,可不使用Worker另開一個(gè)線程
Public下的加入manifest.json文件
{
"timestamp":1706518420707,
"msg":"更新內(nèi)容如下:\n--1.添加系統(tǒng)更新提示機(jī)制"
}
這里如果是不向用戶提示更新內(nèi)容,可不填,前段開發(fā)者也無需維護(hù)manifest.json的msg內(nèi)容,這里主要考慮到如果用戶在填長表單的時(shí)候,填了一大半,你這時(shí)候給用戶彈個(gè)更新提示,用戶無法判斷是否影響當(dāng)前表單填寫提交,如果將更新信息展示出來,用戶感知更新內(nèi)容,可判斷是否需要立即刷新,還是提交完表單再刷新。
webpack向manifest.json寫入當(dāng)前時(shí)間戳信息
// 版本號(hào)文件
const filePath = path.resolve(`./public`, 'manifest.json')
// 讀取文件內(nèi)容
readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('讀取文件時(shí)出錯(cuò):', err)
return
}
// 將文件內(nèi)容轉(zhuǎn)換JSON
const dataObj = JSON.parse(data)
dataObj.timestamp = new Date().getTime()
// 將修改后的內(nèi)容寫回文件
writeFile(filePath, JSON.stringify(dataObj), 'utf8', err => {
if (err) {
console.error('寫入文件時(shí)出錯(cuò):', err)
return
}
})
})
如果你無需維護(hù)更新內(nèi)容的話,可直接寫入timestamp
// 生成版本號(hào)文件
const filePath = path.resolve(`./public`, 'manifest.json')
writeFileSync(filePath, `${JSON.stringify({ timestamp: new Date().getTime() })}`)
檢查更新的邏輯
入口文件main.js處引入
我這里檢查更新的文件是放在utils/checkUpdate
// 檢查版本更新
import '@/utils/checkUpdate'
checkUpdate文件內(nèi)容如下
import router from '@/router'
import { Modal } from 'ant-design-vue'
if (process.env.NODE_ENV === 'production') {
let lastEtag = ''
let hasUpdate = false
let worker = null
async function checkUpdate() {
try {
// 檢測前端資源是否有更新
let response = await fetch(`/manifest.json?v=${Date.now()}`, {
method: 'head'
})
// 獲取最新的etag
let etag = response.headers.get('etag')
hasUpdate = lastEtag && etag !== lastEtag
lastEtag = etag
} catch (e) {
return Promise.reject(e)
}
}
async function confirmReload(msg = '', lastEtag) {
worker &&
worker.postMessage({
type: 'pause'
})
try {
Modal.confirm({
title: '溫馨提示',
content: '系統(tǒng)后臺(tái)有更新,請(qǐng)點(diǎn)擊“立即刷新”刷新頁面\n' + msg,
okText: '立即刷新',
cancelText: '5分鐘后提示我',
onOk() {
worker.postMessage({
type: 'destroy'
})
location.reload()
},
onCancel() {
worker &&
worker.postMessage({
type: 'recheck',
lastEtag: lastEtag
})
}
})
} catch (e) {}
}
// 路由攔截
router.beforeResolve(async (to, from, next) => {
next()
try {
await checkUpdate()
if (hasUpdate) {
worker.postMessage({
type: 'destroy'
})
location.reload()
}
} catch (e) {}
})
// 利用worker輪詢
worker = new Worker(
/* webpackChunkName: "checkUpdate.worker" */ new URL('../worker/checkUpdate.worker.js', import.meta.url)
)
worker.postMessage({
type: 'check'
})
worker.onmessage = ({ data }) => {
if (data.type === 'hasUpdate') {
hasUpdate = true
confirmReload(data.msg, data.lastEtag)
}
}
}
這里因?yàn)槿睋Q路由本來就要刷新頁面,用戶可無需感知系統(tǒng)更新信息,直接通過請(qǐng)求頭的Etag即可,這里的Fetch方法就用head獲取相應(yīng)頭就好了。
checkUpdate.worker.js文件如下
let lastEtag
let hasUpdate = false
let intervalId = ''
async function checkUpdate() {
try {
// 檢測前端資源是否有更新
let response = await fetch(`/manifest.json?v=${Date.now()}`, {
method: 'get'
})
// 獲取最新的etag和data
let etag = response.headers.get('etag')
let data = await response.json()
hasUpdate = lastEtag !== undefined && etag !== lastEtag
if (hasUpdate) {
postMessage({
type: 'hasUpdate',
msg: data.msg,
lastEtag: lastEtag,
etag: etag
})
}
lastEtag = etag
} catch (e) {
return Promise.reject(e)
}
}
// 監(jiān)聽主線程發(fā)送過來的數(shù)據(jù)
addEventListener('message', ({ data }) => {
if (data.type === 'check') {
// 每5分鐘執(zhí)行一次
// 立即執(zhí)行一次,獲取最新的etag,避免在setInterval等待中系統(tǒng)更新,第一次獲取的etag是新的,但是lastEtag還是undefined,不滿足條件,錯(cuò)失刷新時(shí)機(jī)
checkUpdate()
intervalId = setInterval(checkUpdate,5 * 60 * 1000)
}
if (data.type === 'recheck') {
// 每5分鐘執(zhí)行一次
hasUpdate = false
lastEtag = data.lastEtag
intervalId = setInterval(checkUpdate, 5 * 60 * 1000)
}
if (data.type === 'pause') {
clearInterval(intervalId)
}
if (data.type === 'destroy') {
clearInterval(intervalId)
close()
}
})
如果不使用worker直接講輪詢邏輯放在checkUpdate即可
Worker引入
從 webpack 5 開始,你可以使用 Web Workers [1] 代替 `worker-loader` [2]。
new Worker(new URL('./worker.js', import.meta.url));
以下版本的就只能用`worker-loader`[3]咯
也可以邏輯寫成字符串,然后通過ToURL給new Worker,如下:
function createWorker(f) {
const blob = new Blob(['(' + f.toString() +')()'], {type: "application/javascript"});
const blobUrl = window.URL.createObjectURL(blob);
const worker = new Worker(blobUrl);
return worker;
}
createWorker(function () {
self.addEventListener('message', function (event) {
// 消費(fèi)信息
self.postMessage('send message')
}, false);
})
worker數(shù)據(jù)通信
// 主線程
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);
// Worker 線程
self.onmessage = function (e) {
var uInt8Array = e.data;
postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};
但是,拷貝方式發(fā)送二進(jìn)制數(shù)據(jù),會(huì)造成性能問題。比如,主線程向 Worker 發(fā)送一個(gè) 500MB 文件,默認(rèn)情況下瀏覽器會(huì)生成一個(gè)原文件的拷貝。為了解決這個(gè)問題,JavaScript 允許主線程把二進(jìn)制數(shù)據(jù)直接轉(zhuǎn)移給子線程,但是一旦轉(zhuǎn)移,主線程就無法再使用這些二進(jìn)制數(shù)據(jù)了,這是為了防止出現(xiàn)多個(gè)線程同時(shí)修改數(shù)據(jù)的麻煩局面。這種轉(zhuǎn)移數(shù)據(jù)的方法,叫做Transferable Objects[4]。這使得主線程可以快速把數(shù)據(jù)交給 Worker,對(duì)于影像處理、聲音處理、3D 運(yùn)算等就非常方便了,不會(huì)產(chǎn)生性能負(fù)擔(dān)。
如果要直接轉(zhuǎn)移數(shù)據(jù)的控制權(quán),就要使用下面的寫法。
// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);
// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
Web Worker 使用教程 - 阮一峰的網(wǎng)絡(luò)日志 (ruanyifeng.com) [5]然而,并不是所有的對(duì)象都可以被轉(zhuǎn)移。只有那些被設(shè)計(jì)為可轉(zhuǎn)移的對(duì)象(用[ Transferable ] IDL 擴(kuò)展屬性修飾),比如ArrayBuffer、MessagePort,ImageBitmap,OffscreenCanvas,才能通過這種方式來傳遞。轉(zhuǎn)移操作是不可逆的,一旦對(duì)象被轉(zhuǎn)移,原始上下文中的引用將不再有效。轉(zhuǎn)移對(duì)象可以顯著減少復(fù)制數(shù)據(jù)所需的時(shí)間和內(nèi)存。
參考資料
[1]https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers: https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FAPI%2FWeb_Workers_API%2FUsing_web_workers
[2]https://github.com/webpack-contrib/worker-loader: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fwebpack-contrib%2Fworker-loader
[3]https://github.com/webpack-contrib/worker-loader: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fwebpack-contrib%2Fworker-loader
[4]http://www.w3.org/html/wg/drafts/html/master/infrastructure.html#transferable-objects: https://link.juejin.cn?target=http%3A%2F%2Fwww.w3.org%2Fhtml%2Fwg%2Fdrafts%2Fhtml%2Fmaster%2Finfrastructure.html%23transferable-objects
[5]https://www.ruanyifeng.com/blog/2018/07/web-worker.html: https://link.juejin.cn?target=https%3A%2F%2Fwww.ruanyifeng.com%2Fblog%2F2018%2F07%2Fweb-worker.html
最后
關(guān)注我,一起攜手進(jìn)階
歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~
