Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

unsetunset 前言 unsetunset

周五晚上组里说前端有 bug,正在吃宵夜的我眉头一紧,立即打开了钉钉(手贱...),看了一下这不是前几天刚解决的吗,果然,使用刷新大法就解决,原因不过是用户一直停留在页面上,新的版本发布后,没有刷新拿不到新的资源。

现在大部分的前端系统都是 SPA,用户在使用中对系统更新无感知,切换菜单等并不能获取最新资源,如果前端是覆盖性部署,切换菜单请求旧资源,这个旧资源已经被覆盖(hash 打包的文件),还会出现一直无响应的情况。

那么,当前端部署更新后,提示一直停留在系统中的用户刷新系统很有必要。

unsetunset 解决方案 unsetunset

  1. 在 public 文件夹下加入 manifest.json 文件,记录版本信息

  2. 前端打包的时候向 manifest.json 写入当前时间戳信息

  3. 在入口 JS 引入检查更新的逻辑,有更新则提示更新

  • 路由守卫 router.beforeResolve(Vue-Router 为例), 检查更新,对比 manifest.json 文件的响应头 Etag 判断是否有更新

  • 通过 Worker 轮询,检查更新,对比 manifest.json 文件的响应头 Etag 判断是否有更新。当然你如果不在乎这点点开销,可不使用 Worker 另开一个线程

Public 下的加入 manifest.json 文件

json复制代码{    "timestamp":1706518420707,  "msg":"更新内容如下:\n--1.添加系统更新提示机制"}

这里如果是不向用户提示更新内容,可不填,前段开发者也无需维护 manifest.json 的 msg 内容,这里主要考虑到如果用户在填长表单的时候,填了一大半,你这时候给用户弹个更新提示,用户无法判断是否影响当前表单填写提交,如果将更新信息展示出来,用户感知更新内容,可判断是否需要立即刷新,还是提交完表单再刷新。

webpack 向 manifest.json 写入当前时间戳信息

js复制代码 // 版本号文件    const filePath = path.resolve(`./public`, 'manifest.json')    // 读取文件内容    readFile(filePath, 'utf8', (err, data) => {      if (err) {        console.error('读取文件时出错:', err)        return      }      // 将文件内容转换JSON      const dataObj = JSON.parse(data)      dataObj.timestamp = new Date().getTime()      // 将修改后的内容写回文件      writeFile(filePath, JSON.stringify(dataObj), 'utf8', err => {        if (err) {          console.error('写入文件时出错:', err)          return        }      })    })

如果你无需维护更新内容的话,可直接写入 timestamp

js复制代码// 生成版本号文件const filePath = path.resolve(`./public`, 'manifest.json')writeFileSync(filePath, `${JSON.stringify({ timestamp: new Date().getTime() })}`)

检查更新的逻辑

入口文件 main.js 处引入

我这里检查更新的文件是放在 utils/checkUpdate

arduino复制代码// 检查版本更新
import '@/utils/checkUpdate'

checkUpdate 文件内容如下

js复制代码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: '系统后台有更新,请点击“立即刷新”刷新页面\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轮询  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)    }  }}

这里因为缺换路由本来就要刷新页面,用户可无需感知系统更新信息,直接通过请求头的 Etag 即可,这里的 Fetch 方法就用 head 获取相应头就好了。

checkUpdate.worker.js 文件如下

js复制代码let lastEtaglet hasUpdate = falselet 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)  }}// 监听主线程发送过来的数据addEventListener('message', ({ data }) => {  if (data.type === 'check') {    // 每5分钟执行一次    // 立即执行一次,获取最新的etag,避免在setInterval等待中系统更新,第一次获取的etag是新的,但是lastEtag还是undefined,不满足条件,错失刷新时机    checkUpdate()    intervalId = setInterval(checkUpdate,5 * 60 * 1000)  }  if (data.type === 'recheck') {    // 每5分钟执行一次    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

js复制代码new Worker(new URL('./worker.js', import.meta.url));

以下版本的就只能用worker-loader

也可以逻辑写成字符串,然后通过 ToURL 给 new Worker,如下:

js复制代码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 数据通信

ini复制代码// 主线程
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);
};

但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做 Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。

如果要直接转移数据的控制权,就要使用下面的写法。

ini复制代码// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);

// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);

Web Worker 使用教程 - 阮一峰的网络日志 (ruanyifeng.com)

然而,并不是所有的对象都可以被转移。只有那些被设计为可转移的对象(用 [Transferable] IDL 扩展属性修饰),比如 ArrayBuffer、MessagePort,ImageBitmap,OffscreenCanvas,才能通过这种方式来传递。转移操作是不可逆的,一旦对象被转移,原始上下文中的引用将不再有效。转移对象可以显著减少复制数据所需的时间和内存。

作者:Zayn

链接:https://juejin.cn/post/7329280514628534313