Vue3 + Vite 移动端 H5:给每次发版打上「版本指纹」,线上排查再也不抓瞎
背景:一个企业微信里排查了半天的坑
最近在做一个跑在企业微信 WebView 里的 Vue3 H5 项目,遇到一个很典型的尴尬场景:
测试说「你这个功能没生效」,我说「我明明发上去了」,然后两个人对着屏幕大眼瞪小眼------因为谁也不知道当前线上部署的到底是哪一版代码。
企业微信 WebView 有几个要命的限制:
- 默认看不到控制台,vConsole 也不是随时能开;
- 有 CDN / 网关缓存,你不确定刷新后拿到的是不是最新产物;
- 多环境(dev / sit / uat / prod)来回发,很容易发错环境还浑然不觉。
所以我需要一个方案:每次打包自动给产物打上版本指纹(版本号 + git commit + 构建时间 + 环境),并且能用最低成本查到它。
这篇就完整记录一下这套方案,代码可直接抄。技术栈:Vue 3 + Vite + TypeScript。
方案概览
一句话:构建时采集信息 → 注入为全局常量 + 落一份 version.json。
查看方式给三种,覆盖各种环境:
| 方式 | 场景 | 怎么看 |
|---|---|---|
访问 /version.json |
企微等看不到控制台时首选 | 浏览器直接开 https://域名/version.json |
| 启动自动打印 | 能开控制台 / vConsole | 进系统自动 console.log 一行 |
window.appVersion |
随时手动查 | 控制台敲 window.appVersion |
信息来源:
- 版本号 :
package.json的version,发版前手动 bump(语义化版本); - 构建时间:打包那一刻的时间(固定东八区);
- 环境:当前构建 mode。
其中只有「版本号」需要人维护,其余全自动。
一步步实现
1. 抽一个「构建信息采集」文件
先把「读版本号 / 读时间」这几件事收拢到一个独立文件 build/version.ts,别塞进 vite.config.ts 里,配置文件会越来越乱。
ts
// build/version.ts
import { readFileSync } from 'node:fs'
import { fileURLToPath, URL } from 'node:url'
import type { Plugin } from 'vite'
export interface BuildInfo {
/** package.json 的版本号,发版前手动 bump */
version: string
/** 打包时间(东八区,YYYY-MM-DD HH:mm:ss) */
buildTime: string
/** 构建 mode(development / sit / uat / production) */
env: string
}
/** package.json 的 version */
function readPkgVersion(): string {
try {
const pkg = JSON.parse(
readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf-8'),
)
return pkg.version || '0.0.0'
} catch {
return '0.0.0'
}
}
/** 构建时间,固定东八区,格式 YYYY-MM-DD HH:mm:ss */
function readBuildTime(): string {
return new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Shanghai', hour12: false })
}
/** 采集当前构建的版本信息 */
export function resolveBuildInfo(mode: string): BuildInfo {
return {
version: readPkgVersion(),
buildTime: readBuildTime(),
env: mode,
}
}
/** 打包结束时把构建信息写一份 version.json 到产物根目录 */
export function versionJsonPlugin(info: BuildInfo): Plugin {
return {
name: 'emit-version-json',
apply: 'build',
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'version.json',
source: JSON.stringify(info, null, 2),
})
},
}
}
这里有两个容易踩的小坑,单独说一下:
坑一:时间补零。 一开始我用 toLocaleString('zh-CN', ...),结果拿到的是 2026-8-14 09:14:36------月和日都不补零,排序、对齐都难受。换成 'sv-SE'(瑞典 locale)就天然是 2026-08-14 09:15:35,补零 + 空格分隔,正好是我想要的格式,一行搞定,不用手写 padStart。
坑二:路径。 build/version.ts 在子目录里,读 package.json 要用 ../package.json,别写成 ./package.json。
2. vite.config.ts:注入全局常量 + 挂插件
build/version.ts 只是「采集」,真正让信息进入产物要靠 Vite 的 define(注入编译期全局常量)和刚才那个插件(落 version.json)。
ts
// vite.config.ts
import { resolveBuildInfo, versionJsonPlugin } from './build/version'
export default defineConfig(({ mode }) => {
// 采集一次,define 和 plugin 复用同一份
const buildInfo = resolveBuildInfo(mode)
return {
// 编译期把版本信息替换为字面量常量
define: {
__APP_VERSION__: JSON.stringify(buildInfo.version),
__APP_BUILD_TIME__: JSON.stringify(buildInfo.buildTime),
},
plugins: [
// ...其他插件
versionJsonPlugin(buildInfo),
],
}
})
define 的值必须是 JSON.stringify 后的字符串------Vite 是把它当代码文本 做替换的,你写 __APP_VERSION__: buildInfo.version(比如 1.2.0)会被当成 1.2.0 这段非法 JS,必须 JSON.stringify 包成 "1.2.0"。
一个小提醒:define 在 vite dev 下同样生效,此时 buildTime 是你启动 dev-server 的时刻,version.json 则只在 build 时产出(插件加了 apply: 'build')。
3. 给全局常量补类型声明
__APP_VERSION__ 这些常量凭空出现,TS 不认识。在 env.d.ts 里声明一下,顺带给 window.appVersion 也补上类型:
ts
// env.d.ts
// 构建期由 vite.config.ts 的 define 注入
declare const __APP_VERSION__: string
declare const __APP_BUILD_TIME__: string
interface Window {
/** 构建版本信息,启动时挂载,便于控制台随时读取 */
appVersion?: import('@/utils/env').AppVersion
}
因为构建产物文件(build/version.ts)不在 tsconfig.json 的 include 里,还要去 tsconfig.node.json 的 include 补一条 "build/**/*.ts",否则 vue-tsc 会漏检这个文件。
4. 应用侧统一出口
我们项目有条硬规矩:所有 import.meta.env.* 必须经 @/utils/env 出口读取 ,不允许业务代码直接摸 import.meta.env。版本信息也遵循同样的约定,在这里加一个 appVersion 出口:
ts
// src/utils/env.ts
export const appVersion = {
version: __APP_VERSION__,
buildTime: __APP_BUILD_TIME__,
/** 汇总展示串,如 `1.2.0 2026-08-14 14:30:00` */
full: `${__APP_VERSION__} ${__APP_BUILD_TIME__}`,
}
export type AppVersion = typeof appVersion
full 是为了控制台里一眼能读,把版本号和构建时间拼成一行;AppVersion 类型顺手导出,给 window.appVersion 复用,避免重复定义。
5. 启动时打印 + 挂到 window
最后一步,做「查看」。新建一个启动插件,干两件事:启动打印一行、挂到 window 供随时手动读。
ts
// src/plugins/version.ts
import { appVersion } from '@/utils/env'
export function setupVersion() {
window.appVersion = appVersion
console.log(`[版本] ${appVersion.full}`)
}
然后在应用启动编排里第一个调用它(它没有任何依赖,放最前,保证任何环境都能第一时间看到部署版本):
ts
// src/plugins/index.ts
import { setupVersion } from './version'
export function setupPlugins(app: App) {
setupVersion() // 无依赖,放最前
setupPinia(app)
// ...后续 router / vant / ...
}
三种查看方式,实测覆盖各种环境
打完包,dist 根目录会多出一个 version.json:
json
{
"version": "1.2.0",
"buildTime": "2026-08-14 09:20:14",
"env": "sit"
}
方式一:直接访问 /version.json(企微场景首选)
浏览器开 https://<域名>/version.json,不用进应用、不用开控制台,一眼就能确认「线上部署的到底是哪一版」。企微 WebView 看不到控制台时,这是最省事的办法。
方式二:启动日志(能开控制台 / vConsole 时)
进应用自动打印一行:
text
[版本] 1.2.0 2026-08-14 09:20:14
方式三:window.appVersion(随时手动查)
控制台里敲:
js
window.appVersion // { version, buildTime, full }
window.appVersion.full // "1.2.0 2026-08-14 09:20:14"
发版流程:只有版本号要手动维护
整套方案里,只有版本号 version 需要人管 ,构建时间、环境全是打包时自动采集。所以发版前记得先 bump 一下 package.json 的版本号(遵循语义化版本):
sh
# 补丁 / 小版本 / 大版本,按语义化版本选一个
npm version patch # 1.2.0 → 1.2.1
npm version minor # 1.2.0 → 1.3.0
npm version major # 1.2.0 → 2.0.0
# 然后正常打包,其余信息自动注入
pnpm build:sit
小结
方案不复杂,核心就一句话:构建时采集信息 → define 注入全局常量 + 插件落一份 version.json。落地下来最实用的两点:
version.json是排查线上部署版本的银弹,尤其在企微这种开不了控制台的环境,访问一个静态文件就够了;- 构建时间 + 环境比版本号更「诚实」,版本号靠人 bump 容易忘,但时间和环境是打包时自动盖的戳,「发没发对环境、是不是刚发的」一目了然。
从此测试再说「你这功能没生效」,先让他开一下 /version.json 对下版本和时间,是不是发错环境、是不是缓存没刷,当场就有答案。