背景
我们的前端项目在发布新版本的时候,如果用户不手动去刷新页面,将会使用本地的静态文件,导致即使发版了,但是有很多的用户还是老版本的代码程序。在进行一些操作时,难免会产生一些垃圾数据,或者没有按照新流程走的数据。因此需要一个前端的发版提示,告诉用户有新版本发布。
实现思路
Etag 和 Last-Modified 缓存验证机制
Etag和Last-Modified是HTTP协议中用于缓存验证的两个重要机制。它们是响应头中的两个字段,允许客户端(如浏览器)与服务器进行沟通,以确定资源是否已经被更新,从而决定是否需要重新下载资源或使用本地缓存的版本。这两种机制可以提高网页加载速度并减少带宽使用。
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 是否改变,若改变则给出存在新版本的提示。
如何实现
-
在每次打包的时候去更改我们创建的 json 文件,可以通过直接访问服务器上的这个文件的地址访问到这个文件。
-
在页面加载时,去运行这个轮询函数,轮询请求这个文件,判断这个文件的 Etag 是否变化,变化则执行相关的提示信息。
-
js 执行是单线程的,因此向这种和业务完全分开的 js 逻辑完全可以不在我们的业务线程中,可以借助 Web Worker 来实现。
Web Worker 介绍
Web Worker 允许网页在其他线程中运行脚本,而不会阻塞用户界面。这意味着你可以执行一些计算密集型或长时间运行的任务,如大量数据处理、图像处理等,而不影响网页的响应性。通过将这些任务移到 Web Worker 中执行,可以保持主页面流畅,提高用户体验。
如何创建 Web Worker
- worker 脚本:
// worker.js
self.onmessage = function(e) {
// 接收从主脚本传来的消息
console.log('Worker: Message received from main script', e.data);
// 执行操作
// 发送结果回主脚本
self.postMessage({});
};
- 主脚本 (执行 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');
具体实现
- 新建一个静态文件 mainifest.json,或者其他的名字都可以,例如
// mainifest.json
{
"timestamp":1706518420707,
"msg":"更新内容如下:\n--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);
}
}
};
}
- 在 vite.config.js 中去引入这个插件
// vite.config.js
import { addTimestampToManifest } from './addTimestampToManifest'
plugins: [addTimestampToManifest()]
- 新建 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();
}
};
- 在主脚本中,启动 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'});
};
这样我们就完成了一个前端的发版提示功能。
欢迎关注我的公众号“前端趴菜成长记”,原创技术文章第一时间推送。