ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

uni-app多端环境配置实战:从手动改配置到自动化注入

uni-app多端环境配置实战:从手动改配置到自动化注入 1. 多端开发的配置混乱远比想象中更致命先说一个我自己的真实经历。去年接了个项目要求一套代码同时发布到微信小程序、支付宝小程序、H5和AppiOS/Android。需求本身不难但痛点在于我需要频繁在测试环境联调后端接口而测试环境的后端地址和线上正式环境的地址完全不一样甚至连App端的H5域名、小程序的request合法域名都跟着变。最初我那套暴力配置法是这样的把API地址写死在一个config.js里每次发布前手动改一遍然后再重新编译。结果就是——改了三天出了两次事故一次在测试环境发现调的是线上接口导致测试数据全写进了正式库另一次是验收演示的时候前端页面调的是测试库数据对不上现场翻车。这让我意识到一个问题**多端开发的项目里环境配置这件事如果不提前设计好等它变成事故才处理代价往往是翻倍的。**而且这个坑不是少数人踩很多刚接触uni-app的开发者甚至一些做了一两年的前端都还在用手动改文件的方式处理test和prod环境。更麻烦的是uni-app这个框架本身跨了三种端而三种端的运行机制差异很大H5端跑在浏览器里环境变量可以在构建时注入相对灵活。小程序端的代码是编译后上传到微信/支付宝平台的环境变量在编译时就必须确定上线后再改就晚了。App端虽然支持运行时判断但在实际打包、热更新、原生插件调用时环境切换也有自己的一套规则。所以所谓多环境配置方案绝不仅仅是把URL从test换成prod这么简单。它要解决的是在什么阶段、用什么机制、把哪一份配置注入到哪一端同时还要保证开发、测试、发布、回溯的全流程可预期。下面把我跑通之后的完整方案拆开讲。包括底层原理、目录结构、代码实现、构建踩坑、以及一些常规文档里根本不会写进去的细节。2. 环境变量在uni-app里的真实作用机制编译期的替换游戏要谈到配置方案就必须先搞清楚uni-app是怎么处理环境变量的。很多人在这里有个误解以为process.env.NODE_ENV和process.env.VUE_APP_XXX在uni-app里是运行时读取的。实际上uni-app是基于Vue的编译框架环境变量在构建阶段就已经被静态替换掉了。2.1 process.env.NODE_ENV什么时候生效在uni-app项目里你可以在代码中直接写process.env.NODE_ENV。这个值在编译时被替换为字符串常见取值是development对应开发模式执行npm run dev:*时注入。production对应生产模式执行npm run build:*时注入。举个例子你写// api.js const baseURL process.env.NODE_ENV production ? https://api.example.com : https://test-api.example.com;在npm run build:h5之后编译产物里这段代码就已经变成了const baseURL https://api.example.com;这个过程是替换不是读取。你写多少逻辑判断都行但在构建完成的那一瞬间分支就已经定死了。所以你在真机上调试、在控制台里打印process.env.NODE_ENV看到的永远是构建时期的值和你运行时改设备时间、切换网络没有任何关系。2.2 自定义环境变量.env文件是怎么被加载的uni-app官方基于Vite或webpack的.env机制支持以下文件.env所有环境都会加载。.env.development开发者模式加载。.env.production生产模式加载。文件内容长这样# .env.production VITE_BASE_URLhttps://api.example.com VITE_MOCKfalse然后代码里通过import.meta.env.VITE_BASE_URLVite工程或者process.env.VITE_BASE_URLwebpack工程访问。但这里有个关键的细节**uni-app在编译不同端时是否加载.env文件的行为略有差异。**从我的实际测试看H5端和App端对.env文件的响应比较正常但小程序端在个别版本里对import.meta.env的支持不稳有时候你会发现自定义变量读出来是undefined。这也是为什么我最终的方案不是单纯依赖.env而是走了一层配置文件 构建时注入的组合拳。后面第4部分会详细展开。2.3 条件编译uni-app特有的最后一道保险uni-app支持条件编译注释格式如下// #ifdef H5 console.log(这段代码只在H5端会编译进去); // #endif // #ifndef MP-WEIXIN console.log(这段代码在非微信小程序端会编译进去); // #endif条件编译是在编译器层面做的处理和process.env.NODE_ENV是两回事。你可以在条件编译里继续叠加环境判断实现某端在test环境下的特殊逻辑。我是建议把条件编译当成最后一道保险而不是主要手段原因很简单条件编译的代码在编辑器里虽然能高亮但容易造成逻辑碎片化特别是当一个文件里散布大量#ifdef时可读性会迅速下降。3. H5、App、小程序三个端的配置差异我踩过的那些端差异的坑uni-app号称一套代码多端运行听起来很美但真到环境配置这个层面三端的差异立刻显现。我按端来讲每个端都有值得单独处理的地方。3.1 H5端配置最灵活但域名和端口要一起管H5端的配置是最接近传统Vue项目的。开发模式跑在本地端口默认8080或5173生产构建是纯静态文件。环境变量在构建时注入逻辑清晰。坑在于**本地开发时你访问的页面地址是http://localhost:8080而后端接口可能是http://10.x.x.x:8080浏览器跨域问题会让你崩溃。**所以H5端在test环境下我建议直接在manifest.json里配上h5.devServer.proxy把接口路径代理到测试后端避免每次联调都被CORS拦住。// manifest.json - h5 节点 devServer: { port: 8080, proxy: { /api: { target: http://test-api.example.com, changeOrigin: true } } }注意一个细节changeOrigin一定要设为true否则后端收到的请求头里Host还是localhost:8080部分后端框架比如Nginx配了域名校验的会直接拒绝请求。3.2 App端独立包体和原生SDK的环境绑定问题App端的配置比H5麻烦。因为App一旦打包成apk/ipa里面的API地址就固化了。如果你用的是process.env.NODE_ENV注入那么使用npm run build:app打出来的包是生产包API地址自然是prod。但你用HBuilderX直接运行到手机时它走的是开发模式API地址是test。这样看着没问题实际有个隐蔽的坑**很多团队习惯用HBuilderX的云打包来打测试包而云打包的配置读取的是你当前项目的manifest.json内容和环境变量文件无关。**也就是说你想打个test环境的App包发给测试但manifest.json里的appid、推送SDK配置却是prod的安装包就是个混血儿。另外App端还有原生插件的问题。比如你集成了某个支付SDK或地图SDK它的key通常要区分测试环境和生产环境。这类配置一般不能走process.env因为原生代码不参与JS编译。我的做法是在manifest.json里维护一份当前打包环境的显式标注云打包前人工确认并且在App启动时通过一个外部可读标识比如scene参数做二次校验。后面会提到。3.3 小程序端request合法域名这个天然的环境闸门小程序端有最特殊的一条规则**wx.request只能请求你在小程序管理后台配置过的合法域名而且域名必须ICP备案、必须是HTTPS真机预览时。**这就导致一个结果你的test域名如果没加进合法域名列表那在真机上所有请求都会直接失败报错http://test-api.example.com 不在以下 request 合法域名列表中。很多新手在这里被卡住以为是代码问题其实纯粹是平台规则。针对这一条我的建议是在微信公众平台后台把https://test-api.example.com和https://api.example.com都加进request合法域名开发环境可以勾选不校验合法域名但这是临时手段团队协作时不现实。process.env.NODE_ENV在小程序的编译阶段照常生效所以你可以在代码里放心使用环境判断。还有一个细节**微信开发者工具内置的环境校验和域名校验默认开启但不同版本的默认值不一样。**如果突然发现小程序请求全部失败先检查开发者工具右上角的详情-本地设置-不校验合法域名是不是被意外关掉了。4. 一套可复用的多环境配置方案目录结构 自动化注入前面铺垫了机制和坑现在讲我最终落地的方案。这个方案的核心思路是config目录统一管理配置构建脚本负责注入代码里通过process.env统一读取不散落硬编码。4.1 目录结构和文件清单我的项目根目录下是这样的project-root/ ├── .env.development ├── .env.production ├── .env.test ├── config/ │ ├── index.js │ ├── test.js │ └── prod.js ├── src/ │ ├── api/ │ │ ├── request.js │ │ └── user.js │ ├── utils/ │ │ └── env.js │ ├── pages/ │ └── manifest.json └── package.json两个核心角色.env.test只在npm run dev:test或npm run build:test时加载。config/目录下的test.js和prod.js定义完整的业务配置包括API域名、CDN路径、埋点开关、版本号等。4.2 环境判断工具类的写法写一个src/utils/env.js统一导出当前环境信息// src/utils/env.js const ENV { DEVELOPMENT: development, TEST: test, PRODUCTION: production }; function getEnv() { // 如果通过自定义命令注入优先读取 if (process.env.UNI_ENV) { return process.env.UNI_ENV; } if (process.env.NODE_ENV production) { return ENV.PRODUCTION; } return ENV.DEVELOPMENT; } function isProd() { return getEnv() ENV.PRODUCTION; } function isTest() { return getEnv() ENV.TEST; } function isDev() { return getEnv() ENV.DEVELOPMENT; } export { getEnv, isProd, isTest, isDev, ENV };这里的process.env.UNI_ENV是个自定义变量后面配合package.json脚本注入。4.3 API请求层的自动环境路由config/index.js是一个汇总入口它做的事很简单根据当前环境返回对应配置。// config/index.js import testConfig from ./test.js; import prodConfig from ./prod.js; import { getEnv, ENV } from /utils/env.js; let config; switch (getEnv()) { case ENV.PRODUCTION: config prodConfig; break; case ENV.TEST: config testConfig; break; default: // 开发环境默认也用test配置方便本地联调 config testConfig; break; } export default config;然后test.js和prod.js结构保持一致以test.js为例// config/test.js export default { baseURL: process.env.VITE_BASE_URL || https://test-api.example.com, cdn: https://test-cdn.example.com, enableLog: true, enableMock: false, appVersion: 1.0.0, paymentEnv: sandbox, mapKey: test-map-key, uploadURL: https://test-upload.example.com };注意process.env.VITE_BASE_URL是Vite环境变量。如果你用的是HBuilderX的webpack工程请对应改成process.env.UNI_BASE_URL或者直接不读环境变量、写死到这里。我更推荐在配置文件里保留一个显式的兜底值这样即使环境变量注入失败也能保证代码能跑只是落到一个已知的环境不会出现运行时崩溃。在src/api/request.js里统一引入config// src/api/request.js import config from /config/index.js; import { isTest } from /utils/env.js; const request (options) { const url ${config.baseURL}${options.url}; // 测试环境打日志线上不打避免不必要的日志输出 if (isTest()) { console.log([request] ${options.method} ${url}, options.data); } return new Promise((resolve, reject) { uni.request({ url, method: options.method || GET, data: options.data || {}, header: options.header || {}, success: (res) { // 业务状态码判断 if (res.data.code 0) { resolve(res.data.data); } else { uni.showToast({ title: res.data.msg || 请求异常, icon: none }); reject(res.data); } }, fail: (err) { reject(err); } }); }); }; export default request;整套逻辑下来业务代码里永远不出现具体域名所有开发者都只面向config这一层。换环境、加环境都不需要改动src/api里的任何文件。4.4 manifest.json和AppID按环境处理manifest.json本身是JSON格式无法直接读取.env。但很多项目里不同环境的appid不一样。比如微信小程序你申请了正式和测试两个小程序账号appid是不同的。我的处理办法是写一个NODE脚本在build之前动态修改manifest.json的appid。核心思路先备份一个manifest.json模板然后根据当前环境替换mp-weixin.appid字段。// scripts/switchEnv.js const fs require(fs); const path require(path); const env process.env.UNI_ENV || development; const manifestPath path.resolve(__dirname, ../src/manifest.json); const manifest JSON.parse(fs.readFileSync(manifestPath, utf-8)); // 这里维护一份不同环境的appid映射 const appidMap { test: { mp-weixin: wx1234567890test, mp-alipay: 202100test }, production: { mp-weixin: wx0987654321prod, mp-alipay: 202100prod } }; if (appidMap[env]) { Object.keys(appidMap[env]).forEach((key) { if (!manifest[key]) { manifest[key] {}; } manifest[key].appid appidMap[env][key]; }); fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); console.log([switchEnv] manifest.json 已切换到 ${env} 环境); }这个脚本虽然简单但极其实用。它有另一个好处**防止你手滑把测试appid发到线上包。**因为脚本每次都从模板生成不会把上一次的残留状态带进去。5. 构建与发布阶段的环境注入package.json脚本设计上面的代码逻辑已经打通了但如果每次执行npm run build还要手动改环境那依然不合格。所以要让构建命令自动携带环境信息。这里的关键是自定义process.env.UNI_ENV的注入。5.1 命令行传参的正确用法先看我在package.json里定义的脚本{ scripts: { dev:h5: uni -p h5, dev:mp-weixin: uni -p mp-weixin, build:h5: uni build -p h5, build:mp-weixin: uni build -p mp-weixin, build:app: uni build -p app, dev:h5:test: cross-env UNI_ENVtest uni -p h5, build:h5:test: cross-env UNI_ENVtest uni build -p h5, build:h5:prod: cross-env UNI_ENVproduction uni build -p h5, build:mp-weixin:test: cross-env UNI_ENVtest uni build -p mp-weixin, build:mp-weixin:prod: cross-env UNI_ENVproduction uni build -p mp-weixin } }这里用了cross-env这个包。为什么用它因为UNI_ENVtest uni build这种写法在Mac/Linux上没问题但Windows的cmd和PowerShell不认。团队里有Windows开发者的项目不用cross-env就会反复踩到环境变量不生效的坑。安装它npm install cross-env --save-dev执行顺序是这样的cross-env设置UNI_ENV为test或production。uni build启动编译。编译过程中Vite读取.env.test或.env.production并且process.env.UNI_ENV在Node端脚本比如上面的switchEnv.js和浏览器端代码中都可用。5.2 让Vite的.env和自定义UNI_ENV配合在Vite环境下你需要在前置脚本里把UNI_ENV映射到Vite的--mode参数。uni-app的CLI创建项目时默认支持--mode。我建议直接用--mode test和--mode production然后把你所有环境配置都放进对应的.env.[mode]文件里。一个常用的做法是在.env.test里写入# .env.test VITE_BASE_URLhttps://test-api.example.com VITE_MOCKfalse NODE_ENVproduction注意uni-app的build命令默认把NODE_ENV设为production所以即使你是test环境的构建process.env.NODE_ENV也是production。这会造成一个隐蔽的bug如果你在代码里用process.env.NODE_ENV production来判断是否走正式接口那test构架包也会走正式接口。所以千万不要在业务代码里单独依赖NODE_ENV一定要靠我们前面定义的UNI_ENV变量来区分test和prod。5.3 让自定义条件编译平台发挥作用如果你的项目还有其他特殊场景比如某个渠道版本的包uni-app允许自定义条件编译平台。在package.json的uniStatistics同级添加uni-app的自定义平台配置放在src/manifest.json里也可以{ condition: { custom: { test-mp: { name: 测试环境小程序, platform: mp-weixin, env: test } } } }然后在代码里// #ifdef TEST-MP console.log(只在测试环境小程序包内); // #endif这个方法可以叠加使用但在生产项目里我不建议过度依赖理由和前面一样——可读性。控制条件编译的使用范围只放少数真正需要端维度差异的逻辑。6. 测试与巡检防止配置错误溜进生产环境整个方案搭好之后最怕的就是你以为对了实际没对。所以我在CI流程里加了几个最低成本的校验手段都是只花几分钟但能救命的事情。6.1 构建产物里的环境标识检查先说一个最简单粗暴的检查方法在页面启动时把当前环境做一次显眼展示。在App.vue的onLaunch里写// App.vue onLaunch: function () { const { isTest, isProd, getEnv } require(./utils/env.js); if (isTest()) { console.warn([Environment] 当前运行环境TEST); // #ifdef H5 document.title [TEST] ${document.title}; // #endif // #ifdef MP-WEIXIN uni.setNavigationBarTitle({ title: [TEST] ${uni.getStorageSync(pageTitle) || 页面} }); // #endif } else if (isProd()) { console.log([Environment] 当前运行环境PRODUCTION); } }这样做的好处是测试人员打开APP或者小程序一眼就知道自己拿到的包是什么环境。减少拿测试包当正式包用这种乌龙事件尤其适合外包项目、甲方验收场景。6.2 用接口探测环境真实性如果觉得UI标识还不够可以在关键流程比如登录成功之后拉一个环境探测接口// 登录成功后执行 function verifyEnvironment() { return request({ url: /ping, method: GET }).then((res) { if (res.env res.env ! config.baseURL) { // 环境不匹配报错并提示 uni.showModal({ title: 环境异常, content: 当前前端环境为${config.baseURL}但后端返回${res.env}可能存在环境错配, showCancel: false }); } }); }这个动作成本极低但能有效捉到一种很尴尬的情况前端配置没写错但后端网关把请求转到了其他环境。6.3 自动化巡检脚本再进一步我写了一个简单的Node脚本用于在build完成后自动从产物里提取可疑的URL片段判断是不是把测试IP或测试域名编进了正式包// scripts/checkBuild.js const fs require(fs); const path require(path); const distDir path.resolve(__dirname, ../dist); const patterns [ /https?:\/\/192\.168\.\d\.\d/gi, /https?:\/\/test[-.]/gi, /https?:\/\/localhost:\d/gi ]; function walk(dir, results []) { const files fs.readdirSync(dir); files.forEach((file) { const fullPath path.join(dir, file); const stat fs.statSync(fullPath); if (stat.isDirectory()) { walk(fullPath, results); } else if (/\.(js|json|html)$/.test(file)) { const content fs.readFileSync(fullPath, utf-8); patterns.forEach((pattern, index) { const matches content.match(pattern); if (matches matches.length) { results.push({ file: path.relative(distDir, fullPath), patternIndex: index, matches: matches.slice(0, 3) }); } }); } }); return results; } const issues walk(distDir); if (issues.length) { console.error(构建产物存在可疑环境地址请检查); issues.forEach((item) { console.error(${item.file}: ${item.matches.join(, )}); }); process.exit(1); } else { console.log(环境巡检通过); }这个脚本在正式构建后自动执行如果发现产物里有测试环境的URL特征构建就失败。以后再也不怕把测试包发上线了。7. 这些做法在本项目里的实际效果与后续可扩展方向这套方案在我们项目里跑了大半年从最开始的三个人维护一个手动改配置的config.js变成现在新来的前端拿到代码十分钟就能独立跑起来联调。整体的收益体现在几个方面发布前不再需要人工检查config.js是否有残留地址。测试环境和生产环境的App包通过标题栏标识一眼可辨。新增一个环境比如预发布pre-prod只需要加一个.env.pre和一份config/pre.js脚本按同样模式扩展不需要动任何业务代码。多端同步发布时H5、小程序、App走的是同一套环境和接口判断逻辑不会出现小程序没问题但App连错环境这种端差异。这套方案不绑定具体的团队规模和项目体量。小项目可以减少一些复杂度比如不加switchEnv.js脚本直接用条件编译处理appid大项目可以进一步结合后端配置中心把环境配置做成动态拉取。但底层思想是一样的放弃手写硬编码用单一数据源管理环境变量在构建期完成注入在上线前加上自动化校验。我在实际使用中最大的体会是环境配置这件事做得好的时候感觉不到它的存在做得不好时每次发版都像在赌运气。与其靠细心多检查这种主观因素去避免事故不如把整个流程交给代码和脚本去约束。毕竟人的注意力是最不可靠的资源尤其是加班到凌晨的时候没有任何人能保证不犯低级错误。最后分享一个我们在后续迭代中准备做的优化方向将config目录里的配置做成可以从远程接口拉取的版本这样遇到紧急的域名迁移或CDN切换时不用重新发版只要让后端配置中心下发生效即可。但远程配置也有自己的风险点比如首次加载失败时的降级策略、缓存过期时间的选择这些都要结合业务场景重新权衡。如果大家有兴趣等我们落地稳定之后再来分享。
返回列表