接手uniapp项目久了,你会发现一个特别现实的问题——代码写完了,联调的时候抠接口地址,测试的时候抠接口地址,上线前还在抠接口地址。一旦项目里对接了四五套后端环境,手动切域名的方式就是给自己埋雷,线上环境一不留神就把测试地址给带上去了。这类事故我见过不止一次,有的发生在凌晨发版,有的发生在给客户演示的当天下午。所以多环境配置这事,不是锦上添花,是一个项目从几个人协作走向正规化的必经之路。
今天这篇就从实操角度,把uni-app开发微信小程序时的多环境配置方案完整拆一遍。从最朴素的配置文件切换,到基于Vite环境的自动化区分,再到配合请求封装、自定义条件编译的完整落地流程,每一步都会讲清楚为什么这么做,以及实际项目中容易踩哪些坑。不管是刚接手uniapp的新手,还是已经写了几个项目但环境管理全凭手改的老手,这篇文章应该都能给你一些可以直接抄作业的东西。
1. 多环境配置的整体设计与方案选型
1.1 先搞清楚你的项目到底需要几套环境
很多人在配环境的时候,第一反应就是"开发、测试、生产"三件套。但实际项目里,环境往往没这么简单。我经历过一个中型电商小程序,光联调环境就有三套:本地联调、开发服务器、集成测试,再加上预发布和生产,整整五套。
配置环境之前,先和你后端同学确认清楚这几个问题:
接口域名有几套?如果后端全部是同一个网关地址,那你只需要配一个BASE_URL。如果是微服务架构,不同的业务域走不同的地址,那你还要考虑用映射表管理一组域名,而不是单纯一个字符串。
有没有独立的静态资源地址?有些项目的图片、文件上传走的是单独的OSS或CDN地址,和业务接口域名不是同一个。这一项容易漏,漏了之后你会发现在测试环境图片正常,一上生产就裂图。
WebSocket、H5跳转、支付回调这类特殊地址是不是也跟着环境走?尤其是支付回调,微信支付的后台回调地址是需要固定的,但小程序体内部跳转的URL、公众号网页授权的地址,多半是跟着环境变的。
把这些信息列成一张表,和你的目录结构放在一起,就是你多环境配置的基础输入。别嫌这一步啰嗦,我做过的项目里,至少有三成环境配置返工,都是因为刚开始没把环境清单梳理干净。
1.2 几套主流配置方案,各自的优缺点
目前社区里常见的做法大致有三类。简单列一下,后面实操都围绕它们展开。
第一类:单独一个config.js,手动切环境。这是入门级做法。项目里建一个config目录,放一个index.js,里面写死几个环境对象,然后用一行注释提醒自己"上线前记得把isProd改成true"。好处是直观,坏处是必须靠人肉自觉。一旦团队成员多起来,总有人忘记切,甚至有人提个PR把你已经切好的环境又改回去,因为这个吵架的情况也不少。
第二类:利用Vite的mode和.env文件自动区分。这是目前vue3版本的uniapp项目里最推荐的方案。uniapp内置的vite会读取项目根目录下的.env.development、.env.production,你甚至可以自定义.env.test这样的文件。通过import.meta.env.MODE拿到的就是你当前启动或打包时用的环境标识,脚本里可以基于它自动切换配置。它最大的优势是"不用人肉记状态",编译时是什么模式就自动用哪套环境,团队成员之间不会互相干扰。
第三类:结合条件编译,做平台维度的差异化配置。这个其实是对第二类的补充。uniapp支持#ifdef这种注释语法,可以用来区分微信小程序、APP、H5等不同平台。比如同一个开发环境,H5联调用的是本机局域网IP,但微信小程序的开发工具里又需要用真机预览,域名不能写localhost,这种平台差异用条件编译处理是最顺的。
我给你的建议也很直接——如果你还在用vue2的老项目,第一类方案改造成本最低;如果从vue3+vite起步,直接上第二类加第三类做兜底。我个人目前的主力方案,就是用.env做环境切换,条件编译处理平台差异,config里保留一份常量映射表来收敛所有配置项。
1.3 为什么我推荐在config层收敛而不是到处读env
新手容易犯的一个错误是:在api请求封装里直接读import.meta.env.VITE_API_BASE_URL,在页面里又直接读import.meta.env.VITE_OSS_URL,甚至图片处理、分享配置、地图SDK的key全部散落在各自文件里读env。这样每个文件都和环境变量耦合,改起来烦躁,排查的时候也容易漏。
我更推荐的模式是,在config目录里建一个环境映射文件,把.env里读到的变量统一做一次转换,组装成业务层需要的结构,所有页面和请求封装只和这个配置文件打交道。
举个例子,你在.env里定义的可能是VITE_API_BASE_URL=http://test-api.example.com,但业务层关心的不是一个字符串,而是一组行为——请求超时时长、是否需要模拟数据、上传文件的URL前缀、遇到401跳转到哪个登录页。这些组装逻辑放在config层,业务代码就干净了,后续加环境、加变量,也只动config这一个文件。
2. 环境配置的核心细节与落地要点
2.1 manifest.json里的那些隐藏坑
微信小程序的多环境配置,绕不开manifest.json。开发者在配置文件里常常只盯着mp-weixin.appid这一个字段,但实际有几个细节值得留意。
appid跟着环境走的问题。很多公司一个小程序主体下有多个小程序账号,比如开发版一个、正式版一个。你在manifest.json里只能写一个appid,除非用HBuilderX的运行/发行弹窗去手动选择。这里就有一个很经典的痛点——你用测试小程序appid预览,调了半天的微信登录都正常,结果打包上线前把appid换成正式账号,发现正式环境下用户登录需要重新配置合法域名、业务域名,甚至支付商户号也可能对不上。所以一个比较稳妥的做法是:把不同环境的appid和域名映射关系写进README,发版走固定流程卡点检查。
微信小程序的合法域名是死的。这一点是微信平台和普通web最大的差异。网页端联调随便localhost、跨域都没事,但小程序开发环境下可以在开发工具里勾选"不校验合法域名",真机上这招是行不通的。所以到了真机预览或者体验版阶段,你必须把环境对应的域名在微信公众平台配置成request合法域名。平时配置多环境的时候,如果域名规划的不好,上线前可能要改多处白名单,很容易漏。建议在环境规划期就把API域名、下载域名、上传域名分开,按固定的三级域名去设计,比如api-dev.example.com、api-test.example.com、api.example.com,这样小程序管理后台的白名单配置也能形成固定套路。
2.2 环境变量的命名规范与管理
环境变量能不能一劳永逸不踩坑,命名规范是关键。Vite环境变量的规则是,只有以VITE_开头的变量才会被import.meta.env暴露给前端代码,其他变量不会被打包进去。这套机制本身很简单,但实际项目里命名混乱带来的问题很常见。
我见过的反面教材长这样:.env.development里写API_URL=xxx,另一个同事在代码里写const baseUrl = 'https://xxx'直接硬编码。你问他的时候,他说不知道有环境变量这回事。这种情况不是他能力不行,是配置的可见性太差。
所以命名和目录结构要做的显眼一点。建议在项目根目录固定以下几个文件:
.env # 公共变量,所有环境共享 .env.development # 开发环境 .env.test # 测试环境 .env.production # 生产环境变量统一用VITE_前缀,并且语义要直白:
VITE_ENV = development VITE_API_BASE_URL = http://dev-api.example.com VITE_OSS_URL = http://dev-oss.example.com还有一个细节,.env文件默认情况下Vite只会帮你加载特定后缀的文件,比如开发模式加载.env.development,生产构建加载.env.production。如果你要用自定义的.env.test,得在打包脚本里通过--mode test告诉Vite去加载它,这个我在第三部分实操里会详细说明。
2.3 配置层缓存的坑:小程序不是改完就生效的
做多环境配置的时候,很多人改完代码,在小程序开发工具里一刷新,发现请求还是走的老地址。原因多半不在你的代码,而是小程序本身的缓存机制。微信小程序的代码包缓存和普通浏览器不一样,你改了代码之后,点击编译确实会重新打包,但真机上已经打开过的小程序进程可能还挂着。
遇到过最典型的情况是:前端把环境切到了测试环境,代码也重新上传体验版了,但是手机端之前打开过正式环境的版本,你重新扫码打开体验版,页面栈或者业务数据里残留了旧的host,看起来就像环境没生效。处理方式很简单,微信小程序里加一个简单的启动日志,在App.vue的onLaunch里打一下当前的接口前缀,真机上看日志一目了然,能省掉大量沟通成本。
3. 实操过程:一套可复制的完整环境配置流程
3.1 初始化目录结构与env文件
假设你已经有一个uniapp vue3项目,第一步先把配置目录建好。我的习惯是这样的:
project-root/ ├── .env ├── .env.development ├── .env.test ├── .env.production ├── src/ │ ├── config/ │ │ ├── index.js │ │ └── env.js │ └── api/ │ └── request.js.env(公共变量)长这样:
VITE_ENV = development VITE_APP_NAME = 我的小程序.env.development(开发环境):
VITE_ENV = development VITE_API_BASE_URL = http://192.168.1.100:8080 VITE_OSS_URL = http://dev-oss.example.com.env.test:
VITE_ENV = test VITE_API_BASE_URL = https://test-api.example.com VITE_OSS_URL = https://test-oss.example.com.env.production:
VITE_ENV = production VITE_API_BASE_URL = https://api.example.com VITE_OSS_URL = https://oss.example.com3.2 自定义mode的加载逻辑
vite默认加载规则里,没有.env.test这个概念。开发模式跑的是.env.development,构建跑的是.env.production。所以test环境需要你给它定义一个mode。
uniapp项目的构建脚本一般写在package.json或者HBuilderX的manifest里。用CLI创建的项目,在package.json里这样加命令:
{ "scripts": { "dev:mp-weixin": "uni -p mp-weixin", "build:mp-weixin": "uni build -p mp-weixin", "build:test:mp-weixin": "uni build -p mp-weixin --mode test", "build:prod:mp-weixin": "uni build -p mp-weixin --mode production" } }注意这里的--mode test对应的就是Vite读取.env.test的逻辑。执行npm run build:test:mp-weixin的时候,import.meta.env.MODE就是test,同时.env和.env.test都会被加载进来,文件的优先级是.env.<mode>覆盖.env。
如果你不是在CLI项目里,而是用HBuilderX可视化界面点点点的,那也没问题——HBuilderX的 manifest.json 里,源码视图下可以在mp-weixin节点下配置env相关字段,或者干脆就建立三个不同名字的manifest来区分环境,但那个方式维护成本略高,我还是更建议用手动脚本。
3.3 写配置文件层,避免到处读env
config/env.js 作用是把原始环境变量转为业务对象:
// src/config/env.js const env = import.meta.env export function getEnvConfig() { return { env: env.VITE_ENV || 'development', apiBaseUrl: env.VITE_API_BASE_URL || '', ossUrl: env.VITE_OSS_URL || '', appName: env.VITE_APP_NAME || '默认名称' } } export const isDev = env.VITE_ENV === 'development' export const isTest = env.VITE_ENV === 'test' export const isProd = env.VITE_ENV === 'production'config/index.js 一次性组装业务参数:
// src/config/index.js import { getEnvConfig, isDev, isProd } from './env' const conf = getEnvConfig() export const config = { env: conf.env, // 统一接口前缀 baseUrl: conf.apiBaseUrl, // 上传文件用的地址,和接口地址可能不一样 uploadUrl: conf.ossUrl + '/upload', // 静态资源访问地址 staticUrl: conf.ossUrl, // 开发模式下可以开调试 debug: isDev, // 线上环境统一走https、不开vconsole之类的开关 enableVconsole: !isProd } export default config这样一来,业务层所有的调用都从config里取,比如请求封装、页面跳转、文件夹上传,全部只依赖config。后面要加环境,就改env文件加一个对应的变量,改config组装逻辑,其他代码动都不用动。
3.4 请求封装的统一处理
光配好地址还不行,请求封装里对这个地址的使用方式也决定了整个配置会不会生效。我在request.js里一般是这么处理baseURL的:
// src/api/request.js import config from '@/config/index.js' export function request(options) { const { url, method = 'GET', data = {}, header = {} } = options return new Promise((resolve, reject) => { uni.request({ url: config.baseUrl + url, method, data, header: { 'Content-Type': 'application/json', ...header }, success: (res) => { // 统一的业务码处理、401跳登录之类的逻辑 if (res.data.code === 0) { resolve(res.data.data) } else { uni.showToast({ title: res.data.message || '请求失败', icon: 'none' }) reject(res) } }, fail: (err) => { // 真正的失败原因,网络问题、域名白名单问题都在这里 console.error(`[request error] ${config.baseUrl}${url}`, err) reject(err) } }) }) }这个封装里有个容易被忽略的点:对config.baseUrl保留了一个显式的引用记录。你在fail的回调里打印出来的完整URL,能够一眼看出当前请求打到了哪个环境。我们团队后来排查环境问题的效率提高了不少,靠的就是这条日志。这里也给新手提个醒,报错信息一定要带环境信息,不然线上问题排查会变成猜谜游戏。
3.5 条件编译处理平台差异
有些配置项是平台维度的,和环境维度正交。举个例子,开发环境下H5端联调,地址写http://localhost:8080没问题,但微信小程序端跑真机预览,localhost指向的是手机自己,必须用局域网IP或线上测试域名。这种情况下用环境变量去区分是不够的,需要条件编译来兜底。
条件编译是uniapp里一个很有意思的机制,它靠注释语法控制编译结果:
// src/config/env.js const platformConfig = { // #ifdef MP-WEIXIN mpAppId: 'wx123456' // #endif // #ifdef H5 h5AppId: '' // #endif }还有一个非常常见的应用场景:分享配置。微信小程序里onShareAppMessage的路径参数,不同环境的页面路径往往不一样,比如测试环境下为了便于定位,你需要把某个列表页的query带上from=test。这种逻辑你写在环境变量里会越来越乱,用条件编译包一下反而清爽。
不过要提醒一句,条件编译虽然好用,但不能滥用。项目里的#ifdef如果到处都是,代码的可读性会急剧下降,后期维护的人根本分不清哪些分支是清理过的、哪些已经失效了。我的经验是:条件编译只用来处理平台差异,不做环境差异;环境的差异一律走.env。
3.6 上传微信小程序前的环境确认清单
环境配置做完之后,真正的考验是发布流程。尤其是团队里有多个前端、多个后端,谁改了什么没人说得清。我在这个环节吃过亏之后,给自己定了一个固定的checklist,分享出来可以直接用:
第一,编译产物检查。微信小程序打包产物一般在dist/build/mp-weixin目录下。上传前全局搜索这个文件夹里有没有遗留的localhost、内网IP,甚至更隐蔽的192.168字样的地址。正规团队会在CI上加一道检查命令,命令行一行就能搞定:
grep -r "localhost\|192.168." dist/build/mp-weixin --include="*.js" | wc -l如果这个数字大于0,就别传了,先回去查_env相关文件的加载逻辑。
第二,微信公众平台合法域名检查。在小程序后台的"开发管理-开发设置-服务器域名"里,确认request合法域名里包含了当前环境要用的API域名。测试环境因为只有体验版在用,可能没加白名单,真机预览时会出现"不在以下合法域名列表中"的报错,这个报错几乎100%可以定位到域名白名单问题。
第三,appid检查。用HBuilderX云打包或者本地CLI跑出来的包,在project.config.json里确认编译出的appid是不是你预期环境对应的账号。这个检查不能省,我遇到过开发工具的appid和CI上面的不一致,导致微信登录死活调不通,排查了一个下午才意识到是两个小程序账号。
第四,环境日志开关。理论上生产环境不应该打开调试日志、vconsole、红点提示。这些开关建议从config层控制,而不是手动注释。说白了,你在.env.production里把VITE_ENV=production一设置,isProd就会自动把vconsole禁用掉,不然哪天忘了删调试代码,线上用户就可能看到一堆奇怪的日志输出。
4. 常见问题与排查技巧实录
4.1 开发模式个人踩过最多次的坑:env文件变更不生效
很多刚用Vite环境变量的同学遇到的情况是:改了.env.development里的地址,保存,刷新,发现还是旧地址。原因在于Vite的环境变量是在启动时加载的,部分配置还会被缓存到node_modules/.vite或者小程序开发工具的缓存里。改完env文件,一定要重启dev服务,HBuilderX里就是重新运行到微信小程序,CLI项目就是Ctrl+C然后重新npm run dev。有时候小程序开发工具已经有编译缓存,也要在那个地方点一下"清缓存并重新编译"。这不是玄学,是工具链的机制问题,不是你的代码逻辑问题。
4.2 配置文件里出现undefined或者空字符串
用import.meta.env.VITE_XXX拿到undefined的情况,90%是因为变量名拼写和env文件里的不对应。这里有个小坑:Vite的环境变量赋值必须严格写成VITE_API_BASE_URL=xxx,中间有没有引号都能读取,但是一旦你多写了空格VITE_API_BASE_URL = xxx(等号两边留白),Vite解析的时候可能直接把变量名带着空格传给代码,导致匹配不到。
另外如果你配置变量值里面有特殊字符,比如&、#,要记得用引号包起来。后端联调地址带token参数的话,这一条极易中招。
4.3 微信小程序开发工具里显示请求正常,真机就失败
这个问题排查顺序一般很固定,先从域名白名单看起。开发工具里可以勾选"不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书",但这一项只对开发工具有效,真机完全不认。第二个要看TLS版本,老手机的系统TLS版本过低,如果后端服务器只支持TLS1.3,同样会有兼容问题。第三个排查项是IPv6,有些新环境纯IPv6,部分安卓机器解析有问题。这三个按顺序排一遍,基本能定位90%的真机网络异常。
4.4 多环境导致的"环境串访"问题
场景:测试环境App跳转到了生产环境的页面,或者反过来。这通常是分享链接、扫码跳转、或者某个web-view里带的固定H5地址导致的。对策:所有跨端跳转的URL、path,统一从config里取,不要在业务代码里写死。config里同一个变量在不同环境的值不一样,业务逻辑自然跟着走。如果历史代码里已经写死了一堆地址,建议全局搜索https://相关字段,一个一个审查归属。
4.5 遇到微信小程序报"url not in domain list"的一种冷门情况
这个报错一般就是白名单问题,但有一种冷门情况是:后端反代完返回了302跳转,跳转后的地址不在白名单里。微信小程序的request只认最终响应的地址,中间跳转的地址不符合域名校验也会报错。这时候前端没法处理,要把后端拉过来一起排查。如果后端说"我们没动过",让他去网关层看看重定向逻辑,多半是网关环境分开配了,测试环境用的跳转域名和生产不一致。
最后再分享一个实用经验
多环境配置做到了一定阶段,其实你会意识到,难点不在技术,而在人和流程之间怎么对齐。技术方案再丝滑,团队里总有人习惯直接复制别人的config文件,或者上线前临时改地址,这都会让环境管理回到起点。
我在项目里后来强制做了一件事:把环境标识打在打包产物的可见位置。具体做法是在小程序启动页加一个环境角标,开发环境显示"DEV",测试环境显示"TEST",生产环境不显示。这个角标用config里的env字段控制,成本极低但效果奇好——测试同事拿到的安装包是什么环境,一眼就知道;产品验收的时候也不会再问你"这是新版还是旧版"。这种小细节,比在文档里写十遍"上线前请检查环境"都管用。
多环境配置没有银弹,不同项目、不同团队规模适合的方案都会不一样。但你只要把env文件的加载机制吃透,把config层收敛好,把发布检查清单固化到流程里,这个事基本就稳了。后续如果项目继续扩大,可以考虑再往CI方向走,把环境检查脚本自动跑起来,那又是另外一个层次的事情了。