前端實(shí)現(xiàn)自動(dòng)檢測(cè)更新的三種方式
前言
對(duì)于一些基于 SPA 構(gòu)建的 ToC 網(wǎng)站,或者嵌入 App 的 Hybrid H5 頁(yè)面,在項(xiàng)目發(fā)布上線后,我們希望能夠自動(dòng)檢測(cè)是否有新版上線并及時(shí)提示用戶刷新頁(yè)面,避免用戶繼續(xù)使用緩存的老版本頁(yè)面。
核心思路就是:周期性地對(duì)比“版本標(biāo)識(shí)”是否發(fā)生變化,如果變化了就提示用戶刷新。本文介紹三種用于生成和獲取“版本標(biāo)識(shí)”的方法。
這些方法都采用輪詢機(jī)制檢測(cè)更新,只是獲取“標(biāo)識(shí)”的方式不同。
通用檢測(cè)邏輯
const LOOP_TIME = 4000;
const loopCheckFlag = async () => {
const preFlag = await getFlag(); // 獲取標(biāo)識(shí)
const loop = async () => {
const curFlag = await getFlag();
if (preFlag && curFlag && curFlag !== preFlag) {
alert("檢測(cè)到有版本更新");
return;
}
setTimeout(loop, LOOP_TIME);
};
loop();
};
loopCheckFlag();
你只需要替換上面的 getFlag() 方法即可切換不同實(shí)現(xiàn)方式。
方法一:對(duì)比前后 ETag 或 Last-Modified
這是最簡(jiǎn)單的一種方式,通過(guò)對(duì)比頁(yè)面響應(yīng)頭中的 Etag 或 Last-Modified 字段判斷頁(yè)面是否更新。
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; - 如果這兩個(gè)值都為
null,則無(wú)法檢測(cè)更新,建議加以判斷處理。
方法二:構(gòu)建時(shí)生成 HTML 的 MD5 文件
由于每次發(fā)布后 index.html 的內(nèi)容都會(huì)發(fā)生變化(比如引用的入口 JS 文件名變化),我們可以在構(gòu)建完成后,生成 HTML 文件的 MD5 值,然后通過(guò)對(duì)比是否變化來(lái)判斷是否有新版本。
獲取方式
async function getFlag() {
const response = await fetch('/htmlVersion.text', {
method: "GET",
cache: "no-cache",
});
return response.text();
}
Node 側(cè)生成 md5 的工具函數(shù)
// 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 插件實(shí)現(xiàn)
// 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);
}
},
};
}
應(yīng)用插件:
// 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 插件實(shí)現(xiàn)
// 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 中引入并應(yīng)用插件:
import CreateHtmlVersion from "./plugins/CreateHtmlVersion";
export default {
webpack: {
plugins: [
new CreateHtmlVersion(process.env.REACT_APP_ENV)
],
}
};
并在根目錄配置環(huán)境變量:
# .env.local REACT_APP_ENV=local
注: 由于這個(gè)項(xiàng)目比較老,是基于 craco,所以項(xiàng)目的根目錄還需新建 .env.local 文件。
方法三:直接對(duì)比 HTML 內(nèi)容或 JS 文件路徑變化
這是最“直接粗暴”的方式 —— 每次輪詢時(shí)獲取 /index.html 的完整文本內(nèi)容,然后進(jìn)行對(duì)比。
async function getFlag() {
const response = await fetch('/', {
method: "GET",
cache: "no-cache",
});
return response.text();
}
該方式簡(jiǎn)單易實(shí)現(xiàn),但對(duì)比的是整份 HTML 內(nèi)容,若只是靜態(tài)資源變更,也可能導(dǎo)致不必要的觸發(fā)。
進(jìn)一步優(yōu)化:對(duì)比 JS 文件路徑是否變化
為了更精準(zhǔn)地檢測(cè)版本變更,可以從 HTML 中提取出所有 <script src="..."> 標(biāo)簽中的 JS 地址,并將它們組成一個(gè)標(biāo)識(shí)進(jìn)行對(duì)比。
當(dāng)構(gòu)建后的 JS 文件帶有哈希(如 /assets/index.abc123.js),只要有資源更新,路徑就會(huì)變化。相比對(duì)比整個(gè) HTML 文本,這種方式更加高效、精準(zhǔn)。
getFlag 實(shí)現(xiàn)
/**
* 用于輪詢檢測(cè)是否有 JS 文件變化
*/
async function getFlag() {
const html = await fetchIndexHtml();
const jsUrls = extractJsUrls(html);
return jsUrls.join(',');
}
工具函數(shù)
/**
* 請(qǐng)求 index.html 內(nèi)容
*/
async function fetchIndexHtml() {
const response = await fetch('/', {
method: 'GET',
cache: 'no-cache',
});
return response.text();
}
// 正則:匹配所有 <script src="..."> 的標(biāo)簽
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(); // 排序,確保對(duì)比結(jié)果穩(wěn)定
}
總結(jié)
| 方法 | 是否依賴后端 | 可控性 | 推薦程度 |
|---|---|---|---|
| ETag / Last-Modified | 是 | 不可控 | ?? |
| 構(gòu)建 MD5 文件對(duì)比 | 否 | 可控 | ???? |
| 對(duì)比 HTML 內(nèi)容 / JS 內(nèi)容 | 否 | 靈活但復(fù)雜 | ??? |
推薦使用第二種方式(構(gòu)建后生成 MD5 文件),兼容性強(qiáng)、部署后可控性高,配合 Vite 或 Webpack 插件都能靈活實(shí)現(xiàn)。
以上就是前端實(shí)現(xiàn)自動(dòng)檢測(cè)更新的三種方式的詳細(xì)內(nèi)容,更多關(guān)于前端自動(dòng)檢測(cè)更新的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript Uploadify文件上傳實(shí)例
這篇文章主要為大家詳細(xì)介紹了Uploadify文件上傳小實(shí)例,一個(gè)簡(jiǎn)單的jsp上傳小功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02
微信小程序網(wǎng)絡(luò)請(qǐng)求模塊封裝的具體實(shí)現(xiàn)
大家做小程序項(xiàng)目的時(shí)候肯定會(huì)遇到數(shù)據(jù)對(duì)接需求,下面這篇文章主要給大家介紹了關(guān)于微信小程序網(wǎng)絡(luò)請(qǐng)求模塊封裝的具體實(shí)現(xiàn),文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-03-03
簡(jiǎn)單談?wù)凧S數(shù)組中的indexOf方法
最近在工作中遇到一個(gè)小問(wèn)題,這篇文章代碼我會(huì)簡(jiǎn)化成小例子展示給大家。給大家詳細(xì)的介紹JS數(shù)組中的indexOf方法,用心看到最后會(huì)有收獲哈,有需要的朋友們下面來(lái)一起看看吧。2016-10-10
JavaScript實(shí)現(xiàn)url參數(shù)轉(zhuǎn)成json形式
這篇文章主要介紹了JavaScript實(shí)現(xiàn)url參數(shù)轉(zhuǎn)成json形式的相關(guān)代碼,有喜歡的小伙伴可以參考下2016-09-09
javascript 語(yǔ)法學(xué)習(xí)練習(xí)
javascript 截取字符串排序2008-12-12
詳解promise.then,process.nextTick, setTimeout 以及 setImmediate的
這篇文章主要介紹了詳解promise.then,process.nextTick, setTimeout 以及 setImmediate的執(zhí)行順序2018-11-11

