WebUpdateNotificationPlugin 版本更新检测说明
本文档说明 pay 项目中 @plugin-web-update-notification/webpack 的工作原理、构建注入方式、运行时检测逻辑,以及排查与验证方法。
1. 背景与目的
SPA / MPA 前端发版后,用户若长期不关闭页面,浏览器仍运行旧版 JS。常见后果:
- 请求已不存在的 chunk 文件(404)
- 页面白屏或功能异常
- 用户看到的是旧 UI / 旧逻辑
WebUpdateNotificationPlugin 在检测到新版本后,弹出「发现新版本,请刷新」提示,引导用户 location.reload() 加载最新资源。
2. 项目中的配置
配置位置:vue.config.js
js
const { WebUpdateNotificationPlugin } = require('@plugin-web-update-notification/webpack')
module.exports = defineConfig({
configureWebpack: {
plugins: [
new WebUpdateNotificationPlugin({
logVersion: true,
}),
],
},
})
| 配置项 | 当前值 | 说明 |
|---|---|---|
logVersion |
true |
构建时在控制台打印版本号 |
versionType |
未配置(默认) | Git 仓库下默认使用 git_commit_hash |
checkInterval |
未配置(默认 10 分钟) | 轮询间隔 |
重要 :插件未按 VUE_APP_ENV(sit/uat/prod)做条件判断,所有环境的 production build 都会启用,包括:
bash
npm run build:skyee:wealth:sit
npm run build:skyee:wealth:uat
npm run build:skyee:wealth:prod
本地 serve:* 开发模式虽也会加载插件,但因 HMR 热更新,实际意义不大。
3. 版本号从哪里来
3.1 构建时(Node / webpack 进程)
插件在 webpack 构建阶段 读取版本号,不是在浏览器里读 git。
默认逻辑(@plugin-web-update-notification/core):
- 检测仓库类型:存在
.git目录 → 判定为 Git 仓库 - 执行系统命令获取短 commit hash:
js
import { execSync } from 'child_process'
function getGitCommitHash() {
return execSync('git rev-parse --short HEAD')
.toString()
.replace('\n', '')
.trim()
}
- 版本号 = 执行
npm run build时,当前工作区 HEAD 对应的短 hash
例如构建时 HEAD 为 084cb6697194d8e77d880061545fb347933c09c2,则版本号为 084cb6697。
3.2 其他可选版本类型
| versionType | 来源 |
|---|---|
git_commit_hash |
git rev-parse --short HEAD(Git 仓库默认) |
svn_revision_number |
svnversion(SVN 仓库默认) |
pkg_version |
process.env.npm_package_version |
build_timestamp |
构建时间戳(非 Git/SVN 时降级) |
custom |
手动指定 customVersion |
4. 构建时注入了哪些内容
webpack 插件在两个钩子中工作:
4.1 emit 阶段
向构建产物写入:
| 文件 | 说明 |
|---|---|
pluginWebUpdateNotice/web_version_by_plugin.json |
远端版本文件,供运行时轮询 |
pluginWebUpdateNotice/webUpdateNoticeInjectScript.*.js |
检测与弹窗逻辑脚本 |
pluginWebUpdateNotice/webUpdateNoticeInjectStyle.*.css |
通知栏样式(非隐藏模式) |
web_version_by_plugin.json 示例:
json
{
"version": "084cb6697",
"silence": false
}
4.2 afterEmit 阶段
修改 dist/.../index.html,在 <head> 中注入:
html
<link rel="stylesheet" href="/skyee/wealth/pluginWebUpdateNotice/webUpdateNoticeInjectStyle.efdbdf2b.css">
<script src="/skyee/wealth/pluginWebUpdateNotice/webUpdateNoticeInjectScript.global.ffdddd40.js"></script>
<script>window.pluginWebUpdateNotice_version = '084cb6697';</script>
在 </body> 前注入通知容器:
html
<div class="plugin-web-update-notice-anchor"></div>
4.3 路径说明
本仓库为 MPA 多入口,publicPath 按平台/项目区分,例如 Skyee wealth:
/skyee/wealth/index.html
/skyee/wealth/pluginWebUpdateNotice/web_version_by_plugin.json
插件会自动读取 webpack 的 publicPath 拼出正确 URL。
5. 运行时如何检测新版本
页面加载后,注入脚本的核心逻辑(简化):
js
// 本地版本:打开页面时 index.html 里写死的,之后不会变
const localVersion = window.pluginWebUpdateNotice_version // 例:'084cb6697'
// 从服务器拉最新 version.json(?t= 防缓存)
fetch('/skyee/wealth/pluginWebUpdateNotice/web_version_by_plugin.json?t=' + Date.now())
.then(res => res.json())
.then(({ version, silence }) => {
if (silence) return
if (version !== localVersion) {
showNotification() // 弹「发现新版本,请刷新」
}
})
判断依据 :本地版本 ≠ 远端 version.json 中的版本 → 认为有新版本。
5.1 触发检测的时机
| 时机 | 默认行为 |
|---|---|
| 页面首次加载 | 立即检测 |
| 定时轮询 | 每 10 分钟 |
| 标签页切回前台 | visibilitychange |
| 窗口重新聚焦 | focus |
| JS 文件加载失败 | 监听 <script> 的 error 事件(chunk 404 时触发) |
5.2 典型时间线
T1 用户打开页面
→ 加载构建 A 的 index.html
→ window.pluginWebUpdateNotice_version = '084cb6697'
T2 团队重新 build 并部署构建 B(新 commit)
→ 服务器 version.json 变为 '5bee76407'
→ 用户浏览器仍运行旧页面,本地仍是 '084cb6697'
T3 轮询 / 切回标签页 / chunk 404
→ fetch version.json → '5bee76407'
→ '084cb6697' !== '5bee76407'
→ 弹出更新提示
用户点击「刷新」后,会加载新的 index.html,本地版本同步更新,不再弹窗(除非 again 部署了更新的版本)。
6. 版本号与 Git Commit 的对应关系
页面源码中的版本号可直接用 git 反查:
bash
git show 084cb6697 --oneline
# 084cb6697 Merge branch 'feature-timeline' into sit
示例(SIT 某次部署):
| 页面中的版本号 | 对应 commit | 提交信息 | 时间 |
|---|---|---|---|
084cb6697 |
084cb6697194d8e77d880061545fb347933c09c2 |
Merge branch 'feature-timeline' into sit | 2026-07-30 10:29:32 |
构建日志中若 logVersion: true,也会打印:
version: 084cb6697
7. 部署与缓存注意事项
7.1 index.html 禁止强缓存
若 index.html 被 CDN/Nginx 长期缓存,可能出现:
- 用户刷新后仍拿到旧
index.html,本地版本号不更新 - 更新提示反复出现或行为异常
推荐 :对 index.html 设置 Cache-Control: no-cache, no-store, must-revalidate。
Nginx 示例:
nginx
location / {
if ( $uri = '/index.html' ) {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
try_files $uri $uri/ /index.html;
}
带 hash 的 JS/CSS(app.[hash].js)可长期缓存;index.html 和 version.json 需能及时更新。
7.2 version.json 请求
插件请求时带 ?t=Date.now() 避免浏览器缓存。若网关层对 query 有异常缓存策略,需一并排查。
8. 如何验证插件已生效
8.1 查看页面源码
在浏览器中「查看网页源代码」,在 <head> 内应能看到:
html
<script>window.pluginWebUpdateNotice_version = 'xxxxxxxx';</script>
8.2 直接访问 version.json
https://<域名>/skyee/wealth/pluginWebUpdateNotice/web_version_by_plugin.json
返回的 version 应与当前最新部署的 commit hash 一致。
8.3 控制台
非 prod 环境可手动触发检测(若页面已挂载全局方法):
js
window.pluginWebUpdateNotice_?.checkUpdate()
8.4 模拟发版检测
- 打开页面,记下
window.pluginWebUpdateNotice_version - 重新 build 并部署(commit 变化)
- 不刷新页面,等待轮询或切走再切回标签页
- 应出现「发现新版本」提示
9. 与 chunk 懒加载 / 代码拆分的兼容性
本仓库 feature-optmiz 等分支对 antd/element、首页 Tab 做了按需懒加载。插件通过 window 捕获阶段 监听 <script> 加载失败:
js
window.addEventListener('error', (e) => {
if (e.target?.tagName === 'SCRIPT') checkUpdate()
}, true)
该监听与业务代码中 import() 的 try/catch 互不冲突:即使 Promise 被 catch,浏览器仍会派发 script error 事件,从而触发版本检测。懒加载反而更容易在发版后触发 chunk 404 检测路径。
10. 常见问题
Q1:本地 localhost 开发会看到更新提示吗?
一般不明显。serve 模式有 HMR,且每次编译版本可能变化,不适合作为线上行为参考。以 build 后部署到 sit/uat/prod 为准。
Q2:同一 commit 重复 build,版本号会变吗?
不会。只要 git rev-parse --short HEAD 结果相同,版本号就相同。未提交的新改动不影响 hash(除非 HEAD 本身变化)。
Q3:CI 浅克隆(git clone --depth 1)有影响吗?
无影响。浅克隆仍包含当前 HEAD 的 commit hash,插件能正常读取。
Q4:如何临时关闭某次部署的更新提示?
构建时设置 silence: true(需改插件配置或发版流程),该次构建的 version.json 中 silence 为 true 时不会弹窗。
Q5:多页面(MPA)每个入口都有独立版本吗?
同一次 build 共用同一 git hash,各入口 index.html 注入的版本号相同;各入口目录下各有自己的 pluginWebUpdateNotice/ 路径(由 publicPath 决定)。
11. 相关文件与依赖
| 路径 / 包 | 说明 |
|---|---|
vue.config.js |
插件注册 |
package.json → @plugin-web-update-notification/webpack |
依赖版本 ^1.6.4 |
config/build.js |
统一 build 入口,透传 --mode=sit 等 |
config/tools.js |
MPA pages / publicPath 配置 |
构建产物 pluginWebUpdateNotice/web_version_by_plugin.json |
运行时轮询的版本文件 |
12. 流程总览
用户 用户浏览器(旧页面) 静态服务器 webpack 构建 开发者/CI 用户 用户浏览器(旧页面) 静态服务器 webpack 构建 开发者/CI #mermaid-svg-w2hvhP0U1lbhbb3I{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-w2hvhP0U1lbhbb3I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-w2hvhP0U1lbhbb3I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-w2hvhP0U1lbhbb3I .error-icon{fill:#552222;}#mermaid-svg-w2hvhP0U1lbhbb3I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-w2hvhP0U1lbhbb3I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-w2hvhP0U1lbhbb3I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-w2hvhP0U1lbhbb3I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-w2hvhP0U1lbhbb3I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-w2hvhP0U1lbhbb3I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-w2hvhP0U1lbhbb3I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-w2hvhP0U1lbhbb3I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-w2hvhP0U1lbhbb3I .marker.cross{stroke:#333333;}#mermaid-svg-w2hvhP0U1lbhbb3I svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-w2hvhP0U1lbhbb3I p{margin:0;}#mermaid-svg-w2hvhP0U1lbhbb3I .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-w2hvhP0U1lbhbb3I text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-w2hvhP0U1lbhbb3I .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-w2hvhP0U1lbhbb3I .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-w2hvhP0U1lbhbb3I .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-w2hvhP0U1lbhbb3I .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-w2hvhP0U1lbhbb3I #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-w2hvhP0U1lbhbb3I .sequenceNumber{fill:white;}#mermaid-svg-w2hvhP0U1lbhbb3I #sequencenumber{fill:#333;}#mermaid-svg-w2hvhP0U1lbhbb3I #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-w2hvhP0U1lbhbb3I .messageText{fill:#333;stroke:none;}#mermaid-svg-w2hvhP0U1lbhbb3I .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-w2hvhP0U1lbhbb3I .labelText,#mermaid-svg-w2hvhP0U1lbhbb3I .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-w2hvhP0U1lbhbb3I .loopText,#mermaid-svg-w2hvhP0U1lbhbb3I .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-w2hvhP0U1lbhbb3I .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-w2hvhP0U1lbhbb3I .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-w2hvhP0U1lbhbb3I .noteText,#mermaid-svg-w2hvhP0U1lbhbb3I .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-w2hvhP0U1lbhbb3I .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-w2hvhP0U1lbhbb3I .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-w2hvhP0U1lbhbb3I .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-w2hvhP0U1lbhbb3I .actorPopupMenu{position:absolute;}#mermaid-svg-w2hvhP0U1lbhbb3I .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-w2hvhP0U1lbhbb3I .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-w2hvhP0U1lbhbb3I .actor-man circle,#mermaid-svg-w2hvhP0U1lbhbb3I line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-w2hvhP0U1lbhbb3I :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户此前已打开页面 localVersion = 084cb6697 localVersion 更新为 5bee76407 npm run build:skyee:wealth:sit execSync('git rev-parse --short HEAD') 部署 index.html + version.json + 静态资源 新 commit 再次 build 部署新版本 version.json = 5bee76407 fetch version.json?t=... { version: 5bee76407 } 084cb6697 !== 5bee76407 弹窗「发现新版本,请刷新」 点击刷新 加载新 index.html
文档维护 :若调整 vue.config.js 中插件配置或更换版本策略,请同步更新本文档。