前端自动检测更新的 3 种方式!

大家好,我是 前端架构师 - 大卫

更多优质内容请关注微信公众号 @程序员大卫

初心为助前端人🚀,进阶路上共星辰✨,

您的点赞👍与关注❤️,是我笔耕不辍的灯💡。

前言

对于一些基于 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() 方法即可切换不同实现方式。

方法一:对比前后 ETagLast-Modified

这是最简单的一种方式,通过对比页面响应头中的 EtagLast-Modified 字段判断页面是否更新。

ts 复制代码
async function getFlag() {
  const response = await fetch('/', {
    method: "HEAD",
    cache: "no-cache",
  });
  return response.headers.get("Etag") || response.headers.get("last-modified");
}

⚠️ 注意:

  • 这种方式依赖后端是否返回 Etaglast-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 插件都能灵活实现。

相关推荐
一只叫煤球的猫几秒前
普通程序员,从开发到管理岗,为什么我越升职越痛苦?
前端·后端·全栈
vvilkim8 分钟前
Electron 自动更新机制详解:实现无缝应用升级
前端·javascript·electron
vvilkim11 分钟前
Electron 应用中的内容安全策略 (CSP) 全面指南
前端·javascript·electron
aha-凯心22 分钟前
vben 之 axios 封装
前端·javascript·学习
漫谈网络25 分钟前
WebSocket 在前后端的完整使用流程
javascript·python·websocket
遗憾随她而去.37 分钟前
uniapp 中使用路由导航守卫,进行登录鉴权
前端·uni-app
xjt_09011 小时前
浅析Web存储系统
前端
foxhuli2292 小时前
禁止ifrmare标签上的文件,实现自动下载功能,并且隐藏工具栏
前端
青皮桔2 小时前
CSS实现百分比水柱图
前端·css
失落的多巴胺2 小时前
使用deepseek制作“喝什么奶茶”随机抽签小网页
javascript·css·css3·html5