
1. 项目概述与需求拆解先说说我接手这个事情的背景。公司同时维护三端产品线H5、iOS/Android App、微信小程序用的都是uni-app框架。之前代码里写接口地址的习惯特别随意有人直接写死http://192.168.1.10:8080有人用https://api.example.com还有人测试环境和线上环境共用一个接口改配置全靠全局搜索替换每次发版都提心吊胆生怕漏改一个域名导致线上数据错乱。uni-app 这套方案本身的跨端能力很强但正因为能同时编译到 H5、App、小程序环境配置的问题才更容易被忽略。很多人以为process.env.NODE_ENV就能区分所有环境实际在 uni-app 里这套逻辑还不完全够用尤其是区分“测试环境”和“生产环境”的时候必须把条件编译、环境变量、自定义构建参数结合起来才能做到一套代码在三个端、两套环境之间平滑切换。这篇文章就把我踩过的坑、验证过的方案完整梳理一遍。核心目标有三个一套代码H5、App、小程序都能跑test 环境走测试域名prod 环境走正式域名切换时不用改源代码构建产物要能一眼看出是从哪个环境打出来的避免发错包。适用对象是正在用 uni-app 做跨端项目的同学尤其是团队里有多人协作、需求变更频繁、需要快速迭代的中小型团队。如果你只是个人做 demo环境问题不突出但只要你开始接真实后端、涉及联调和上线这套方案就能帮你省掉很多无意义的撕扯。先说结论我最终选用的方案是“配置文件 构建参数 条件编译”三者组合。下面我会把为什么这么选、每一步怎么落地说清楚。2. 整体方案设计与核心选型逻辑2.1 多环境配置的本质是什么环境配置管的无非是三类东西接口基础地址前端功能开关比如是否开启日志、是否使用 mock、是否展示调试入口第三方平台凭证App 里的推送 key、小程序 appid、H5 统计 SDK 的 id。这些内容一旦写死后面每次联调、测试、发布都要动代码改动就意味着风险。理想状态是把“构建时环境”和“运行时读取”分离构建之前确定当前打的是什么包然后代码只负责从环境对应的配置里取值。uni-app 项目里环境信息有两个天然载体process.env和运行平台。process.env.NODE_ENV在 HBuilderX 打包时默认是development在 CLI 创建的项目里可以通过 vue 命令或交叉环境变量传入。但这里有个坑process.env.NODE_ENV在 uni-app 打包时并不总能区分 test 和 prod因为很多场景下非生产环境都叫 development。所以我引入了process.env.NODE_ENV判断基础模式再用自定义构建参数UNI_ENV或VITE_APP_ENV来专门指定 test/prod。2.2 为什么不用单个.env文件了事Vite 本身支持.env.development、.env.productionuni-app CLI 创建的项目也可以用 Vite 的这套机制。我在最初尝试时也用了.env文件但很快发现几个新问题小程序平台里如果配置里出现了浏览器专有的window相关逻辑编译时容易报错需要额外兼容同一个后端环境H5 域名和小程序域名往往不一样因为服务器要求域名校验单个VITE_API_BASE不够用需要按平台再拆分import.meta.env在 uni-app 的部分旧版本编译链中支持不彻底尤其到了 App 原生渲染层会丢失。所以最稳妥的做法是把环境配置写成常量 JS 文件然后通过uni-app的条件编译#ifdef去选择平台字段配合顶部入口读环境。这样无需依赖框架对.env的实现细节只要 JavaScript 能跑这套配置就能跑。缺点是需要多写一点代码但换来的是全端全环境统一值得。2.3 方案目录结构设计我的统一方案目录长这样src/ ├── config/ │ ├── index.js │ ├── env.test.js │ ├── env.prod.js │ └── env.dev.js ├── utils/ │ └── request.js ├── pages/ ├── static/ └── App.vueconfig/index.js是总入口负责判断当前构建环境并导出对应配置对象。env.dev.js、env.test.js、env.prod.js分别存放不同环境下的基础信息。这样目录清晰后端域名变更时只需改对应文件不动业务代码。2.4 跨端平台差异的应对策略uni-app 可以通过process.env.UNI_PLATFORM拿到当前编译平台值为h5、mp-weixin、app等。我常把它和process.env.NODE_ENV一起用动态拼接出不同端、不同环境下的接口地址。设计时特别要注意不要用一个字符串包打天下因为 H5 和小程序面对同一个后端时请求头、跨域策略、cookie 携带方式都不一样。比如 H5 端在浏览器里调试时本机localhost:8080需要代理到测试服务器而小程序端不允许随便使用 ip 和端口必须用备案域名。这些场景不是一个 URL 就能覆盖的所以我会把配置拆成apiBaseUrl、h5ProxyTarget、oauthRedirectUri等多个字段。3. 核心实现细节与配置要点3.1 环境配置文件的标准化写法先看env.test.js和env.prod.js的标准写法。我通常把公共字段提炼一下避免三个文件里重复一大堆。config/env.test.js// 测试环境公共配置 export const testEnv { isProd: false, useMock: true, enableDebug: true, baseUrl: { H5: /api, // H5 走代理路径由 devServer 转发到目标服务器 MP_WEIXIN: https://test-api.example.com, // 小程序必须用完整域名 APP: https://test-api.example.com, }, fileBaseUrl: https://test-static.example.com, thirdParty: { amapKey: test_amap_key, }, }config/env.prod.js// 生产环境公共配置 export const prodEnv { isProd: true, useMock: false, enableDebug: false, baseUrl: { H5: https://api.example.com, MP_WEIXIN: https://api.example.com, APP: https://api.example.com, }, fileBaseUrl: https://static.example.com, thirdParty: { amapKey: prod_amap_key, }, }config/env.dev.js用于本地开发可以不写域名H5 直接走代理小程序和 App 则指向本地电脑 IP 或者内网测试服务器。config/index.js里统一读取import { testEnv } from ./env.test import { prodEnv } from ./env.prod import { devEnv } from ./env.dev function getPlatform() { return process.env.UNI_PLATFORM || h5 } function getEnvConfig() { const envFlag process.env.UNI_ENV || process.env.NODE_ENV || development if (envFlag production || envFlag prod) { return prodEnv } if (envFlag test || envFlag testing) { return testEnv } return devEnv } const rawEnv getEnvConfig() const currentPlatform getPlatform() export const config { ...rawEnv, platform: currentPlatform, isH5: currentPlatform h5, isMpWeixin: currentPlatform mp-weixin, isApp: currentPlatform app, currentBaseUrl: rawEnv.baseUrl[currentPlatform] || rawEnv.baseUrl.H5, } export default config这样写的好处是业务代码里永远只需要import { config } from /config然后统一用config.currentBaseUrl发请求完全不用关心现在打的是哪个端哪个环境。3.2 package.json 构建脚本怎么配用 HBuilderX 界面打包也可以但如果想让团队里所有人都用同一套命令最好通过 CLI 方式或者 HBuilderX 配合自定义脚本。我的package.json脚本示例{ scripts: { dev:h5: uni, dev:mp-weixin: uni -p mp-weixin, build:mp-weixin:test: cross-env UNI_ENVtest uni build -p mp-weixin, build:mp-weixin:prod: cross-env UNI_ENVprod uni build -p mp-weixin, build:h5:test: cross-env UNI_ENVtest uni build -p h5, build:h5:prod: cross-env UNI_ENVprod uni build -p h5, build:app:test: cross-env UNI_ENVtest uni build -p app, build:app:prod: cross-env UNI_ENVprod uni build -p app } }这里用了cross-env保证 Windows 和 macOS 下都能正确设置环境变量。UNI_ENV就是我在config/index.js里用来判断 test/prod 的开关。需要注意uni build -p app在 CLI 项目里通常生成的是 App 资源包真正打包安装包还是要在 HBuilderX 云打包或本地离线打包时选择资源目录。所以build:app我通常理解为“生成 App 端的编译产物”。3.3 请求库统一封装环境配置最终要落到请求层。我的utils/request.js基于 uni.request 做了封装核心逻辑是统一从 config 获取baseUrl请求前拼接完整地址。import config from /config const request (options) { const { url, method, data, header {}, timeout 15000 } options const baseUrl config.currentBaseUrl const fullUrl /^https?:\/\//.test(url) ? url : ${baseUrl}${url} return new Promise((resolve, reject) { uni.request({ url: fullUrl, method: method || GET, data: data || {}, timeout, header: { Content-Type: application/json, ...header, }, success(res) { if (res.statusCode 200 res.statusCode 300) { resolve(res.data) } else { reject(new Error(接口错误${res.statusCode})) } }, fail(err) { reject(err) }, }) }) } export default request封装里有几个细节值得留意。第一如果业务代码传入的 url 本身是http://或https://开头就直接使用方便个别第三方接口直连。第二测试环境下我习惯在 header 里加一个X-Env: test的自定义字段后端可以据此识别请求来源环境排查问题更快捷。第三App 端如果遇到证书校验问题不要轻易在正式环境关闭校验尽量让运维配好证书。3.4 H5、小程序、App 三端各自的坑先说 H5。最麻烦的是跨域。本地开发时apiBaseUrl如果直接写线上测试域名浏览器会因为 CORS 拦截请求即便后端开了一个Access-Control-Allow-Origin也得处理 preflight 请求。我选择的是让 H5 走相对路径/api然后由 vite 的 devServer 代理到测试服务器。这样既绕开了跨域上线时配置文件再改成正式域名不影响代码。再说小程序。微信小程序平台有域名白名单机制request 合法域名必须在 mp 后台配置而且不能有端口不能是 IP。所以我给MP_WEIXIN单独配置一个域名。测试环境如果临时要联调建议后端也提供带正确的 HTTPS 证书域名不要用 ip 强行加白名单那样很容易被审核拒绝。最后说 App。App 端不像 H5 有浏览器同源政策也不像小程序有白名单限制但存在两个问题一是 Android 9 默认禁止明文 HTTP 协议访问如果测试环境还是http://需要在manifest.json中配置android:usesCleartextTraffictrue或者只在 debug 包开启二是 App 打包时process.env.NODE_ENV可能不会执行 webpack 的替换导致process.env.UNI_ENV在纯原生渲染层读取不到。解决办法是用打包工具预定义__UNI_ENV__一份到全局对象或者干脆让 App 端也读config/index.js里的统一出口因为那个文件在编译前就完成了计算不会运行时丢失。4. 实操落地与常见错误排查4.1 从零引入这套配置的完整步骤我给团队里小伙伴演示操作时一般分五步走创建src/config目录添加dev/test/prod三个环境文件和index.js汇总入口在package.json中加入包含UNI_ENV的构建脚本并安装cross-env改造utils/request.js让所有请求走config.currentBaseUrl在 H5 端配置vite.config.js的 server.proxy把/api代理到目标环境用不同命令分别打 test 和 prod 包在页面上打印当前环境标识验证切换结果。上面第 4 步的vite.config.js关键配置如下import { defineConfig } from vite import uni from dcloudio/vite-plugin-uni export default defineConfig({ plugins: [uni()], server: { port: 8080, proxy: { /api: { target: https://test-api.example.com, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ), }, }, }, })这段配置的作用是H5 代码里发/api/user/login请求时vite dev server 会把请求转发到https://test-api.example.com/user/login。注意rewrite中把/api前缀去掉了因为很多后端路径本身不包含/api如果后端接口普遍带/api前缀这里就不用 rewrite。4.2 环境标识不要省给页面加上角标有一种“发错包”的笑话在业内太常见了。开发同学拿着 prod 包以为是 test 包联调或者拿 test 包给客户演示结果接口全是 mock 数据。为了避免这些我在登录页和首页右上角加入一个环境角标根据config.isProd判断是否渲染。测试环境显示“测试环境”四个大字生产环境显示小字“prod”有时候还附加一个随机颜色区分。其实做法很简单在App.vue的onLaunch里把环境信息挂到全局或者直接在页面onLoad里读取import config from /config onLoad(() { if (!config.isProd config.enableDebug) { uni.showToast({ title: 当前测试环境, icon: none, duration: 2000, }) } })这个小细节的好处是任何人打开页面第一眼就知道环境对不对不用去翻控制台或者看 network 请求域名。尤其是多端联调时测试手机、开发机浏览器、小程序开发者工具可能同时开着没有角标很容易串环境。4.3 环境字段在 manifest.json 中的处理uni-app 的manifest.json里有很多配置是分端分的比如 App 的 appid、小程序 appid、H5 的 title 等。理论上不同环境可以对应不同 appid但实操中大部团队不会为 test 单独注册一个微信小程序 appid因为测试版本用真机预览或者体验版就够了。我的建议是小程序测试环境使用“开发版/体验版”账号appid 与 prod 保持一致后端通过登录态自动识别环境H5测试环境使用独立子域名如test-app.example.com避免污染正式环境的 localStorageApp如果是 Android测试包和正式包必须用不同 applicationId否则无法同时安装在同一台手机。这个可以在manifest.json中通过“自定义打包基座”或离线打包时修改包名实现。很多新人容易忽略“不同环境的数据隔离”。H5 端 localStorage 是按域名隔离的如果测试和正式环境共用域名用户可能在测试环境登录了正式数据这是非常危险的。所以我强烈建议 H5 双环境用不同子域名小程序双环境用不同项目账号或体验版。4.4 常见问题 Fast Check结合我自己的运维经验做了一个速查表覆盖最常见的报错和现象现象可能原因解决方式小程序 request 失败提示url not in domain list当前环境域名未添加到小程序后台合法域名在 mp 后台添加或关闭校验仅限开发H5 页面请求 504vite 代理 target 不可达或路径 rewrite 不对用 postman 测试 target 直连是否通再看代理配置App 安卓访问http://接口报错Android 9 阻塞明文流量在 manifest 配置 usesCleartextTraffic或测试环境改用https构建后process.env.UNI_ENV为 undefined脚本没通过 cross-env 注入检查 package.json script确认是cross-env UNI_ENVtest开头测试包显示了生产域名配置文件环境选择逻辑错误检查config/index.js判断顺序打印process.env.NODE_ENV确认同一手机 test 包和 prod 包相互覆盖包名或 appid 冲突为 test 包设置独立 applicationId小程序开发者工具无法访问局域网 ip开发者工具默认不校验域名但禁用了局域网权限详情 → 本地设置 → 勾选“不校验合法域名”或在“网络”中选择不限制排查环境问题第一个动作永远是“先定位当前构建的环境到底是谁”。我习惯在config/index.js里写一行console.log([env] current: , envFlag, currentPlatform, config.currentBaseUrl)然后看编译输出或端上控制台。通过日志排除“环境变量压根没注入”的问题比瞎猜要高效得多。5. 进阶优化与工程化扩展5.1 把环境配置做成动态下发以上方案属于“静态构建时配置”优点稳定可控缺点是需要重新打包才能调整接口地址。有些团队追求更灵活希望不换包就能改环境比如 App 安装包内置一个“环境切换”入口测试人员扫码后自动切换服务器。我这里是这么做的在 App 端封装一个syncRemoteEnv()函数启动时先读取本地storage里的自定义环境配置如果发现存在远程拉取的环境覆盖标记就用它覆盖本地config否则用默认构建配置。这个能力我限定在非 prod 环境或者启用enableDebug时有效正式环境一律禁止远程覆盖。它的好处是测试人员不用等重新打包坏处是容易出现“环境错乱”问题。所以我设置了二次确认提示并且环境标识必须显示在页面上防止测试搞混。5.2 自动化构建区分环境如果你们团队用 Jenkins 或 GitLab CI构建命令就可以那套已经设好的脚本直接接入。比如 GitLab CI 里可以按分支判断main分支打包 proddev分支打包 test合并请求分支打包 test。关键是在流水线里设置同一个环境变量stages: - build variables: UNI_ENV: test build-prod: stage: build script: - npm install - cross-env UNI_ENVprod npm run build:mp-weixin:prod only: - main把这些接入之后开发人员只管合并代码不用手动选择环境也减少了发错包的几率。我建议至少做到三个自动化检查构建成功后读取产物里的config文件确认内嵌域名是目标环境的生成一个包含 git commit 号和构建时间的version.json上传到静态资源服务器测试包自动上传到企业微信或邮件分享附上环境说明。5.3 配置文件与 TypeScript 类型约束如果你的项目用了 TypeScript我会给配置对象定义强类型避免低级的“拼错字段”问题。示例interface EnvConfig { isProd: boolean useMock: boolean enableDebug: boolean baseUrl: RecordH5 | MP_WEIXIN | APP, string fileBaseUrl: string thirdParty: Recordstring, string }当后端接口新增一个网关地址时需要在三个环境文件里同时补充这个字段。如果没有类型约束只改了其中一个上线后就会发现某个环境读undefined非常隐蔽。类型定义至少能帮助你在编译阶段发现缺字段不让带病代码进到测试。5.4 多端的“环境配置”与“权限配置”配合环境配置除了接口地址还经常牵扯到权限。比如测试环境需要注入一个“测试账号”但生产环境绝不能出现测试账号相关内容。我的做法是在环境配置里增加featureFlags对象featureFlags: { showMockLogin: true, verboseLog: true, useDemoData: false, }页面里以config.featureFlags.xxx控制按钮和入口显示。这套开关体系比直接注释代码优雅得多尤其适合团队里后端没就绪、前端需要 mock 数据的阶段。上线前只要检查prodEnv.featureFlags里的危险项是否全部为 false 即可。6. 最后分享两个小经验第一个环境配置一定不要让业务页面直接去读process.env.UNI_ENV这种底层字段。我今天已经把底层读取逻辑收敛到了config/index.js业务页面只对着config.currentBaseUrl、config.featureFlags、config.isProd编程。这样即使以后换了一套环境变量机制页面也不用改。第二个写环境配置文件时记得把“后端 API 路径前缀”也统一定义成变量例如/user/getInfo不要拼在页面代码里而是放到 API 封装层。因为不同后端服务网关的路径可能完全不同如果页面散落了一百个接口路径环境一变就是一场大工程。我通常把接口统一抽到src/api目录下每个模块一个文件内部通过 request 发请求这样一个文件里的代码就是以“有组织”的方式管理路径而不是散落各处。这套方案在我接手之后从四十分钟的人工改配置缩短到了两秒的构建命令切换团队里新同学拿到项目后不再问“我该改哪里”文档里只有一句话“测试打包跑 test 命令正式打包跑 prod 命令看顶部角标确认即可。”这就够了。