前端发版提示

150 阅读6分钟

背景

我们的前端项目在发布新版本的时候,如果用户不手动去刷新页面,将会使用本地的静态文件,导致即使发版了,但是有很多的用户还是老版本的代码程序。在进行一些操作时,难免会产生一些垃圾数据,或者没有按照新流程走的数据。因此需要一个前端的发版提示,告诉用户有新版本发布。

实现思路

Etag 和 Last-Modified 缓存验证机制

EtagLast-ModifiedHTTP 协议中用于缓存验证的两个重要机制。它们是响应头中的两个字段,允许客户端(如浏览器)与服务器进行沟通,以确定资源是否已经被更新,从而决定是否需要重新下载资源或使用本地缓存的版本。这两种机制可以提高网页加载速度并减少带宽使用。

Etag.png

Last-Modified

Last-Modified 是一个 HTTP 响应头,它指定了资源在服务器上的最后修改时间。当客户端首次请求某个资源时,服务器除了返回资源内容外,还会在响应头中包含该资源的Last-Modified 值。

  • 如果资源自那以后没有被修改过,服务器将返回 304 Not Modified 状态码,告诉客户端可以继续使用缓存中的副本。
  • 如果资源已经被修改了,那么服务器会正常返回 200 OK 状态码,并附上新的资源内容和最新的 Last-Modified 时间戳。
Etag

ETag 是实体标签的意思,它提供了一种更灵活的方式来验证资源的新旧程度。ETag 可以是任何不透明的字符串,通常是基于资源内容生成的一个哈希值。当客户端首次请求资源时,服务器不仅返回资源本身,还在响应头中添加一个 ETag 字段。对于后续的请求,客户端可以通过 If-None-Match 请求头来询问服务器是否有匹配于之前提供的 ETag 的资源版本:

  • 若服务器发现资源没有变化(即 ETag 相同),则返回 304 Not Modified 状态码,指示客户端使用本地缓存。
  • 如果资源发生了改变(即 ETag 不同),服务器就会返回 200 OK 状态码及新版本的资源内容,并更新 ETag
两者的区别
  • Last-Modified 简单易用,但其精度受限于文件系统的时间粒度,可能不足以区分快速连续发生的修改。
  • ETag 提供了更高的精确度,尤其适用于动态内容或者对时间戳敏感性要求较高的场景。

如何使用 Etag 和 Last-Modified

既然有标识可以判断发布的版本信息,我们只需要对比他的 Etag 就可以判断程序有没有更新。那使用我们业务代码来判断这个 Etag 显然是不合适的,因此我们可在项目中单独拎出来一个专门用来判断的文件,我们可以去轮询这个文件判断这个 Etag 是否改变,若改变则给出存在新版本的提示。

如何实现

  1. 在每次打包的时候去更改我们创建的 json 文件,可以通过直接访问服务器上的这个文件的地址访问到这个文件。

  2. 在页面加载时,去运行这个轮询函数,轮询请求这个文件,判断这个文件的 Etag 是否变化,变化则执行相关的提示信息。

  3. js 执行是单线程的,因此向这种和业务完全分开的 js 逻辑完全可以不在我们的业务线程中,可以借助 Web Worker 来实现。

Web Worker 介绍

Web Worker 允许网页在其他线程中运行脚本,而不会阻塞用户界面。这意味着你可以执行一些计算密集型或长时间运行的任务,如大量数据处理、图像处理等,而不影响网页的响应性。通过将这些任务移到 Web Worker 中执行,可以保持主页面流畅,提高用户体验。

如何创建 Web Worker
  1. worker 脚本:
// worker.js
self.onmessage = function(e) {
  // 接收从主脚本传来的消息
  console.log('Worker: Message received from main script', e.data);

  // 执行操作

  // 发送结果回主脚本
  self.postMessage({});
};
  1. 主脚本 (执行 worker 脚本的地方)
// main.js 或其他位置

// 创建 worker
versionWorker = new Worker(new URL('worker 脚本文件路径', import.meta.url));

versionWorker.onmessage = (event) => {
  // 接受到的 worker 脚本文件发送的信息,即上面的 self.postMessage({})
};

// 发送给 worker 脚本的信息,即 self.onmessage = function(e) 的 e
versionWorker.postMessage('start');

