大家好,我是 前端架构师 - 大卫。
更多优质内容请关注微信公众号 @程序员大卫。
初心为助前端人🚀,进阶路上共星辰✨,
您的点赞👍与关注❤️,是我笔耕不辍的灯💡。
前言
对于一些基于 SPA 构建的 ToC 网站,或者嵌入 App 的 Hybrid H5 页面,在项目发布上线后,我们希望能够自动检测是否有新版上线并及时提示用户刷新页面,避免用户继续使用缓存的老版本页面。
核心思路就是:周期性地对比"版本标识"是否发生变化,如果变化了就提示用户刷新。本文介绍三种用于生成和获取"版本标识"的方法。
💡 这些方法都采用轮询机制检测更新,只是获取"标识"的方式不同。
🌟 通用检测逻辑
ts
const LOOP_TIME = 4000;
const loopCheckFlag = async () => {
const preFlag = await getFlag(); // 获取标识
const loop = async () => {
const curFlag = await getFlag();
if (preFlag && curFlag && curFlag !== preFlag) {
alert("检测到有版本更新");
return;
}
setTimeout(loop, LOOP_TIME);
};
loop();
};
loopCheckFlag();
你只需要替换上面的 getFlag()
方法即可切换不同实现方式。
方法一:对比前后 ETag
或 Last-Modified
这是最简单的一种方式,通过对比页面响应头中的 Etag
或 Last-Modified
字段判断页面是否更新。
ts
async function getFlag() {
const response = await fetch('/', {
method: "HEAD",
cache: "no-cache",
});
return response.headers.get("Etag") || response.headers.get("last-modified");
}
⚠️ 注意:
- 这种方式依赖后端是否返回
Etag
或last-modified
;- 如果这两个值都为
null
,则无法检测更新,建议加以判断处理。
方法二:构建时生成 HTML 的 MD5 文件
由于每次发布后 index.html
的内容都会发生变化(比如引用的入口 JS 文件名变化),我们可以在构建完成后,生成 HTML 文件的 MD5 值,然后通过对比是否变化来判断是否有新版本。
🔧 获取方式
ts
async function getFlag() {
const response = await fetch('/htmlVersion.text', {
method: "GET",
cache: "no-cache",
});
return response.text();
}
🔐 Node 侧生成 md5
的工具函数
ts
// plugins/common/util.ts
import fs from 'fs/promises';
import crypto from 'crypto';
export function getMd5(filePath: string): Promise<string> {
return fs.readFile(filePath).then((data) =>
crypto.createHash('md5').update(data).digest('hex')
);
}
2.1 Vite 插件实现
ts
// plugins/CreateHtmlVersion/index.ts
import fs from "fs/promises";
import path from "path";
import type { ConfigEnv, Plugin } from "vite";
import { getMd5 } from "../common/util";
export default function createHtmlVersion(configEnv: ConfigEnv): Plugin {
return {
name: "vite-plugin-html-md5",
apply: "build",
async closeBundle() {
if (configEnv.mode !== "production") return;
const outDir = "dist";
const htmlPath = path.resolve(outDir, "index.html");
const versionPath = path.resolve(outDir, "htmlVersion.text");
try {
const md5 = await getMd5(htmlPath);
await fs.writeFile(versionPath, md5);
console.log("✅ 生成 htmlVersion.text 成功:", md5);
} catch (e) {
console.error("❌ 生成 htmlVersion.text 失败:", e);
}
},
};
}
应用插件:
ts
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import createHtmlVersion from "./plugins/CreateHtmlVersion";
export default defineConfig((configEnv) => ({
plugins: [
vue(),
createHtmlVersion(configEnv),
],
}));
2.2 Webpack 插件实现
ts
// plugins/CreateHtmlVersion/index.ts
import path from 'path';
import fs from 'fs/promises';
import { getMd5 } from "../common/util";
export default class CreateHtmlVersion {
private env: string;
constructor(env: string) {
this.env = env;
}
apply(compiler) {
if (this.env === "local") return;
compiler.hooks.done.tapPromise("CreateHtmlVersion", async (stats) => {
try {
const outputPath = stats.compilation.outputOptions.path;
const htmlFilePath = path.join(outputPath, "index.html");
const versionFilePath = path.join(outputPath, "htmlVersion.text");
const md5 = await getMd5(htmlFilePath);
await fs.writeFile(versionFilePath, md5);
console.log('✅ 生成 htmlVersion.text 成功:', md5);
} catch (e) {
console.error('❌ 生成 htmlVersion.text 失败:', e);
}
});
}
}
在 craco.config.js
中引入并应用插件:
ts
import CreateHtmlVersion from "./plugins/CreateHtmlVersion";
export default {
webpack: {
plugins: [
new CreateHtmlVersion(process.env.REACT_APP_ENV)
],
}
};
并在根目录配置环境变量:
bash
# .env.local
REACT_APP_ENV=local
注: 由于这个项目比较老,是基于
craco
,所以项目的根目录还需新建.env.local
文件。
方法三:直接对比 HTML 内容或 JS 文件路径变化
这是最"直接粗暴"的方式 ------ 每次轮询时获取 /index.html
的完整文本内容,然后进行对比。
ts
async function getFlag() {
const response = await fetch('/', {
method: "GET",
cache: "no-cache",
});
return response.text();
}
该方式简单易实现,但对比的是整份 HTML 内容,若只是静态资源变更,也可能导致不必要的触发。
🔍 进一步优化:对比 JS 文件路径是否变化
为了更精准地检测版本变更,可以从 HTML 中提取出所有 <script src="...">
标签中的 JS 地址,并将它们组成一个标识进行对比。
当构建后的 JS 文件带有哈希(如 /assets/index.abc123.js
),只要有资源更新,路径就会变化。相比对比整个 HTML 文本,这种方式更加高效、精准。
✅ getFlag
实现
ts
/**
* 用于轮询检测是否有 JS 文件变化
*/
async function getFlag() {
const html = await fetchIndexHtml();
const jsUrls = extractJsUrls(html);
return jsUrls.join(',');
}
🔧 工具函数
ts
/**
* 请求 index.html 内容
*/
async function fetchIndexHtml() {
const response = await fetch('/', {
method: 'GET',
cache: 'no-cache',
});
return response.text();
}
// 正则:匹配所有 <script src="..."> 的标签
const scriptSrcRegex = /<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*><\/script>/gi;
/**
* 从 HTML 中提取所有 JS 地址
*/
function extractJsUrls(html: string): string[] {
const result: string[] = [];
let match: RegExpExecArray | null;
while ((match = scriptSrcRegex.exec(html)) !== null) {
result.push(match[1]);
}
return result.sort(); // 排序,确保对比结果稳定
}
总结
方法 | 是否依赖后端 | 可控性 | 推荐程度 |
---|---|---|---|
ETag / Last-Modified |
✅ 是 | ❌ 不可控 | ⭐⭐ |
构建 MD5 文件对比 | ❌ 否 | ✅ 可控 | ⭐⭐⭐⭐ |
对比 HTML 内容 / JS 内容 | ❌ 否 | ✅ 灵活但复杂 | ⭐⭐⭐ |
✅ 推荐使用第二种方式(构建后生成 MD5 文件),兼容性强、部署后可控性高,配合 Vite 或 Webpack 插件都能灵活实现。