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的vue3+vite版本配合得最自然。
顺便说一下为什么不用接口下发。我做过的几个项目里,接口下发配置的方案在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-weixin,App是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_VERSION=1.0.0 VITE_API_TIMEOUT=15000然后是.env.test文件,测试环境专属配置:
# .env.test # 测试环境配置 VITE_API_BASE_URL=https://test-api.example.com VITE_MOCK_ENABLED=true VITE_LOG_LEVEL=debug VITE_UPLOAD_URL=https://test-upload.example.com VITE_DOWNLOAD_URL=https://test-download.example.com再是.env.production文件,生产环境专属配置:
# .env.production # 生产环境配置 VITE_API_BASE_URL=https://api.example.com VITE_MOCK_ENABLED=false VITE_LOG_LEVEL=error VITE_UPLOAD_URL=https://upload.example.com VITE_DOWNLOAD_URL=https://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 request<T = any>(options: RequestOptions): Promise<T> { return new Promise((resolve, reject) => { const header: Record<string, string> = { 'Content-Type': 'application/json', ...(options.header as Record<string, string>), }; // 生产环境自动带上token,test环境如果有需要也可以带 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 test,Vite会加载.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_URL=https://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-age=31536000),如果文件名不带hash,就会导致用户加载到旧版本资源。
Vite默认会对JS和CSS文件名添加hash,比如index-1a2b3c4d.js。但这个hash只有当文件内容变化时才会变化。如果你在test环境构建了一次,又用相同代码构建了一次,文件名可能是一样的——这不是bug,是内容没变。
更要注意的是:index.html本身不能设置长缓存,否则用户始终加载旧页面。我见过一个项目把index.html也设成了max-age=31536000,结果每次发版后用户都要强制刷新才能看到新内容。正确的做法是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/index,URL前缀不变,通常没问题。
三是分享链接是否正常。小程序分享出去的链接、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_URL=https://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环境为true,prod环境为false。这样逻辑简单,也不会误伤其他端。
8. 后续扩展与维护建议
8.1 从双环境扩展到多环境(staging/preview)
test和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+ 环境文件开始,先把变量集中管理起来,然后再逐步完善构建脚本和部署流程。环境治理这件事,越早做收益越大,拖到项目中期再重构,成本会翻好几倍。希望这篇内容能帮你少走点弯路。