
1. 项目概述与痛点拆解说实话看到uni-app多端(H5、app、小程序) test prod多环境配置方案这个标题我第一反应就是兄弟你是不是被环境切换坑惨了是不是曾经在测试环境联调接口联到怀疑人生结果打包上线之后发现所有请求都打到了测试服务器或者在H5上跑得好好的一到小程序里就发现baseURL是错的白屏半天找不出原因这些坑我基本都踩过。uni-app这个框架最大的卖点是一套代码多端运行但这句广告语背后藏着一个很现实的问题不同端的环境配置逻辑完全不一样。H5是纯网页环境变量在构建时就被写死了小程序有自己的一套config机制还分微信、支付宝、百度等不同平台App端更麻烦有些配置要打进原生包里有些要在运行时动态读取。再加上test和prod两套环境数量组合一下就变成了端数量 x 环境数量 至少6套配置要管理。如果不提前设计好方案后期维护就是一场灾难。这篇内容我打算从一个实际项目的角度出发把整个多环境配置方案从头到尾捋一遍。适合正在用uni-app做跨端项目、被环境问题折磨过的同学也适合那些项目还没到多环境阶段、但想提前把坑填上的朋友。我会把方案设计、代码实现、踩坑记录、排查技巧都写清楚尽量做到看完就能直接落地。2. 方案设计思路与选型考量2.1 先想清楚到底有哪几个维度的环境需要管理很多人一提多环境就只想到baseURL这是最常见的误区。实际上在一个uni-app项目里需要跟着环境切换的东西远比想象中多。第一层是接口地址。这是最基础的test环境和prod环境的API域名肯定不一样这个大家都懂。第二层是各类第三方服务的密钥和配置。比如微信小程序的appid、H5端用的地图SDK key、App端推送服务的key、统计SDK的渠道ID等等。这些配置如果写死在一个文件里切换环境的时候要么手工改要么靠注释切换极易出错。第三层是业务逻辑层面的差异。有些功能在测试环境需要开启mock模式、需要打印更多日志、需要跳过某些校验在生产环境则要全部关掉。这些开关如果散落在代码各个角落维护起来相当痛苦。第四层是构建相关的差异化配置。比如H5端的publicPath、路由模式、小程序端的appid定义、App端的manifest配置这些在test和prod下可能也需要不同设置。所以做多环境方案第一步绝对不是急着写代码而是把项目里所有环境相关的东西列个清单搞清楚到底有哪些变量。我习惯的做法是建一个表格把配置项、影响范围、test值、prod值全部列出来然后才开始动手设计代码结构。2.2 选型文件的配置方案而非接口动态下发uni-app社区里关于多环境配置的方案有好几种简单分类一下纯手动切换方案写一个config.js里面用注释切换test和prod的配置。这是最原始的做法只适合一个人维护且逻辑极简单的项目。接口动态下发方案app启动后先请求一个配置接口拿到当前环境的配置。优点是灵活改配置不用发版缺点是要维护额外接口而且首屏展示会依赖这个请求网络一慢就白屏。静态编译注入方案利用构建工具的env机制在编译时根据命令行参数注入不同配置。这也是目前大多数成熟项目的选择。我最终选的是第三种基于Vite的import.meta.env机制结合uni-app的编译钩子来做。原因很简单配置在构建时就已经确定不会出现运行时才知道环境的问题代码里不需要写任何环境判断逻辑而且这个方案和uni-app的vue3vite版本配合得最自然。顺便说一下为什么不用接口下发。我做过的几个项目里接口下发配置的方案在App端尤其容易出问题——离线包场景下配置请求失败会导致启动卡死就算不考虑离线包每次启动多一次网络往返在弱网环境下体验属实拉胯。另外动态配置意味着前端代码里需要处理配置加载完成前的情况复杂度会上升一个量级。对于test/prod这种固定环境来说静态注入已经完全够用了。2.3 目录结构与环境文件规划确定了方案之后先看看我最终采用的目录结构设计├─ .env.test ├─ .env.production ├─ src │ ├─ config │ │ ├─ index.ts │ │ └─ env.ts │ ├─ manifest.json │ └─ pages.json ├─ vite.config.ts └─ package.json这里有一个关键点要提前说uni-app官方CLI创建的项目中.env文件的加载机制和纯Vite项目略有不同。在uni-app里运行和发布走的是不同的命令所以环境文件的命名和加载顺序需要特别确认。我自己验证过的做法是.env.test对应uni build -p h5 --mode test这种带--mode参数的构建.env.production其实是Vite默认会加载的production模式文件。按照Vite的规则加载优先级是.env.[mode].env.local.env。也就是说如果同时存在.env和.env.test并且两者都有同一个变量那么--mode test时后者会覆盖前者。这里还有个uni-app特有的坑不同端的命令不一样比如H5是uni build -p h5小程序是uni build -p mp-weixinApp是uni build -p app。如果要把--mode test和这些命令组合起来就必须在每个端命令后面都加上模式参数而不能只改一次。这个问题我在后面常见问题部分会详细说。3. 核心细节解析与实操要点3.1 环境变量的定义与检测时机Vite的import.meta.env机制本质上是在编译阶段对代码做字符串替换。你在import.meta.env.VITE_API_BASE_URL写下的代码在构建产物里会被直接替换成实际的字符串值。这意味着第一环境变量的值必须在构建时就已经确定不能在运行时动态修改。第二import.meta.env里只有以VITE_开头的变量才能被暴露到业务代码中其他变量只在构建脚本里可见。这两点看起来简单但实际项目里很多人会踩到变量死活读不到的问题九成都是因为变量名没加VITE_前缀。这里还要注意一个检测时机的问题。import.meta.env的替换发生在编译阶段所以如果你写的是const baseUrl import.meta.env.VITE_API_BASE_URL那没问题但如果你把这段代码写在一个被动态import的模块里并且这个模块是运行时才加载的那就要确认该模块是否也被纳入了编译替换范围。在我实际测试中uni-app Vite 4.x版本下动态import的模块同样会被处理但保险起见我还是建议把环境变量的读取集中在一个顶层模块里避免在嵌套函数中使用。3.2 配置文件的类型定义与默认值设计单一环境变量散落在代码各处肯定不行最好做一个集中管理的地方。我习惯在src/config/env.ts里把所有环境变量收拢并做类型定义和默认值兜底// src/config/env.ts interface EnvConfig { apiBaseUrl: string; mockEnabled: boolean; logLevel: debug | info | error; apiTimeout: number; uploadUrl: string; downloadUrl: string; } function parseEnv(): EnvConfig { // 读取 import.meta.env 中的变量 const env import.meta.env; return { apiBaseUrl: env.VITE_API_BASE_URL || https://default.example.com, mockEnabled: env.VITE_MOCK_ENABLED true, logLevel: (env.VITE_LOG_LEVEL as EnvConfig[logLevel]) || error, apiTimeout: Number(env.VITE_API_TIMEOUT) || 15000, uploadUrl: env.VITE_UPLOAD_URL || , downloadUrl: env.VITE_DOWNLOAD_URL || , }; } export const envConfig parseEnv();这里有几个设计上的小心思一是所有环境变量都通过env.ts统一导出业务代码只依赖envConfig不直接碰import.meta.env。好处是以后想加新的配置项只需要改一个文件而且类型提示完整不容易拼错变量名。二是默认值不能省。我见过很多项目配置文件里直接写import.meta.env.VITE_API_BASE_URL而不给默认值结果某个端忘记配置这个变量运行时拿到undefined请求全部报错。给默认值不是让你随意填一个而是要保证在配置缺失时系统能正常启动、至少能给出明显错误而不是静默失败。三是类型定义要严格。特别是logLevel这种有限取值字符串直接用字符串拼接很容易出错类型限定能帮你在编译期就发现配置值写错的问题。3.3 不同端的差异化处理逻辑在uni-app里还有一个绕不开的问题H5、小程序、App三个端对环境变量读取方式有差异吗先给结论在编译阶段import.meta.env的替换在所有端都是生效的但有一些细节需要注意。H5端是最标准的Vite行为import.meta.env直接可用。小程序端uni-app在编译时会把import.meta.env相关代码做转换处理。实测中我遇到过一个情况在小程序里明明定义了VITE_API_BASE_URL但编译后总是读取不到。查了半天发现是因为我在src/env.d.ts里做了类型扩展声明但声明的字段名和实际变量名对不上导致写成import.meta.env.VITE_API_BASE_URL时TypeScript报错编译过程把该行代码当成无效处理掉。App端要特别注意如果用的是uni-app的App原生渲染引擎即非webview渲染那么import.meta.env的处理方式可能和H5不完全一样。在官方文档里App端的环境变量支持是从HBuilderX 3.x某个版本才开始完善的如果你还在用旧版本的HBuilderX最好先集中升级一下。我自己实际项目中用的是vue3 vite的CLI创建方式App端的import.meta.env表现是正常的。除了import.meta.env之外另一个经常被忽略的点是process.env.NODE_ENV在uni-app所有端中都可用但取值在不同命令下不一样。运行开发模式时是development执行uni build时是production。如果业务代码里写if (process.env.NODE_ENV development)那么在H5的开发服务器下会命中但在小程序的开发模式下不一定。原因是uni-app的uni build -p mp-weixin命令不带--mode时NODE_ENV也是production。这个坑很多人会踩到我这里提前说透。3.4 manifest.json 与多环境适配manifest.json是uni-app的全局配置文件里面包含appid、小程序配置、App图标、SDK配置等。问题来了一个manifest.json怎么应对多环境这就必须说到uni-app IDE和CLI两种创建方式的区别了。如果你用HBuilderX创建项目manifest.json在可视化界面里不好做动态切换但如果你用npx degit dcloudio/uni-preset-vue#vite创建CLI项目manifest.json只是个普通JSON文件可以在构建时做处理。实操中最常见的一种做法是在小程序平台配置里test和prod使用不同的appid。这个需求在微信小程序很常见因为你开发时用的测试号、正式上线用的是另一个企业号。在CLI项目中manifest.json里的小程序appid可以从环境变量读取这样test和prod就可以用不同的小程序账号编译。具体实现上uni-app官方提供的uni-appCLI编译流程中manifest.json会在编译阶段被读取并合并。我尝试过在构建前用Node脚本改写manifest.json的方式也有用环境变量读取的方式。更优雅的做法是写一个vite插件在构建前根据mode替换manifest.json中的字段。但需要注意manifest.json合并时mp-weixin的配置是嵌套在mp-weixin对象下的直接改根字段没用。3.5 请求封装与baseURL自动适配有了环境配置之后最直接的应用场景就是请求封装。我通常会在封装的request模块里引envConfig并且针对不同端做差异化处理。// src/utils/request.ts import { envConfig } from /config/env; export function getBaseUrl(): string { // #ifdef H5 return window.location.origin.startsWith(http://localhost) ? envConfig.apiBaseUrl : envConfig.apiBaseUrl; // #endif // #ifndef H5 return envConfig.apiBaseUrl; // #endif }这里用到的#ifdef和#ifndef是uni-app的条件编译注释在编译时会把不属于当前端的代码块直接去掉。这是uni-app多端开发中非常重要的一个工具后面我在请求封装、路由跳转等处都会用到。条件编译的存在让一套代码多端适配有了更细的粒度。比如H5端可以读取浏览器的window.location来做一些逻辑而小程序和App端没有这个对象相关代码必须放在条件编译注释里否则编译后运行时会报错。还有一点小程序端的网络请求对域名有白名单限制如果你的test接口域名没加到小程序后台的request合法域名里真机调试时请求会直接失败。这个问题不是前端代码能解决的必须在微信公众平台后台配好。4. 实操过程与核心环节实现4.1 环境文件配置实战说了这么多理论下面进入实战环节。我以一个真实项目的配置为例把每个文件的内容和用途都过一遍。先看根目录的.env文件这个是所有模式下都会加载的公共配置放一些不分环境的变量# .env # 所有环境都一样的配置放这里 VITE_APP_NAME我的跨端应用 VITE_APP_VERSION1.0.0 VITE_API_TIMEOUT15000然后是.env.test文件测试环境专属配置# .env.test # 测试环境配置 VITE_API_BASE_URLhttps://test-api.example.com VITE_MOCK_ENABLEDtrue VITE_LOG_LEVELdebug VITE_UPLOAD_URLhttps://test-upload.example.com VITE_DOWNLOAD_URLhttps://test-download.example.com再是.env.production文件生产环境专属配置# .env.production # 生产环境配置 VITE_API_BASE_URLhttps://api.example.com VITE_MOCK_ENABLEDfalse VITE_LOG_LEVELerror VITE_UPLOAD_URLhttps://upload.example.com VITE_DOWNLOAD_URLhttps://download.example.com这组配置看起来简单但有几个细节值得说明。第一VITE_MOCK_ENABLED这里我用的是字符串true和false。在env.ts里我写的是env.VITE_MOCK_ENABLED true这就能正确处理布尔值字符串。有些同学直接写成if (import.meta.env.VITE_MOCK_ENABLED)由于import.meta.env的值在编译替换后是字符串非空字符串永远为真所以mock永远开启排查起来会非常困惑。第二超时时间、上传地址这些配置虽然没有在标题里明显提到但实际项目中几乎必然要用到。提前放进环境配置里总比后面需求来了临时加要省事。第三.env.production这个文件名有点特殊。Vite会默认加载.env.production吗准确说是当--mode production时才会加载。而uni build命令默认的mode就是production所以.env.production在默认构建时会被自动加载。我们做多环境设计时通常还会自定义一个--mode staging之类的中间环境那时就需要新建.env.staging文件并指定--mode staging命令。4.2 package.json 脚本命令设计环境配置写好了脚本命令也得跟上。设计合理的npm scripts可以大大减少人工操作和手误。我项目中的package.json脚本设置如下{ scripts: { dev:h5: uni, dev:mp-weixin: uni -p mp-weixin, build:h5:test: uni build -p h5 --mode test, build:h5:prod: uni build -p h5 --mode production, build:mp-weixin:test: uni build -p mp-weixin --mode test, build:mp-weixin:prod: uni build -p mp-weixin --mode production, build:app:test: uni build -p app --mode test, build:app:prod: uni build -p app --mode production } }这个脚本设计最关键的一点是每条构建脚本都显式指定了--mode不会出现我以为构建的是测试包结果打出来是正式包的情况。这里要解释一个容易混淆的点uni build -p h5 --mode test到底做了什么-p h5指定的是构建平台--mode test指定的是读取哪份环境文件。两者互不干扰可以自由组合。所以你要打测试环境的小程序包命令就是uni build -p mp-weixin --mode test。另外dev模式默认读取.env文件不会自动加载.env.test。如果你开发时想连测试环境接口可以启动命令加上--mode test比如uni -p h5 --mode test。这在某些场景下很实用比如后端联调时你想在测试环境排错但又不想把本地的本地代理配置翻个底朝天。4.3 请求封装中的环境切换实现环境配置最核心的使用者就是请求模块。这里给出一个更完整的封装示例包含多端适配和不同环境的处理逻辑。// src/utils/request.ts import { envConfig } from /config/env; interface RequestOptions extends UniApp.RequestOptions { skipAuth?: boolean; } export function getBaseUrl(): string { return envConfig.apiBaseUrl; } export function getUploadUrl(): string { return envConfig.uploadUrl; } function showErrorToast(message: string) { uni.showToast({ title: message, icon: none, duration: 2000, }); } /** * 统一请求入口 */ export function requestT any(options: RequestOptions): PromiseT { return new Promise((resolve, reject) { const header: Recordstring, string { Content-Type: application/json, ...(options.header as Recordstring, string), }; // 生产环境自动带上tokentest环境如果有需要也可以带 const token uni.getStorageSync(token); if (token) { header[Authorization] Bearer ${token}; } uni.request({ ...options, url: ${getBaseUrl()}${options.url}, header, success: (res) { // 统一处理返回结构 const data res.data as { code: number; data: T; msg: string }; if (res.statusCode 200) { if (data.code 0) { resolve(data.data); } else { // 业务错误 if (!options.skipAuth data.code 401) { // 登录失效处理 uni.removeStorageSync(token); uni.navigateTo({ url: /pages/login/index }); } reject(new Error(data.msg || 请求失败)); } } else { reject(new Error(HTTP ${res.statusCode})); } }, fail: (err) { // 网络错误时test环境显示更详细的错误信息 if (envConfig.logLevel debug) { console.error(request fail, options.url, err); } reject(err); }, }); }); }这里有几个和生产部署相关的考虑。一是请求的baseURL拼接我直接用了envConfig.apiBaseUrl没有做任何环境判断。因为envConfig已经是构建时确定好的对象这里的代码在任何端都返回正确的环境地址不需要重复判断。二是错误处理里区分了业务错误和网络错误。code 0是成功约定code 401是登录失效这些都是实际项目的常见约定你可以根据后端协议自行调整。关键是envConfig.logLevel debug时的日志打印这在测试环境排查问题非常有用生产环境不会输出多余的debug日志。三是小程序端的域名白名单问题。如果你在测试环境用了http协议不是https那么微信小程序开发工具还能正常请求但真机预览时就会被拦截。这是微信平台的安全策略没法在前端绕过必须在微信公众平台后台把测试域名加入到request合法域名列表里。4.4 mock 配置的按环境开关mock是个好工具但用不好就是灾难。我在环境配置里单独设计了VITE_MOCK_ENABLED开关并在请求封装里做了统一处理。具体做法是这样的定义一个mockData模块里面根据接口路径返回模拟数据然后在request函数开头判断if (envConfig.mockEnabled mockData[options.url]) { console.warn([mock] 使用mock数据: ${options.url}); setTimeout(() { resolve(mockData[options.url]); }, 300); return; }注意几个细节mock逻辑必须在request函数的最前面这样网络请求根本不会发出效率更高。mock数据要带一点延迟模拟真实网络环境避免出现本地秒开部署后卡死的心理落差。mock开关是全局的如果你只想mock某个接口而不是全部建议在mockData定义里做标记而不是改环境变量。我遇到过最坑的mock事故是test环境的VITE_MOCK_ENABLED写成true结果这个配置被带到了生产环境构建用户看到的所有数据都是写死的假数据。这种问题非常隐蔽因为接口返回结构完全正常只是数据不对。排查方式是用环境变量检查页——在app里隐藏一个入口点击后把当前环境的envConfig展示出来测试和生产是否串环境一目了然。4.5 App端的特殊处理与离线打包注意事项App端和H5、小程序不太一样它的环境配置有一部分不是靠import.meta.env就能搞定的。首先是App的manifest配置比如App名称、包名、图标、启动图。如果你想在test和prod下打出不同的包比如test版的App名称带上测试字样或者用不同的包名避免覆盖安装那么需要在构建前动态修改manifest.json。一种可行方案是写一个Node脚本根据环境参数改写manifest.json后再执行uni build。其次是App离线打包的场景。如果你用云打包HBuilderX标准打包那么环境变量在编译时已经注入没问题如果你走离线打包原生工程里的某些配置可能需要手动同步比如推送SDK的key、打包签名等。这时候可能会出现前端代码是对的但原生层配置是错的的错位问题。我的处理方式是写一份文档列清楚test和prod离线打包时需要手工修改哪些原生配置项并在每次打包前对照检查。第三是App内置webview的请求问题。如果你的App里嵌入了web页面这些页面的环境配置是独立的不受App侧环境变量控制。需要在web页面侧也维护一套环境配置机制两边保持一致。常见的做法是在web页面的URL里带上环境参数由App端在打开webview时根据当前环境拼接URL。这块在debug模式下尤其要注意不然会出现App里打开web页面报错单独用浏览器打不开页面却正常的迷惑现象。4.6 H5端的publicPath与路由模式适配H5独有的一个环境相关问题部署路径。如果你把H5部署在https://example.com/app这个子路径下而构建时publicPath默认是/那所有静态资源路径都会错误。这种问题在test和prod环境之间经常因为部署路径不一致而出现。在.env文件中可以加一个VITE_PUBLIC_PATH变量然后在vite.config.ts里读取并设置base// vite.config.ts import { defineConfig, loadEnv } from vite; import uni from dcloudio/vite-plugin-uni; export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd(), ); return { plugins: [uni()], base: env.VITE_PUBLIC_PATH || /, }; });这里用到了loadEnv函数来读取环境变量这在vite.config.ts里是常见的做法。需要注意的是loadEnv第三个参数传了空字符串这表示不限制变量前缀——因为vite.config.ts里的base需要读取VITE_PUBLIC_PATH如果第三个参数默认值是VITE_那读到的是空对象就会出问题。postition的坑点在于H5端不仅构建时要关注部署路径路由模式也很关键。uni-app的H5默认是hash路由URL长这样https://example.com/app/#/pages/home/index。如果换成history路由就需要服务器配置支持fallback到index.html否则刷新页面会404。这个在不同环境下服务器的配置往往不同所以我在项目文档里会明确记录每个环境的部署路径和路由模式要求。5. 常见问题与排查技巧实录5.1 环境变量读取不到的排查清单遇到环境变量读取不到的问题先别慌按这个顺序排查第一确认变量名前缀是不是VITE_。不是以这个前缀开头的变量在业务代码里永远读不到这不是bug是Vite的设计。如果你确实需要暴露非VITE_开头的变量要么改前缀要么在vite.config.ts里用define手动注入。第二确认当前构建命令的--mode是否和对应的.env文件匹配。比如你执行uni build -p h5 --mode testVite会加载.env.test文件如果你写的是--mode testing它会去找.env.testing而不是.env.test加载不到自然就是默认值。第三检查变量是否被其他文件优先级覆盖。Vite加载环境文件的优先级是从上到下越靠后的优先级越高。如果你在.env和.env.test里定义了同名变量后加载的会覆盖先加载的。可以用一个临时页面或者console.log把import.meta.env整个打出来看看实际值是什么。第四确认是在编译阶段使用不是运行时动态拼接。import.meta.env是编译时替换的静态值如果你把它写在一个被动态拼接的字符串里比如const key VITE_ API_BASE_URL; const val import.meta.env[key];这在多数情况下是拿不到的。因为编译替换是基于静态语法的没法处理动态key。第五检查env.d.ts类型声明是否正确。如果你的TypeScript项目中import.meta.env的类型声明和实际变量名不匹配虽然编译可能不报错但在某些版本的uni-app编译插件下有可能会导致代码被异常处理。5.2 test环境打包成prod包的问题这是多环境配置最严重的事故一旦发生就是线上故障。这类问题的成因通常有几个一是命令敲错。比如本该执行build:mp-weixin:test结果手滑执行了build:mp-weixin:prod。这种问题没法完全靠自觉解决我的经验是在CI/CD流水线中把环境参数和构建命令绑定开发人员只能选择打测试包或打正式包这种语义化按钮而不能直接输入命令。二是环境变量被污染。比如本地开发时为了调试方便在.env.local里写了VITE_API_BASE_URLhttps://test-api.example.com。之后执行uni build -p h5 --mode production时.env.local的优先级比.env.production高结果生产环境的构建结果API地址是测试的。这是Vite官方明确写过的坑但很多人并不知道。解决.env.local污染问题的办法是尽量少用.env.local或者至少记得在构建前删除它。在CI/CD流水线上因为环境是全新拉取的基本不会出现.env.local但在本机构建时很容易忘记它的存在。三是配置缓存。某些构建工具或IDE会缓存环境变量导致修改.env文件后构建结果还是旧的。出现这种情况先把node_modules/.vite缓存目录删除再重新构建。5.3 小程序端请求失败与域名白名单小程序端的网络请求有严格的域名白名单机制。不论test还是prod环境请求域名都必须在微信公众平台后台加入请求合法域名否则真机调试时请求直接失败。一个常见场景是测试环境用的是http://192.168.1.100:8080这种局域网地址。小程序真机调试时手机和电脑必须在同一个网段而且这个IP域名在微信公众平台后台根本无法添加为合法域名必须是HTTPS。所以测试小程序的接口环境建议直接用已配置HTTPS的测试域名或者使用微信开发者工具自带的不校验合法域名选项进行真机调试。这里给出一个经验在设计测试环境的API域名时尽量和生产环境保持一致的形式。比如生产是https://api.example.com测试就用https://test-api.example.com。这样在微信后台配置域名时更方便也不容易出现生产能通、测试不能通的差异。5.4 本机多环境调试的经验心得最后分享一个我实际工作中摸索出来的调试技巧。如果你使用的是HBuilderX内置浏览器或者微信开发者工具可以在开发时通过不同的启动命令访问不同的环境。比如开一个终端运行uni -p h5 --mode test另一个终端运行uni -p h5 --mode production两个开发服务器分别对应test和prod配置调试时只需切换浏览器端口或地址。这种方式最大的好处是可以同时对比test和prod环境下同一个页面的行为和表现不用反复切换构建命令。另外我强烈建议在项目里加一个当前环境标识的视觉提示。比如在test环境下页面顶部固定一个黄色细条写着TEST在prod环境下不显示。这个提示能极大减少诶我现在到底在哪个环境的困惑。实现方式也很简单在App.vue的onLaunch里读取envConfig判断如果是test环境就用uni的showTabBar和showNavigationBarLoading做颜色调整或者更粗暴地加一个全局view。当然这个提示条只能在开发阶段用上线前要确认它不会出现在生产环境的UI上——毕竟客户如果看到测试环境的黄条体验会很奇怪。6. 基于上述配置的服务部署与发布建议6.1 前端构建产物的输出目录与部署映射多环境配置的最终目标是让构建产物能够部署到正确的环境。这里建议在构建脚本里加入输出目录的区分方便部署时直接定位。Vite默认的输出目录是dist但多环境时如果都在同一目录输出容易混淆。我习惯在vite.config.ts里根据环境动态设置build.outDir// vite.config.ts export default defineConfig(({ mode }) { const env loadEnv(mode, process.cwd(), ); const isTest mode test; return { plugins: [uni()], base: env.VITE_PUBLIC_PATH || /, build: { outDir: isTest ? dist-test : dist-prod, }, }; });这样做的好处很明显本地构建后目录名直接告诉你这是哪个环境的产物部署脚本也可以按目录去取件。另外小程序端的构建产物通常需要上传到微信公众平台如果test和prod的小程序版本同时开发区分输出目录能有效避免传错包的事故。6.2 静态资源hash与缓存清理前端部署还有一个容易被忽视的问题静态资源缓存。生产环境通常会给静态资源设置长缓存比如Cache-Control: max-age31536000如果文件名不带hash就会导致用户加载到旧版本资源。Vite默认会对JS和CSS文件名添加hash比如index-1a2b3c4d.js。但这个hash只有当文件内容变化时才会变化。如果你在test环境构建了一次又用相同代码构建了一次文件名可能是一样的——这不是bug是内容没变。更要注意的是index.html本身不能设置长缓存否则用户始终加载旧页面。我见过一个项目把index.html也设成了max-age31536000结果每次发版后用户都要强制刷新才能看到新内容。正确的做法是index.html用no-cache让浏览器在每次访问时都去服务器验证文件是否更新。6.3 H5部署到子路径时的资源路径问题这其实是对应前面部署路径的一个补充实践。假设生产环境H5部署在https://example.com/app/构建时VITE_PUBLIC_PATH设为/app/那么页面访问地址是https://example.com/app/自动跳转到/app/#/pages/home/index。静态资源路径是/app/assets/index-xxx.js。根路径https://example.com/可能用来部署官网或其他应用。这种情况下有几个细节要验证一是资源加载是否正常。把index.html里的script和link标签的src/href路径都检查一遍确保前缀是/app/而不是/。二是路由跳转是否正常。hash模式下从#/pages/home/index跳到#/pages/detail/indexURL前缀不变通常没问题。三是分享链接是否正常。小程序分享出去的链接、H5页面的分享卡片地址都要正确带上/app/前缀。如果这些看起来都是对的但我还是建议你在部署后做一次完整的线上检查清单用无痕模式打开页面、刷新路由、检查Network面板的请求域名、确认API请求的baseURL。这套检查做完基本就能确定环境切换是否真的生效了。6.4 小程序的版本管理与环境标识小程序发布时微信公众平台有开发版“体验版”和正式版的区别。这就相当于天然的test环境和prod环境隔离。我在实践中的做法是开发时连接微信开发者工具上传代码时选择上传到体验版这个体验版对应的就是test环境因为构建时用的是--mode test。等测试完毕再执行build:mp-weixin:prod用上传工具的上传正式版功能发布到线上。这里有个细节微信开发者工具的上传功能有个版本号输入框每次上传最好都递增版本号方便在后台区分。另外同一个小程序后台可以同时存在多个版本但只有正式版会对用户生效体验版需要先添加体验成员才能看到。所以test环境的小程序包即使打错了传到体验版影响范围也有限比直接发线上要好排查得多。但要注意的是test环境如果用的是测试号而不是正式小程序后台的测试小程序那需要自行在微信公众平台注册测试小程序获取独立的appid并在manifest.json里区分。这个操作建议在项目初始化时就让管理员完成不要在开发中途更换appid否则各种缓存问题会缠着你。7. 我的实际踩坑记录与改进历程7.1 一次记忆深刻的生产环境连测试数据库事件这个事故我得详细讲一讲因为太典型了。有一次项目交付在即前端需要出一个H5的演示包给客户看。当时我图省事在本地直接执行了uni build -p h5没有加--mode test。按我之前的理解默认mode应该是production所以打出的是生产包。但客户打开页面后所有列表数据都是空的控制台请求直接打到测试服务器。问题出在哪后来排查发现那天我本地项目根目录有一个历史遗留的.env.local文件里面写着VITE_API_BASE_URLhttps://test-api.example.com。当执行uni build -p h5时Vite加载的是.env和.env.production加上.env.local而由于.env.local优先级最高VITE_API_BASE_URL被覆盖成了测试地址。这个事故直接教会我一件事永远不要在项目目录里长期保留.env.local它就像一颗环境炸弹随时可能把生产包炸成测试包。现在我的做法是把.env.local添加进.gitignore需要本地调试时临时创建调试完立刻删除。7.2 manifest.json 动态化的演进过程最初我的manifest.json是一个静态文件test和prod共用同一个小程序appid。后来test环境需要单独的测试账号就只能手工改manifest.json再去构建改了忘记改回来是家常便饭。改进方案是用Node脚本在构建前改写manifest.json。思路是在scripts目录下建一个update-manifest.js读取环境变量后patch对应字段然后再调用uni build。这个方案能解决appid切换的问题但引入了一个新问题manifest.json被脚本改动之后如果开发工具重新读取可能会报配置已变更提示。尤其是HBuilderX打开项目的时候它会直接读取manifest.json做可视化渲染改乱了会导致IDE识别异常。后来我发现uni-app CLI项目中manifest.json其实可以在构建时用环境变量替换只要写成模板字符串就行。但这种做法对JSON格式要求严格如果一个引号写错整个构建就崩了可读性也差。权衡下来目前我的推荐是能不改manifest就不改尽量只通过import.meta.env管理业务环境配置确实需要改的平台字段用独立脚本处理并且每次执行完恢复现场。7.3 TypeScript类型系统对多环境配置的隐藏支持在TypeScript项目中import.meta.env默认只有MODE、BASE_URL、PROD、DEV、SSR这几个字段。要想让VITE_API_BASE_URL这些自定义变量有类型提示就需要在src/env.d.ts里做类型扩展// src/env.d.ts interface ImportMetaEnv { readonly VITE_API_BASE_URL?: string; readonly VITE_MOCK_ENABLED?: string; readonly VITE_LOG_LEVEL?: string; readonly VITE_UPLOAD_URL?: string; readonly VITE_DOWNLOAD_URL?: string; readonly VITE_PUBLIC_PATH?: string; } interface ImportMeta { readonly env: ImportMetaEnv; }这个文件本身不复杂但它解决了一个实际问题写import.meta.env.VITE_API_BASE_URL时如果拼写错误TypeScript会直接提示属性不存在能把大量手误消灭在编译期。顺带说一个更进阶的技巧类型定义和env.ts里的接口可以联动。比如env.ts里定义了EnvConfig接口你可以让parseEnv函数的返回值类型就是EnvConfig这样调用envConfig.apiBaseUrl时编辑器能自动提示字段名。这个体验比裸用import.meta.env舒服太多。7.4 测试环境登录态管理的小技巧test环境往往需要频繁切换不同账号来模拟不同角色。如果你的应用登录态存的是token并且每个token对应一个用户身份那调试起来比较麻烦。好在uni-app可以运行时清除本地缓存。我通常在开发者工具里直接执行uni.clearStorageSync()来快速重置登录态。更进一步的方案是在test环境里做一个账号快捷切换的隐藏入口。通过环境变量识别是否test环境如果是就在我的页面下方显示一个账号列表点击即可切换登录身份。这在联调和演示场景非常实用。但注意这个功能绝不能出现在prod环境里否则任何一个用户都能切换到别人账号。我的实现方式是用条件编译 环境变量双重判断把这段代码隔离在// #ifdef H5 || MP-WEIXIN和// #ifndef PROD这种块里。不过说句实话条件编译里加环境判断有时候很容易混乱我的做法是只用一个环境变量VITE_ENABLE_DEV_TOOLS控制test环境为trueprod环境为false。这样逻辑简单也不会误伤其他端。8. 后续扩展与维护建议8.1 从双环境扩展到多环境staging/previewtest和prod双环境是最基础的诉求但实际项目很容易出现第三个、第四个环境。比如staging预发布、preview演示、dev开发联调。根据我前面的方案新增环境只需要两步新建一个.env.xxx文件然后在package.json里加一条对应的构建脚本。其他代码完全不用改。这套方案的扩展性优势在于环境变量集中、配置逻辑统一、构建命令语义化。新增一个环境只是加文件 加脚本不会引入新的逻辑分支。这比在业务代码里写一堆if (process.env.NODE_ENV xxx)要干净得多。需要注意的只有一点环境文件里的配置项要尽量保持一致。比如.env.test里有VITE_UPLOAD_URL那.env.staging里也要有否则运行时拿到的是默认值行为就可能和预期不一致。建议在新增环境文件时以test配置为模板完整复制再修改差异项。8.2 CI/CD 流水线配置的大致思路多环境配置方案的最终价值一定要和CI/CD流水线结合起来。前端同学手动敲命令构建总会有敲错的时候流水线一旦配好环境参数从界面选项传入基本不会出错。以常见的GitLab CI或GitHub Actions为例可以设置两个手动触发的作业一个是构建测试环境执行npm run build:h5:test npm run build:mp-weixin:test另一个是构建生产环境执行npm run build:h5:prod npm run build:mp-weixin:prod。每个作业的产物分别上传到对应的服务器或发布平台。这里多提醒一句生产环境的构建最好增加一道确认步骤不能和测试环境一样随意触发。实践中我见过有人在CI里配置了自动触发生产构建结果某个feature分支合入主干后生产包也跟着构建发布了连带测试环境的配置一起上了线。这种事故不需要发生一次就会让团队对CI产生恐惧。8.3 配置文件的安全管理环境文件里除了baseURL有时还会包含一些密钥类信息比如地图SDK的key、推送平台的appkey等。这时候就要注意安全问题了。首先.env、.env.test这类文件如果包含敏感信息必须确保它们不会提交到公共代码仓库。.gitignore里要把.env.local排除至于.env.test和.env.production如果是团队内部项目且所有成员都可信可以提交如果是开源项目或外部协作原则上不提交而是在CI/CD变量里配置。其次密钥信息放在前端环境变量里本质上就不安全。因为构建产物是公开的任何人打开开发者工具都能看到。所以真正敏感的密钥比如支付密钥、服务端密钥绝不能放前端必须走后端代理。前端的VITE_变量只放不敏感但需要按环境区分的配置。8.4 维护环境配置文件清单长期维护多个环境文件容易出现某个环境的配置忘了更新的问题。我建议在项目根目录放一份ENV-GUIDE.md里面用表格列出所有环境文件、对应场景、常用构建命令、注意事项。每新增一个环境或者改一个环境变量都同步更新这份文档。这个文档不仅是给团队其他人看的也是给三个月后的自己看的。说实话环境配置这种东西隔一段时间不碰再看的时候真的很陌生。有了一份清晰的清单能少踩不少坑。好了以上就是我基于uni-app多端项目落地的一套test prod多环境配置方案。从最基础的环境变量设计到请求封装的统一处理再到各种端的差异化适配最后到部署发布和常见问题排查基本把整个闭环都覆盖到了。如果你现在正在为uni-app项目的环境问题头疼可以从最简单的env.ts 环境文件开始先把变量集中管理起来然后再逐步完善构建脚本和部署流程。环境治理这件事越早做收益越大拖到项目中期再重构成本会翻好几倍。希望这篇内容能帮你少走点弯路。