前言
周五晚上组里说前端有bug,正在吃宵夜的我眉头一紧,立即打开了钉钉(手贱…),看了一下这不是前几天刚解决的吗,果然,使用刷新大法就解决,原因不过是用户一直停留在页面上,新的版本发布后,没有刷新拿不到新的资源。
现在大部分的前端系统都是SPA,用户在使用中对系统更新无感知,切换菜单等并不能获取最新资源,如果前端是覆盖性部署,切换菜单请求旧资源,这个旧资源已经被覆盖(hash打包的文件),还会出现一直无响应的情况。
那么,当前端部署更新后,提示一直停留在系统中的用户刷新系统很有必要。
解决方案
- 在public文件夹下加入manifest.json文件,记录版本信息
- 前端打包的时候向manifest.json写入当前时间戳信息
- 在入口JS引入检查更新的逻辑,有更新则提示更新
- 路由守卫router.beforeResolve(Vue-Router为例),检查更新,对比manifest.json文件的响应头Etag判断是否有更新
- 通过Worker轮询,检查更新,对比manifest.json文件的响应头Etag判断是否有更新。当然你如果不在乎这点点开销,可不使用Worker另开一个线程
Public下的加入manifest.json文件
{ "timestamp":1706518420707, "msg":"更新内容如下:\n--1.添加系统更新提示机制" }
|
这里如果是不向用户提示更新内容,可不填,前段开发者也无需维护manifest.json的msg内容,这里主要考虑到如果用户在填长表单的时候,填了一大半,你这时候给用户弹个更新提示,用户无法判断是否影响当前表单填写提交,如果将更新信息展示出来,用户感知更新内容,可判断是否需要立即刷新,还是提交完表单再刷新。
webpack向manifest.json写入当前时间戳信息
const filePath = path.resolve(`./public`, 'manifest.json') readFile(filePath, 'utf8', (err, data) => { if (err) { console.error('读取文件时出错:', err) return } const dataObj = JSON.parse(data) dataObj.timestamp = new Date().getTime() writeFile(filePath, JSON.stringify(dataObj), 'utf8', err => { if (err) { console.error('写入文件时出错:', err) return } }) })
|
如果你无需维护更新内容的话,可直接写入timestamp
const filePath = path.resolve(`./public`, 'manifest.json') writeFileSync(filePath, `${JSON.stringify({ timestamp: new Date().getTime() })}`)
|
检查更新的逻辑
入口文件main.js处引入
我这里检查更新的文件是放在utils/checkUpdate
// 检查版本更新 import '@/utils/checkUpdate'
|
checkUpdate文件内容如下
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' }) 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: '系统后台有更新,请点击“立即刷新”刷新页面\n' + msg, okText: '立即刷新', cancelText: '5分钟后提示我', onOk() { worker.postMessage({ type: 'destroy' }) location.reload() }, onCancel() { worker && worker.postMessage({ type: 'recheck', lastEtag: lastEtag }) } }) } catch (e) {} }
router.beforeEach(async (to, from, next) => { next() try { await checkUpdate() if (hasUpdate) { worker.postMessage({ type: 'destroy' }) location.reload() } } catch (e) {} })
worker = new 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) } } }
|
这里因为缺换路由本来就要刷新页面,用户可无需感知系统更新信息,直接通过请求头的Etag即可,这里的Fetch方法就用head获取相应头就好了。
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' }) 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) } }
addEventListener('message', ({ data }) => { if (data.type === 'check') { checkUpdate() intervalId = setInterval(checkUpdate,5 * 60 * 1000) } if (data.type === 'recheck') { 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 代替 worker-loader
。
new Worker(new URL('./worker.js', import.meta.url));
|
以下版本的就只能用worker-loader
咯
也可以逻辑写成字符串,然后通过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) { self.postMessage('send message') }, false); })
|
worker数据通信
var uInt8Array = new Uint8Array(new ArrayBuffer(10)); for (var i = 0; i < uInt8Array.length; ++i) { uInt8Array[i] = i * 2; } worker.postMessage(uInt8Array);
self.onmessage = function (e) { var uInt8Array = e.data; postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString()); postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength); };
|
但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。
如果要直接转移数据的控制权,就要使用下面的写法。
worker.postMessage(arrayBuffer, [arrayBuffer]);
var ab = new ArrayBuffer(1); worker.postMessage(ab, [ab]);
|
Web Worker 使用教程 - 阮一峰的网络日志 (ruanyifeng.com)
然而,并不是所有的对象都可以被转移。只有那些被设计为可转移的对象(用[ Transferable ] IDL 扩展属性修饰),比如ArrayBuffer、MessagePort,ImageBitmap,OffscreenCanvas,才能通过这种方式来传递。转移操作是不可逆的,一旦对象被转移,原始上下文中的引用将不再有效。转移对象可以显著减少复制数据所需的时间和内存。