6.7 KiB
环境变量分层(.env 与 WebSitePolicy 的分工)
适用范围:项目根 .env / .env.development / .env.production、src/types/env.d.ts、src/policy/custom/webSitePolicy.ts、src/common/http.interceptor.ts 的地址取值。
本文记录"为什么这样分层""三端读取为什么必须写字面量",以及新增一个变量的固定步骤。错误处理口径见 error-handling.md,路由与权限见 page-routing.md、permission.md。
1. 分层模型:谁覆盖谁
vite.config.ts 没有设置 envDir / envPrefix,所以走 Vite 默认:
- 加载目录 = 项目根(
uniapp/youjiala/,即index.html所在层),不是src/; - 只有
VITE_前缀的键会注入import.meta.env,其它键不会(也不该)出现在前端产物里。
| 文件 | 何时生效 | 是否提交 |
|---|---|---|
.env |
所有 mode 的基底 | 提交 |
.env.development |
uni / uni -p xxx(mode=development) |
提交 |
.env.production |
uni build / uni build -p xxx(mode=production) |
提交 |
.env.local、.env.development.local、.env.production.local |
同名 mode 下优先级最高 | 不提交(.gitignore 里 *.local 已覆盖) |
mode 由 uni 命令决定(要指定别的 mode 得显式加 --mode):
uni / uni -p mp-weixin / uni -p app => development => .env + .env.development(+.local)
uni build / uni build -p xxx => production => .env + .env.production(+.local)
同名项后者覆盖前者;.env.development / .env.production 里只列与 .env 不同的项,重复抄一遍只会让三处值互相打架。
2. 只放"换环境要动"的东西
当前只有三个键,全部是后端根地址:
| 键 | 生效场景 | 缺省回落值 |
|---|---|---|
VITE_WEB_API_ROOT |
发布构建(三端共用) | https://uat.youjiala.com |
VITE_WEB_API_ROOT_DEV |
开发构建,H5 | http://localhost:5235 |
VITE_WEB_API_ROOT_DEV_APP |
开发构建,App 真机/模拟器 | https://uat.youjiala.com |
分工纪律:
- 品牌与文案(
Title/Copy/Beian)、路由常量(Dashboard/Web401~404)、时间格式留在webSitePolicy.ts的常量里,不进.env:它们跟着版本走,不跟着环境走。 - 密钥、token、数据库口令一律禁止进
.env。前端产物人人可读,.env又被提交,任何写进来的凭据等同于公开发布。 - 本机差异(自己的后端端口、局域网 IP)写
.env.*.local,不要提交本机地址。
3. fallback:删掉 .env 也必须能构建
webSitePolicy.ts 里每个地址都写成"env 优先、常量兜底":
/** 取环境变量:非字符串、空串、纯空白都算未配置,回落 fallback */
function env(value: string | undefined, fallback: string): string {
const text = String(value ?? "").trim();
return text.length > 0 ? text : fallback;
}
export class WebSitePolicy {
static readonly WebApiRootDev: string = env(
import.meta.env.VITE_WEB_API_ROOT_DEV,
"http://localhost:5235",
);
}
所以:
.env缺失、键漏写、值写成空串或只有空格 —— 都落到文件里的常量,构建不报错、产物可运行;- 文件里的常量是最后一道防线,不是摆设,改动它等于改默认环境,需要同期更新本文与 README 的表格。
http.interceptor.ts 按端挑选常量(#ifdef APP-PLUS 是 uni 的条件编译,只有 App 构建会把那行编进去):
let devBaseUrl = WebSitePolicy.WebApiRootDev;
// #ifdef APP-PLUS
devBaseUrl = WebSitePolicy.WebApiRootDevApp;
// #endif
export const baseUrl = import.meta.env.DEV ? devBaseUrl : WebSitePolicy.WebApiRoot;
4. 硬规矩:只能用字面量读
import.meta.env.VITE_WEB_API_ROOT; // ✅ 三端都正常
import.meta.env[key]; // ❌ 小程序 / App 真机上拿到 undefined
H5 走浏览器 ESM,import.meta.env 是个真实对象,动态取键也能 work;小程序与 App 端由 uni 编译器在构建期把 import.meta.env.VITE_XXX 静态替换成字符串字面量,动态键没人替换,运行时对象根本不存在。
src/types/env.d.ts 显式声明这三个键(vite/client 自带的 ImportMetaEnv 有 [key: string]: any 索引签名,不声明时拼错键名是 any、编译器一声不响)。新增变量必须同时补这份声明,否则失去拼写保护。
5. 新增一个环境变量的固定四步
- 项目根
.env加VITE_XXX=默认值,只在需要按 mode 区分的文件里写覆盖; src/types/env.d.ts的ImportMetaEnv补一行readonly VITE_XXX: string;;- 消费点写成字面量
import.meta.env.VITE_XXX;若是地址/配置类,走webSitePolicy的env()包裹以保留 fallback,业务代码继续只读WebSitePolicy.*; - 更新本文 §2 的表格与 README 的"后端地址"一节。
6. 实测结论(哨兵法)
改动 .env 后不能只看代码,要验证值真的进了产物。做法:把 .env.production 的 VITE_WEB_API_ROOT 临时改成一个必然不可能出现在源码里的域名(如 https://env-probe.invalid),跑 npm run build 与 npm run build:mp-weixin,然后在产物里搜这个域名。
本轮结果(2026-10-13):
| 端 | 产物命中哨兵 | 残留 import.meta |
结论 |
|---|---|---|---|
| H5 | 是 | 否 | env 生效 |
| mp-weixin | 是(dist/build/mp-weixin/policy/custom/webSitePolicy.js) |
否 | 编译器已静态替换,小程序同样吃 .env |
验证完必须把 .env.production 恢复原值。搜索小程序产物要用递归方式:Get-ChildItem -Recurse | Select-String,Select-String -Path 'dist\**\*.js' 这种通配在 PowerShell 里不可靠,会给出假的"0 命中"。
7. 常见坑
- 改了
.env不生效:Vite 不会热更新 env 文件,必须重启uni/uni -p进程。 - 小程序请求发不出去:平台侧要求
https且域名需在小程序后台"request 合法域名"里登记;本地联调用urlCheck: false(manifest.json的mp-weixin.setting)只是让开发者工具不拦,正式包仍受平台约束。 - H5 连本地后端报 CORS:是后端未放行开发端口,不是
.env的问题;改VITE_WEB_API_ROOT_DEV换到已放行的地址即可。 - App 真机连不上:真机/模拟器上没有宿主机的
localhost,走VITE_WEB_API_ROOT_DEV_APP(默认已指向 UAT),需要连本机后端时把它改成局域网 IP。 - 别在
.env里写注释续行:dotenv 只认整行#注释,一行末尾换行再接裸文本会被当成键值行解析。