环境变量在前端项目中的正确使用与安全实践
环境变量搞崩了,本地跑得好好的,一上测试服就404
昨天下午差点被一个环境变量问题整崩溃。项目在本地开发时一切正常,API 调用稳得很,结果一部署到测试环境,所有接口直接 404。我第一反应是:后端又改路由了?但查了一圈,人家说没动。那问题肯定在我这儿。
我一开始以为是 Nginx 配置的问题,折腾了半小时 proxy_pass,结果发现根本不是。后来才意识到:前端代码里写死的 API 地址是 http://localhost:3001!这在本地当然没问题,但测试环境怎么可能访问 localhost?
试了三种方案,前两种都踩了坑
我赶紧去翻 Vite 的文档(我们项目用的是 Vite + React)。Vite 推荐用 .env 文件管理环境变量,于是我在项目根目录加了几个文件:
.env.development.env.test.env.production
比如 .env.development 里写:
VITE_API_URL=http://localhost:3001
然后代码里这么用:
const API_URL = import.meta.env.VITE_API_URL;
fetch(${API_URL}/users)
本地跑 npm run dev 没问题。但当我打测试包:vite build --mode test,结果打包后的代码里 VITE_API_URL 居然还是 undefined!
这里我踩了个大坑:Vite 默认只加载以 VITE_ 开头的环境变量,而且 必须在构建时确定。更关键的是,--mode test 对应的文件名应该是 .env.test,但我一开始写成了 .env.testing,所以压根没加载。
改对文件名后,本地 build 出来的静态文件里确实有了正确的 URL。但新问题来了:测试环境有好几套(test、staging、uat),每次换环境都要重新 build 一次?这太反人类了。运维同事直接甩脸:“不能动态配吗?我们通过 Nginx 注入环境变量。”
真正的痛点:构建时注入 vs 运行时注入
这时候我才意识到问题本质:Vite(以及 Webpack)这类打包工具,环境变量是在 构建阶段 替换进去的,打包完就固化了。但很多公司(包括我们合作的客户)希望同一个构建产物能部署到多个环境,靠运行时读取配置——比如从 window.ENV 或一个单独的 config.js 文件。
我之前一直用构建时注入,因为简单。但现在需求变了,得支持运行时配置。怎么办?
我试了下在 index.html 里加一段内联脚本:
<script>
window.APP_CONFIG = {
API_URL: "https://jztheme.com/api"
};
</script>
然后 JS 里直接读 window.APP_CONFIG.API_URL。这样确实能动态改,但有个恶心的问题:这个 config.js 得由运维在部署时生成,或者手动替换。而且如果忘了更新,就又回退到默认值(如果有设的话)。
后来我发现一个折中方案:既保留 Vite 的便利性,又支持运行时覆盖。
最终方案:双保险,优先运行时配置
我的思路是:构建时注入一个默认值,但允许在页面加载时被外部配置覆盖。
首先,保留 .env 文件作为兜底:
# .env.production
VITE_API_URL=https://jztheme.com/api
然后,在 main.js 或入口文件里,不直接用 import.meta.env.VITE_API_URL,而是封装一个 getConfig 函数:
// config.js
function getConfig() {
// 优先使用运行时注入的 window.__APP_CONFIG__
if (window.__APP_CONFIG__ && window.__APP_CONFIG__.API_URL) {
return {
apiUrl: window.__APP_CONFIG__.API_URL,
};
}
// 兜底用构建时注入的变量
return {
apiUrl: import.meta.env.VITE_API_URL || 'https://jztheme.com/api',
};
}
export const appConfig = getConfig();
接着,在 index.html 里预留一个位置让运维注入配置(通常放在 <head> 最顶部):
<!DOCTYPE html>
<html>
<head>
<script>
// 这个会被部署脚本动态替换
window.__APP_CONFIG__ = {
API_URL: "https://test-api.jztheme.com"
};
</script>
<!-- 其他 head 内容 -->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
这样,如果运维在部署时替换了 window.__APP_CONFIG__,就用他们的;如果没有(比如本地开发),就自动回退到 .env 里的值。
亲测有效。现在我们打一次包,运维只需要改 HTML 里那一小段 JS,就能部署到任意环境。再也不用为不同环境分别 build 了。
踩坑提醒:这三点一定注意
整个过程折腾了快一天,总结几个血泪教训:
- 变量名必须带
VITE_前缀:Vite 默认只暴露以VITE_开头的变量到客户端,这是安全机制。别傻乎乎地写API_URL=xxx,它不会被注入到import.meta.env里。 - 不要在 HTML 里直接写敏感信息:虽然
window.__APP_CONFIG__方便,但所有人都能在浏览器里看到。所以这里只放公开的 API 地址,绝不能放密钥、token 之类的东西。 - 本地开发时怎么模拟运行时配置? 我在
vite.config.js里加了个插件,在 dev 模式下自动注入一个假的__APP_CONFIG__,避免报错:
// vite.config.js
export default defineConfig({
plugins: [
// ...其他插件
{
name: 'mock-app-config',
transformIndexHtml(html) {
if (process.env.NODE_ENV === 'development') {
return html.replace(
'<head>',
<head><script>window.__APP_CONFIG__ = { API_URL: "http://localhost:3001" };</script>
);
}
return html;
}
}
]
});
这样本地开发时也不用改 HTML,自动生效。
还有个小瑕疵,但无大碍
目前这个方案有个小问题:如果运维忘了注入 __APP_CONFIG__,而 .env 里的地址又是生产环境的,那测试环境就会意外调到生产 API。虽然概率低,但理论上存在风险。
为了解决这个,我后来在 getConfig 里加了个校验:
function getConfig() {
const runtimeUrl = window.__APP_CONFIG__?.API_URL;
const buildTimeUrl = import.meta.env.VITE_API_URL;
// 如果是测试/预发环境,但用了生产 URL,报警
if (
import.meta.env.MODE !== 'production' &&
buildTimeUrl?.includes('jztheme.com') &&
!runtimeUrl
) {
console.warn('⚠️ 检测到可能误用生产 API,请确认是否注入了 __APP_CONFIG__');
}
return {
apiUrl: runtimeUrl || buildTimeUrl || 'https://jztheme.com/api',
};
}
至少能给开发者一个提示,不至于静默出错。
以上是我踩坑后的总结。这套方案虽然不是最优雅的(理想情况是后端提供统一网关,前端不用管环境),但在现有约束下算是平衡了灵活性和安全性。如果你有更好的方案,比如用 JSON 配置文件动态加载,或者结合 Docker 环境变量注入,欢迎评论区交流!

暂无评论