结合Worker通知应用更新

概述

项目部署上线后,特别是网页项目,提示正在操作系统的用户去更新版本非常 important。一般我们都会用"刷新大法"来清理缓存,但是对于正在操作网页的用户,不造系统更新了,请求的还是老版本的资源。

为了确保用户能够及时获得最新的功能和修复的 bug,我们需要通知用户刷新页面获取最新的代码。

方案

每次打包时,都生成一个时间戳,作为系统的伪版本,放到JSON文件中,通过对比文件的响应头Etag判断是否有更新。具体步骤如下:

1: 在public文件夹下加入manifest.json文件,里面存放两个字段:更新内容、更新时间戳

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

3: 在入口文件main.js中引入检查版本更新的逻辑,有更新则提示更新。

路由守卫router.beforeResolve(vite+vue),检查更新,对比manifest.json文件的响应头Etag判断是否有更新

通过Worker轮询,检查更新,对比manifest.json文件的响应头Etag判断是否有更新。Worker线程并不影响其他线程的逻辑。

流程如下:

实现

注意:以下为vite+vue为例

新建manifest.json

public目录下新建manifest.json

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

vite.config.js配置

配置vite打包输出更新mainfest.json

js 复制代码
import { defineConfig, type AliasOptions } from 'vite'
import { fileURLToPath, URL } from 'node:url';

import vue from '@vitejs/plugin-vue'

import path from 'path'
import { readFile, writeFile } from 'fs'

// 获取路径
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
    }
  })
})


const alias: AliasOptions = {
  '@': fileURLToPath(new URL('./src', import.meta.url)),
};

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      ...alias,
    },
  },
})

新建src/checkUpdate.js

检查更新文件

  1. 客户端发起请求,请求中包含上次获取的资源的ETag。
  2. 服务器收到请求后,比较客户端提供的ETag与当前资源的ETag是否一致。 5.如果一致,则返回HTTP 304 Not Modified响应,表示资源未发生变化,客户端可以使用缓存的版本。
  3. 如果不一致,服务器返回最新的资源内容,同时更新ETag。
  4. 客户端收到响应后,更新本地缓存的资源内容和ETag。
js 复制代码
import router from "./router/index.ts";
//上次的Etag
let lastEtag = "";
//是否更新
let hasUpdate = false;
//创建worker线程
// const worker = new Worker();
const worker =  new Worker(new URL("./checkUpdate.worker.js", import.meta.url))


console.log(worker, "worker");



//检查版本更新
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;
    console.log((lastEtag = etag), "lastEtag = etag");
  } catch (e) {
    return Promise.reject(e);
  }
}

async function confirmReload(msg = "", lastEtag) {
  worker &&
    worker.postMessage({
      type: "pause",
    });
  try {
    console.log("版本更新了");
  } catch (e) {}
}
// 路由拦截
router.beforeEach(async (to, from, next) => {
  next();
  try {
    await checkUpdate();
    if (hasUpdate) {
      worker.postMessage({
        type: "destroy",
      });
      location.reload();
    }
  } catch (e) {}
});

worker.postMessage({
  type: "check",
});

worker.onmessage = ({ data }) => {
  console.log(data, "data");
  if (data.type === "hasUpdate") {
    hasUpdate = true;
    confirmReload(data.msg, data.lastEtag);
  }
};

新建src/checkUpdate.worker.js

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)
  }
}

// 监听主线程发送过来的数据
addEventListener('message', ({ data }) => {
  console.log(data,'消息')
  if (data.type === 'check') {  
     console.log('checkcheckcheck')
    // 每5分钟执行一次
    // 立即执行一次,获取最新的etag,避免在setInterval等待中系统更新,第一次获取的etag是新的,但是lastEtag还是undefined,不满足条件,错失刷新时机
    // checkUpdate()
    intervalId = setInterval(()=>{
      checkUpdate()
      console.log('检查版本更新')
    },  3 * 1000)
  }
  if (data.type === 'recheck') {
    // 每5分钟执行一次
    hasUpdate = false
    lastEtag = data.lastEtag
    intervalId = setInterval(()=>{
      checkUpdate()
      console.log('检查版本更新')
    },  3 * 1000)
    console.log('recheckrecheckrecheck')
  }
  if (data.type === 'pause') {
    clearInterval(intervalId)
  }
  if (data.type === 'destroy') {
    clearInterval(intervalId)
    close()
  }
})

入口文件

示例:main.ts

js 复制代码
import "./checkUpdate.js"

注意

在工程化项目中,由于要打包后部署,因此work线程文件需要处理资源路径

js 复制代码
//创建worker线程
// const worker = new Worker();
const worker =  new Worker(new URL("./checkUpdate.worker.js", import.meta.url))
相关推荐
@大迁世界9 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路18 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug21 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213823 分钟前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中1 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星1 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗1 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全