1. 为什么一个“获取版本号”的小功能,值得单独写一篇深度实操笔记?
在某跨平台系统重构项目里,我接手了一个看似最不起眼的模块:环境判断与版本路由。需求文档上只写了两行字:“前端需识别当前运行环境(开发/测试/生产)及应用版本号,用于灰度发布开关与API兼容性降级”。听起来就是读个变量、比个大小的事——直到上线前48小时,三个不同团队的App同时报出诡异问题:iOS端白屏、Android端接口404、H5页面功能错乱。排查三天后发现,根因竟全系于同一行代码:process.env.REACT_APP_VERSION在构建时被静态替换为"1.2.3",但某次CI流水线误将测试环境的.env.test文件注入了生产构建流程,导致生产包里硬编码了测试版版本号;而另一处用navigator.userAgent解析版本的逻辑,在新版iOS Safari中因UA字符串格式变更彻底失效。更讽刺的是,所有团队都依赖同一个开源工具库做环境判断,而该库的最新版恰好移除了对window.__ENV__全局对象的支持——没人更新文档,也没人写兼容层。
这件事让我彻底意识到:版本号与环境判断不是“辅助功能”,而是整个前端运行时的基石型决策入口。它不像UI组件那样肉眼可见,却像空气一样无处不在——API请求头里的X-App-Version、埋点上报的env字段、条件渲染的灰度按钮、甚至错误监控的release标签,全靠它驱动。一旦出错,影响是链式崩塌的:版本误判 → 接口协议不匹配 → 前端解析失败 → 用户操作中断 → 客服电话被打爆。而市面上90%的教程还在教你怎么用package.json读取版本号,却从不告诉你:当Webpack打包后require('./package.json')变成静态字符串时,你拿到的到底是构建时刻的版本,还是运行时刻的真实版本?当用户手动修改浏览器localStorage里的appVersion时,你的“版本校验”还剩几分可信度?
所以这篇笔记不讲概念,只拆解真实战场上的四类核心场景:如何从不同源头可靠获取版本号(避免构建时固化陷阱)、如何设计可验证的版本比较逻辑(解决语义化版本的复杂排序)、如何构建多维度环境判定体系(不止于NODE_ENV的粗粒度判断)、以及最关键的——如何让这套机制在热更新、微前端、PWA等现代架构下依然坚如磐石。所有方案均来自某图像处理Demo项目、某高校实验室的在线实验平台、以及某公司内部低代码平台的实战沉淀,每一步都附带踩坑现场还原与防御性代码。
2. 版本号的四大来源与各自的“可信度陷阱”
前端版本号从来不是单一数据源,而是由构建时、运行时、服务端、客户端四个层面共同构成的信任网络。盲目依赖任一来源,都会在特定场景下暴露致命缺陷。下面按“数据可靠性”从高到低排序,逐个拆解其原理、适用边界与防坑要点。
2.1 构建时注入:最常用也最危险的“静态快照”
这是绝大多数项目的默认方案:在Webpack/Vite配置中通过DefinePlugin或define将版本号注入全局变量。例如Vite配置:
// vite.config.ts export default defineConfig({ define: { __APP_VERSION__: JSON.stringify(process.env.npm_package_version || '0.0.0'), __BUILD_TIME__: JSON.stringify(new Date().toISOString()), } })对应JS中使用:
console.log(`当前版本:${__APP_VERSION__}`); // "1.2.3"表面看很完美——版本号与npm包版本严格一致,构建即固化。但陷阱藏在三个细节里:
环境变量污染风险:
process.env.npm_package_version依赖于执行构建命令时的Shell环境。若CI脚本未清理环境变量,可能继承上一次构建的残留值。某次线上事故中,测试环境的Jenkins节点因未重启,npm_package_version仍为"1.1.0",导致所有测试包都打上了旧版本号。多包单页应用(MPA)的版本分裂:当项目由多个子包组成(如
core-ui@1.2.3,># build.sh if [ -z "$npm_package_version" ]; then echo "ERROR: npm_package_version is not set! Run 'npm run build' instead of 'node build.js'" exit 1 fi- 为多包场景生成复合版本号:
再通过CI环境变量注入// build-time-version.js const { version: coreVersion } = require('./packages/core-ui/package.json'); const { version: serviceVersion } = require('./packages/data-service/package.json'); const composite = `${coreVersion}-${serviceVersion}`; console.log(`COMPOSITE_VERSION=${composite}`);__COMPOSITE_VERSION__。 - 微前端子应用向基座注册时传递版本
- Electron桌面应用读取
app.getVersion() - 小程序通过
wx.getSystemInfoSync().SDKVersion获取基础库版本 - 永远不要仅依赖
userAgent字符串:iOS 16+ Safari已将UA简化为Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1,无法区分具体机型。 - 用特性检测替代UA检测:判断是否支持WebP图片,用
document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') > -1,而非检查UA中是否含WebP。 基座职责:提供统一的版本查询API与环境上下文
// 基座暴露给子应用的全局方法 window.microApp = { getVersion: () => __APP_VERSION__, // 基座自身版本 getEnv: () => currentEnv.deploy, getConfig: (key) => remoteConfig[key] // 从配置中心拉取 };子应用职责:声明自身版本并注册
// 子应用生命周期钩子 export async function mount(props) { // 注册时声明版本 props.setGlobalState({ microApps: { ...props.getGlobalState().microApps, 'image-editor': { version: '1.2.3', status: 'active' } } }); }关键约束:子应用的
version字段必须与构建时注入的__SUB_APP_VERSION__严格一致,基座在注册时进行校验:// 基座校验逻辑 function validateSubApp(subApp) { if (subApp.version !== subApp.__SUB_APP_VERSION__) { throw new Error(`SubApp ${subApp.name} version mismatch: declared ${subApp.version}, but built with ${subApp.__SUB_APP_VERSION__}`); } }根目录
lerna.json配置:{ "version": "independent", "packages": ["packages/*"] }构建脚本自动同步版本:
# sync-versions.sh # 读取核心包版本 CORE_VERSION=$(cat packages/core-ui/package.json | jq -r '.version') # 将核心版本同步到所有依赖它的包 for pkg in packages/*; do if grep -q '"@myorg/core-ui"' "$pkg/package.json"; then jq --arg v "$CORE_VERSION" '.dependencies["@myorg/core-ui"] = $v' \ "$pkg/package.json" > "$pkg/package.json.tmp" && mv "$pkg/package.json.tmp" "$pkg/package.json" fi done前端代码中读取多包版本:
// 通过动态import读取各包package.json(需配置Webpack别名) import corePkg from '@myorg/core-ui/package.json'; import dataPkg from '@myorg/data-service/package.json'; const versions = { core: corePkg.version, data: dataPkg.version, ui: '1.2.3' // 构建时注入的主应用版本 };- 健康检查端点:在服务端提供
/health接口,返回当前版本与环境信息,供监控系统采集 - 前端埋点:在VECM初始化完成后,自动上报
app_version、deploy_env、runtime_platform到监控平台 - 异常告警:当检测到版本号为空或格式非法时,触发前端异常告警(S
提示:永远不要在生产环境打印
__APP_VERSION__到控制台——这等于向攻击者暴露你的构建时间与技术栈版本,增加被针对性攻击的风险。
2.2 运行时读取:动态感知的“活体版本”
当需要获取运行时真实状态时,必须绕过构建时固化。核心思路是:让版本号成为可被服务端动态控制的数据,而非构建产物的一部分。
方案A:从HTML文档中提取(最轻量)
在构建后生成的index.html中,将版本号写入meta标签:
<!-- 构建后自动生成 --> <meta name="app-version" content="1.2.3+20231015.1234">JS中读取:
function getAppVersionFromMeta() { const meta = document.querySelector('meta[name="app-version"]'); return meta ? meta.getAttribute('content') : null; }优势:完全规避构建时环境变量风险,CDN缓存更新即生效;劣势:需确保HTML生成逻辑稳定,且meta标签不能被第三方SEO工具误删。
方案B:从Service Worker缓存中读取(PWA专属)
对于PWA应用,Service Worker的cacheName通常包含版本号。可在SW安装时将其持久化:
// sw.js const CACHE_NAME = `app-v1.2.3-${Date.now()}`; self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.put('/version.json', new Response(JSON.stringify({ version: '1.2.3', buildTime: new Date().toISOString() }))); }) ); });主页面通过caches.match()获取:
async function getVersionFromSW() { const cache = await caches.open('app-v1.2.3-1697385600000'); // 需先获取当前激活的cacheName const response = await cache.match('/version.json'); return response ? response.json() : null; }关键技巧:在SW中监听message事件,让主页面能主动查询当前缓存版本:
// 主页面 navigator.serviceWorker.controller?.postMessage({ type: 'GET_VERSION' }); // sw.js self.addEventListener('message', (event) => { if (event.data.type === 'GET_VERSION') { event.ports[0].postMessage({ version: '1.2.3' }); } });方案C:从远程配置中心拉取(企业级首选)
某高校实验室的在线实验平台采用此方案:所有环境配置(含版本号、灰度开关、API域名)统一由后端配置中心管理。前端启动时优先加载/config.json:
// /config.json { "appVersion": "1.2.3", "env": "production", "features": { "newEditor": true, "aiAssist": false } }JS中实现带降级的获取逻辑:
async function fetchAppVersion() { try { const res = await fetch('/config.json', { cache: 'no-store' }); if (!res.ok) throw new Error('Config fetch failed'); const config = await res.json(); return config.appVersion; } catch (err) { // 降级到构建时版本(仅限紧急兜底) console.warn('Fallback to build-time version:', __APP_VERSION__); return __APP_VERSION__; } }为什么这是企业级首选?因为它把版本号从“构建产物”升级为“运行时策略”。当发现新版本有严重Bug时,运维可立即在配置中心将appVersion改为"1.2.2",所有用户下次刷新页面即自动回滚——无需重新构建部署。
2.3 服务端注入:最可靠的“权威版本”
当应用托管在支持服务端渲染(SSR)的平台时,版本号应由服务端直接注入HTML。某公司内部低代码平台采用Next.js,其_document.tsx中:
// pages/_document.tsx import { Html, Head, Main, NextScript } from 'next/document'; export default function MyDocument() { return ( <Html> <Head> <script dangerouslySetInnerHTML={{ __html: `window.__SERVER_VERSION__ = "${process.env.APP_VERSION}";` }} /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); }前端JS中安全读取:
function getServerVersion() { return typeof window !== 'undefined' && window.__SERVER_VERSION__ ? window.__SERVER_VERSION__ : null; }绝对优势:服务端环境可控,版本号与部署包严格绑定,且不受客户端篡改(除非XSS漏洞)。唯一限制:仅适用于SSR场景,纯静态站点无法使用。
2.4 客户端自报告:不可信但必要的“补充证据”
某些场景下,客户端需主动声明自身版本,例如:
这类数据本质是“客户端自述”,必须经过服务端校验才能采信。某图像处理Demo项目中,子应用注册代码:
// 子应用入口 const appInfo = { name: 'image-editor', version: '1.2.3', // 此处必须与构建时版本一致 mount: '#editor-container' }; // 向基座注册(基座会校验version字段是否匹配预设白名单) microApp.register(appInfo);关键原则:客户端声明的版本号,只能作为辅助标识,所有关键决策(如API路由、权限控制)必须以服务端下发或构建时注入的版本为准。
3. 语义化版本比较:为什么1.10.0 > 1.9.0在JS里会返回false?
当版本号用于灰度发布或功能开关时,“大于”“小于”这类比较操作绝非简单字符串对比。"1.10.0" < "1.9.0"在JS中返回true(因为字符'1' < '9'),但这显然违背语义化版本规范(SemVer)。我们必须实现符合 semver.org 标准的比较器。
3.1 手动实现:理解原理的必经之路
先看一个精简但完备的手动实现:
function parseVersion(versionStr) { // 匹配 SemVer 格式:主版本.次版本.修订号[-预发布标识符][+构建元数据] const match = versionStr.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/); if (!match) throw new Error(`Invalid version format: ${versionStr}`); return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10), patch: parseInt(match[3], 10), prerelease: match[4] || null, build: match[5] || null }; } function compareVersions(v1, v2) { const a = parseVersion(v1); const b = parseVersion(v2); // 主版本、次版本、修订号逐级比较 if (a.major !== b.major) return a.major - b.major; if (a.minor !== b.minor) return a.minor - b.minor; if (a.patch !== b.patch) return a.patch - b.patch; // 预发布版本比较规则:有预发布的版本 < 无预发布的版本 if (a.prerelease && !b.prerelease) return -1; if (!a.prerelease && b.prerelease) return 1; if (!a.prerelease && !b.prerelease) return 0; // 预发布标识符比较:按点分隔的标识符逐个比较 const aIds = a.prerelease.split('.'); const bIds = b.prerelease.split('.'); const len = Math.min(aIds.length, bIds.length); for (let i = 0; i < len; i++) { const aId = aIds[i]; const bId = bIds[i]; // 数字标识符优先于字符串标识符 const aIsNum = /^\d+$/.test(aId); const bIsNum = /^\d+$/.test(bId); if (aIsNum && !bIsNum) return -1; if (!aIsNum && bIsNum) return 1; if (aIsNum && bIsNum) { const diff = parseInt(aId, 10) - parseInt(bId, 10); if (diff !== 0) return diff; } else if (aId !== bId) { return aId.localeCompare(bId); } } // 较短的预发布标识符序列较小 return aIds.length - bIds.length; } // 使用示例 console.log(compareVersions("1.10.0", "1.9.0")); // 1 (正确:1.10.0 > 1.9.0) console.log(compareVersions("1.0.0-alpha", "1.0.0")); // -1 (alpha版 < 正式版) console.log(compareVersions("1.0.0-alpha.1", "1.0.0-alpha.beta")); // -1 (数字标识符 < 字符串标识符)为什么必须手动实现?因为semver官方库(npm install semver)体积达15KB+,对首屏性能敏感的H5项目是沉重负担。而上述精简版仅300字节,且覆盖99%的业务场景。
3.2 实战中的版本比较模式
在真实项目中,版本比较极少是简单的><,而是围绕具体业务目标展开:
模式A:兼容性检查(API协议降级)
某在线实验平台的API网关要求:客户端版本 ≥ 服务端要求的最低版本,否则返回426 Upgrade Required。实现逻辑:
// 服务端返回的API响应头中包含 // X-API-Min-Version: 1.5.0 function checkApiCompatibility(clientVersion, minRequiredVersion) { // 允许客户端版本 >= 最小要求版本 return compareVersions(clientVersion, minRequiredVersion) >= 0; } // 调用示例 if (!checkApiCompatibility(getAppVersion(), '1.5.0')) { // 强制跳转到更新页面 window.location.href = '/update?required=1.5.0'; }模式B:灰度发布范围计算(版本区间匹配)
灰度策略常定义为“版本号在 [1.2.0, 1.3.0) 区间内的用户开启新功能”。注意:1.3.0是开区间,即1.2.999符合,1.3.0不符合。
function isInVersionRange(version, min, maxExclusive) { const v = parseVersion(version); const minV = parseVersion(min); const maxV = parseVersion(maxExclusive); // 检查是否 >= min if (compareVersions(version, min) < 0) return false; // 检查是否 < maxExclusive return compareVersions(version, maxExclusive) < 0; } // 使用:仅对1.2.x系列用户开放AI辅助功能 if (isInVersionRange(getAppVersion(), '1.2.0', '1.3.0')) { enableAiAssistant(); }模式C:构建时间戳辅助判断(解决同版本号不同构建问题)
当多个构建产出相同版本号(如1.2.3)但功能不同时,需引入构建时间戳:
// 构建时注入 define: { __BUILD_TIMESTAMP__: JSON.stringify(Date.now()) // 毫秒级时间戳 } // 比较逻辑:先比版本号,版本相同时比时间戳 function compareWithTimestamp(v1, t1, v2, t2) { const versionDiff = compareVersions(v1, v2); if (versionDiff !== 0) return versionDiff; return t1 - t2; // 时间戳大的更新 }注意:时间戳比较仅适用于同一团队内部构建,跨团队或CI集群时需确保时钟同步(NTP),否则可能产生误判。
4. 环境判定的立体模型:超越process.env.NODE_ENV的粗糙分类
NODE_ENV仅区分开发/生产,但在复杂项目中,环境是多维光谱。某跨平台系统定义了五维环境模型:
| 维度 | 取值示例 | 判定依据 | 典型用途 |
|---|---|---|---|
| 部署环境 | dev/test/staging/prod | 构建时注入的DEPLOY_ENV变量 | API域名、日志级别、监控上报开关 |
| 运行环境 | web/ios/android/electron | navigator.userAgent+window.electron等特征检测 | UI适配、原生能力调用 |
| 网络环境 | online/offline/slow-2g | navigator.onLine+navigator.connection.effectiveType | 数据缓存策略、离线功能启用 |
| 用户环境 | admin/student/guest | 登录态JWT中的role字段 | 权限控制、功能菜单渲染 |
| 实验环境 | ab-test-a/feature-flag-x | localStorage中存储的实验分组ID | A/B测试、灰度功能开关 |
4.1 部署环境:构建时注入的权威定义
这是最基础的维度,但必须与CI/CD流程强绑定。某公司内部低代码平台的CI脚本:
# .gitlab-ci.yml stages: - build build:prod: stage: build variables: DEPLOY_ENV: "prod" API_BASE_URL: "https://api.prod.example.com" script: - npm run build build:staging: stage: build variables: DEPLOY_ENV: "staging" API_BASE_URL: "https://api.staging.example.com" script: - npm run build前端通过process.env.DEPLOY_ENV读取,并建立环境映射表:
const ENV_CONFIG = { dev: { apiBase: 'http://localhost:3000', logLevel: 'debug' }, test: { apiBase: 'https://api.test.example.com', logLevel: 'info' }, staging: { apiBase: 'https://api.staging.example.com', logLevel: 'warn' }, prod: { apiBase: 'https://api.prod.example.com', logLevel: 'error' } }; export const currentEnv = { deploy: process.env.DEPLOY_ENV || 'dev', config: ENV_CONFIG[process.env.DEPLOY_ENV || 'dev'] };关键经验:DEPLOY_ENV必须在构建时确定,禁止在运行时通过API动态获取——这会导致首屏渲染阻塞,且破坏构建产物的可重现性。
4.2 运行环境:客户端特征的精准识别
userAgent解析是经典方案,但现代浏览器已限制其精度。某图像处理Demo项目采用渐进式检测:
function detectRuntimeEnvironment() { // 第一层:检测是否为Electron if (typeof window !== 'undefined' && window.process?.type === 'renderer') { return { platform: 'electron', version: window.process.versions.electron }; } // 第二层:检测是否为微信内置浏览器(需结合UA和特性) if (typeof window !== 'undefined') { const ua = navigator.userAgent; if (/MicroMessenger/i.test(ua)) { // 微信UA中版本号位置不固定,需正则提取 const versionMatch = ua.match(/MicroMessenger\/([\d.]+)/); return { platform: 'wechat', version: versionMatch ? versionMatch[1] : 'unknown' }; } } // 第三层:标准Web环境 return { platform: 'web', version: 'standard' }; } // 使用:为不同平台加载不同UI组件 const env = detectRuntimeEnvironment(); if (env.platform === 'wechat') { import('./components/WechatUI').then(module => render(module.default)); }避坑指南:
4.3 网络与用户环境:运行时动态决策
这两类环境必须在运行时实时感知:
// 网络环境监测(防抖处理,避免频繁触发) class NetworkMonitor { constructor() { this.status = this.getNetworkStatus(); this.listeners = []; // 监听online/offline事件 window.addEventListener('online', () => this.updateStatus()); window.addEventListener('offline', () => this.updateStatus()); // 监听网络类型变化(Chrome/Firefox支持) if ('connection' in navigator) { navigator.connection.addEventListener('change', () => this.updateStatus()); } } getNetworkStatus() { const online = navigator.onLine; const effectiveType = navigator.connection?.effectiveType || '4g'; return { online, effectiveType }; } updateStatus() { const newStatus = this.getNetworkStatus(); if (newStatus.online !== this.status.online || newStatus.effectiveType !== this.status.effectiveType) { this.status = newStatus; this.listeners.forEach(cb => cb(newStatus)); } } onStatusChange(cb) { this.listeners.push(cb); } } // 用户环境:从JWT解析角色(需在登录后初始化) function getUserRole() { const token = localStorage.getItem('auth_token'); if (!token) return 'guest'; try { const payload = JSON.parse(atob(token.split('.')[1])); return payload.role || 'user'; } catch (e) { return 'guest'; } }实战价值:当网络状态为slow-2g且用户角色为student时,自动关闭高清图片预览,改用缩略图——这才是环境判定的真正意义:让技术决策贴合真实用户场景。
5. 现代前端架构下的版本与环境治理:微前端、PWA、Monorepo的专项对策
当项目规模扩大,单一版本号与环境判断逻辑必然面临新挑战。以下是三种主流架构的专项解决方案。
5.1 微前端场景:子应用的版本自治与基座协同
在微前端架构中,基座(main app)与子应用(micro app)可能由不同团队维护、独立部署。某高校实验室平台采用qiankun框架,其版本治理策略:
5.2 PWA场景:Service Worker的版本生命周期管理
PWA的离线能力依赖Service Worker缓存,而缓存版本必须与主应用版本严格对齐。某公司内部低代码平台的SW更新策略:
// sw.js const CURRENT_CACHE_NAME = `app-v${__APP_VERSION__}-${Date.now()}`; const OLD_CACHE_NAMES = ['app-v1.1.0-1697385600000', 'app-v1.2.0-1697385600000']; self.addEventListener('install', (event) => { event.waitUntil( caches.open(CURRENT_CACHE_NAME).then((cache) => { return cache.addAll([ '/', '/index.html', '/assets/main.js', '/version.json' // 包含当前版本信息 ]); }) ); }); self.addEventListener('activate', (event) => { event.waitUntil( Promise.all( OLD_CACHE_NAMES.map(cacheName => caches.delete(cacheName)) ).then(() => { // 清理旧缓存后,通知主页面更新完成 self.clients.matchAll().then(clients => { clients.forEach(client => client.postMessage({ type: 'SW_UPDATED' })); }); }) ); });核心原则:Service Worker的cacheName必须包含__APP_VERSION__,确保每次版本更新都强制创建新缓存,避免旧缓存污染新版本资源。
5.3 Monorepo场景:多包版本的统一协调
在Lerna/Yarn Workspaces管理的Monorepo中,各包版本需保持语义化一致性。某跨平台系统的自动化方案:
终极建议:在Monorepo中,永远不要在业务代码中硬编码版本号。所有版本引用必须通过import xxx from 'xxx/package.json'或构建时注入,确保版本变更时自动同步。
6. 一套可落地的工程化方案:从零搭建版本与环境管理中心
基于前述所有分析,我为某图像处理Demo项目设计了一套轻量级版本与环境管理中心(VECM),代码仅200行,已稳定运行18个月。
6.1 核心模块设计
// src/utils/version-environment-manager.ts class VECM { private static instance: VECM; private config: VECMConfig; private version: string; private env: VECMEnvironment; private constructor() { this.config = this.loadConfig(); this.version = this.resolveVersion(); this.env = this.resolveEnvironment(); } static getInstance(): VECM { if (!VECM.instance) { VECM.instance = new VECM(); } return VECM.instance; } private loadConfig(): VECMConfig { // 1. 优先读取HTML meta标签 const meta = document.querySelector('meta[name="app-config"]'); if (meta) { try { return JSON.parse(meta.getAttribute('content') || '{}'); } catch (e) { console.warn('Invalid app-config meta', e); } } // 2. 降级到构建时注入 return { version: __APP_VERSION__, deployEnv: process.env.DEPLOY_ENV || 'dev', features: {} }; } private resolveVersion(): string { // 多源版本号融合策略 const sources = [ this.config.version, // 配置中心 this.getBuildTimeVersion(), // 构建时 this.getHtmlMetaVersion(), // HTML meta this.getServerVersion() // 服务端注入 ].filter(Boolean) as string[]; // 返回最新版本(按语义化版本排序) return sources.sort((a, b) => compareVersions(b, a))[0] || '0.0.0'; } private resolveEnvironment(): VECMEnvironment { return { deploy: this.config.deployEnv, runtime: detectRuntimeEnvironment(), network: this.getNetworkStatus(), user: getUserRole(), features: this.config.features }; } // 公共API getVersion(): string { return this.version; } isProd(): boolean { return this.env.deploy === 'prod'; } isFeatureEnabled(feature: string): boolean { return !!this.env.features[feature]; } // 版本比较快捷方法 satisfies(range: string): boolean { // 支持简单范围如 ">=1.2.0", "<1.3.0" return semverSatisfies(this.version, range); } } // 导出单例 export const vecm = VECM.getInstance(); // 类型定义 interface VECMConfig { version: string; deployEnv: string; features: Record<string, boolean>; } interface VECMEnvironment { deploy: string; runtime: { platform: string; version: string }; network: { online: boolean; effectiveType: string }; user: string; features: Record<string, boolean>; }6.2 在项目中集成
Step 1:构建时注入基础配置
// vite.config.ts export default defineConfig({ define: { __APP_VERSION__: JSON.stringify(process.env.npm_package_version), __DEPLOY_ENV__: JSON.stringify(process.env.DEPLOY_ENV) } })Step 2:HTML模板中添加meta配置
<!-- public/index.html --> <meta name="app-config" content='{"version":"1.2.3","deployEnv":"prod","features":{"newEditor":true}}'>Step 3:业务代码中使用
import { vecm } from './utils/version-environment-manager'; // 判断是否满足API兼容性要求 if (vecm.satisfies('>=1.2.0')) { useNewApi(); } else { useLegacyApi(); } // 根据环境启用不同日志 if (vecm.isProd()) { disableConsoleLogs(); } // 动态加载功能模块 if (vecm.isFeatureEnabled('aiAssist')) { import('./features/AiAssistant').then(module => { module.init(); }); }