具体实现

  1. 新建一个静态文件 mainifest.json,或者其他的名字都可以,例如
// mainifest.json
{
  "timestamp":1706518420707,
  "msg":"更新内容如下:\n--1.添加系统更新提示机制"
}

这里面的内容可以自己更改,可以自动获取自动生成发版日志文件中的内容,或者在一个文件中手动维护,放上具体的发版内容,如果不做这么精细化的话,完全可以存任何内容,只要在打包的时候改一下文件的内容即可。

  1. 在打包的时候,去动态的修改这个文件,写一个插件函数,在每次打包的时候就会运行这个函数
// addTimestampToManifest.ts 这是从项目中复制的是 ts 的,要是想要 js 的可以自行转换

import * as fs from 'fs-extra'; // 使用fs-extra,它提供了Promise接口且与TypeScript兼容更好
import path from 'path';

interface PluginOptions {
  dir: string; // 假设outDir是Vite配置中传递给插件的选项
}

interface Bundle {
  [key: string]: any; // 假设bundle是一个对象,键值对的具体类型取决于实际情况
}

export function addTimestampToManifest() {
  return {
    name: 'add-timestamp-to-manifest',
    apply: 'build',
    async generateBundle(options: PluginOptions, _bundle: Bundle) {
      const manifestPath = path.resolve(options.outDir, 'manifest.json');
      const timestamp = new Date().toISOString();
      try {
        const manifestData = await fs.readJson(manifestPath);
        manifestData.timestamp = timestamp;
        await fs.writeJson(manifestPath, manifestData, { spaces: 2 });
        console.log(`manifest.json updated with timestamp: ${timestamp}`);
      } catch (error) {
        console.error('Error updating manifest.json:', error);
      }
    }
  };
}
  1. 在 vite.config.js 中去引入这个插件
// vite.config.js

import { addTimestampToManifest } from './addTimestampToManifest'

plugins: [addTimestampToManifest()]
  1. 新建 worker 脚本,写脚本代码
// 这个也是 ts 文件,要使用 js 也直接把类型删掉就行
self.onmessage = async (event): Promise<void> => {
  // 为了更精准的控制 worker 的执行,只有在主脚本传 start 消息的时候才执行
  if (event.data.type === 'start') {
    let versionTimer: NodeJS.Timeout | undefined;
    let previousTag: string = '';

    const environment: string = '访问服务器的相对文件路径';

    // 通过获取ETag并设置计时器来验证版本
    const verifyVersion = async (): Promise<void> => {
      previousTag = await getEtag();
      timer();
    };

    // 设置计时器以定期检查版本
    const timer = (): void => {
      clearTimeout(versionTimer);
      versionTimer = undefined;
      versionTimer = setTimeout(async () => {
        // 对比逻辑
        await judge();
        timer();
      }, 10000);
    };

    const getEtag = async (): Promise<string> => {
      const response = await fetch(`${window.location.protocol}//${window.location.host}${environment}`, {
        method: 'HEAD',
        // 不取缓存文件
        cache: 'no-cache',
      });
      return response.headers.get('etag') || response.headers.get('last-modified') || '';
    };

    const judge = async (): Promise<void> => {
      const currentTag: string = await getEtag();
      if (currentTag !== previousTag) {
        previousTag = currentTag;
        // 向主脚本发送消息,执行提示操作
        self.postMessage({ type: 'updateAvailable' });
      }
    };

    verifyVersion();
  }
};

  1. 在主脚本中,启动 worker 脚本
// 在主进程中引用
export const verifyVersion = async (): Promise<void> => {
  // 创建 web worker
  versionWorker = new Worker(new URL('worker 脚本文件路径', import.meta.url));

  versionWorker.onmessage = (event: MessageEvent) => {
    // 收到消息
    if (event.data.type === 'updateAvailable') {
      // 可以换成其他的提示信息和提示方式
      console.log('存在新版本')
    }
  };
  // 启动 worker 脚本
  versionWorker.postMessage({type:'start'});
};

这样我们就完成了一个前端的发版提示功能。

欢迎关注我的公众号“前端趴菜成长记”,原创技术文章第一时间推送。

1729526975023.jpg