Vite 环境变量与运行时配置最佳实践:从 .env 到 一份包 跑多环境,以及开发环境代理、请求配置等
一、背景
在前端项目开发中,我们经常需要针对不同环境(开发 / 测试 / 预发 / 生产)切换 API 地址或应用配置。
Vite 提供了 .env
文件机制在构建时注入环境变量到 import.meta.env
,但构建后的产物配置是静态的、不可再修改。
在实际生产部署中,常常需要“一份包跑多个环境”,例如相同构建产物要部署到 测试 / 预发 / 生产。
这就需要 运行时配置覆盖 来实现灵活性。
本文结合 .env
与 config.js
,总结一套 开发方便 + 部署灵活 的最佳实践方案。
二、.env
文件加载规则
Vite 会根据 mode
加载对应的 .env
文件,并按顺序合并:
文件名 | 加载范围 | 场景 |
---|---|---|
.env | 所有模式 | 通用配置 |
.env.local | 所有模式,本机专用 | 本地私密,不提交 git |
.env.[mode] | 指定模式 | 区分 dev / prod / staging |
.env.[mode].local | 指定模式,本机专用 | 本机覆盖,敏感配置 |
加载顺序(后覆盖前):
.env → .env.local → .env.[mode] → .env.[mode].local
与命令的关系
{"scripts": {"dev": "vite", // 默认 mode=development"build": "vite build", // 默认 mode=production"build:staging": "vite build --mode staging", // 使用 .env.staging"preview": "vite preview" // 预览 dist 产物}
}
npm run dev
→.env.development
npm run build
→.env.production
npm run build:staging
→.env.staging
npm run preview
→ 仅预览打包产物(不会重新加载.env
)
三、环境变量命名规则
1. 必须以 VITE_
开头才会注入前端
VITE_API_BASE_URL="https://api.example.com"
- 以
VITE_ 开头
的变量才会注入到import.meta.env
。 - 不以 VITE_ 开头的变量仍可写在
.env
,但只能在vite.config.js
中通过loadEnv
读取(loadEnv
使用见下文)
⚠️ 注意:不要把私密信息(token、密码)放在
VITE_
变量里,会暴露到前端。
2. 注释与基本语法
- 注释用
#
- 值里有空格/特殊字符 → 加引号
- 也支持
export KEY=value
3. 所有值都是字符串
const enabled = import.meta.env.VITE_FEATURE_ENABLED === 'true'
const timeout = Number(import.meta.env.VITE_TIMEOUT_MS || '5000')
如需对象:
VITE_FLAGS='{"newUI":true,"abTest":0.3}'
const flags = JSON.parse(import.meta.env.VITE_FLAGS || '{}')
4. 变量展开(dotenv-expand)
允许在 .env
中引用已有变量,避免重复:
VITE_API_HOST="https://api.example.com"
VITE_API_VERSION="v1"
VITE_API_BASE_URL="${VITE_API_HOST}/${VITE_API_VERSION}"
结果:VITE_API_BASE_URL=https://api.example.com/v1
5. 内置变量
Vite 提供一些内置变量:
import.meta.env.MODE // "development" | "production" | "staging"
import.meta.env.DEV // true | false
import.meta.env.PROD // true | false
import.meta.env.BASE_URL // 部署时的基础路径
6. 在 vite.config.js
中读取 loadEnv
Vite 提供了 loadEnv(mode, root, prefix)
方法来读取 .env
文件中的变量。
import { defineConfig, loadEnv } from 'vite'export default defineConfig(({ mode }) => {const env = loadEnv(mode, process.cwd(), '') // 读取全部变量console.log(env) // 打印所有环境变量return {server: {proxy: {'/api': {target: env.VITE_API_PROXY_TARGET,changeOrigin: true,rewrite: (p) => p.replace(/^\/api/, ''),},},},}
})
参数说明
-
mode
当前运行模式,例如development
、production
、staging
。
Vite 会根据这个值去加载对应的.env.[mode]
文件。 -
root
项目根目录路径。
一般写process.cwd()
,表示当前 Node.js 进程的工作目录。
这告诉 Vite 从哪里开始查找.env
文件。 -
prefix
用于过滤变量前缀。默认是'VITE_'
,只会返回VITE_
开头的变量。
如果写成''
(空字符串),则返回所有.env
文件中的变量,包括非VITE_
的。
为什么用 process.cwd()
?
process.cwd()
是 Node.js API,表示当前执行命令的工作目录。- 当你在项目根目录执行
npm run dev
时,它就是项目的根目录。 - 所以配合
loadEnv(mode, process.cwd(), '')
,就能确保 Vite 从项目根目录读取.env.*
文件。
示例
.env.development
NODE_ENV=development
APP_SECRET=123456 # 不会注入客户端
VITE_API_BASE_URL=http://localhost:3000/api
vite.config.js
export default defineConfig(({ mode }) => {const env = loadEnv(mode, process.cwd(), '')console.log(env.NODE_ENV) // "development"console.log(env.APP_SECRET) // "123456"console.log(env.VITE_API_BASE_URL) // "http://localhost:3000/api"return {define: {__APP_SECRET__: JSON.stringify(env.APP_SECRET), // 可选择性注入}}
})
7. 自定义暴露前缀
除了默认的 VITE_
,你还可以通过 vite.config.js
中的 envPrefix
配置,允许其它前缀的变量也暴露到客户端代码中:
export default defineConfig({envPrefix: ['VITE_', 'APP_']
})
这样,在 .env
文件中写:
APP_TITLE="我的应用"
VITE_API_BASE_URL="https://api.example.com"
就可以在前端代码里直接访问:
console.log(import.meta.env.APP_TITLE) // "我的应用"
console.log(import.meta.env.VITE_API_BASE_URL) // "https://api.example.com"
envPrefix
vs loadEnv
的区别
它们看起来相似,但作用范围不同:
-
envPrefix
(vite.config.js 配置项)- 控制哪些变量可以被注入到 客户端代码(import.meta.env) 中。
- 默认只允许
VITE_
前缀的变量暴露给前端。 - 修改
envPrefix
可以增加其它前缀,例如APP_
。 - 影响:运行时前端能不能访问。
-
loadEnv(mode, root, prefix)
(函数参数)- 用于在
vite.config.js
里读取.env
文件。 prefix
参数决定返回的变量前缀过滤:'VITE_'
→ 只返回 VITE_ 开头的变量。''
(空字符串) → 返回所有变量(包括非 VITE_ 的)。
- 影响:配置阶段你能拿到哪些变量。
- 用于在
举个例子 🚀
.env.development
SECRET_KEY=123456
APP_TITLE="我的应用"
VITE_API_BASE_URL="http://localhost:3000/api"
vite.config.js
import { defineConfig, loadEnv } from 'vite'export default defineConfig(({ mode }) => {const env = loadEnv(mode, process.cwd(), '') // 拿到全部变量console.log(env.SECRET_KEY) // 123456console.log(env.APP_TITLE) // 我的应用console.log(env.VITE_API_BASE_URL) // http://localhost:3000/apireturn {envPrefix: ['VITE_', 'APP_'] // 允许 APP_ 前缀变量暴露到前端}
})
前端代码
console.log(import.meta.env.VITE_API_BASE_URL) // ✅ 可访问
console.log(import.meta.env.APP_TITLE) // ✅ 可访问
console.log(import.meta.env.SECRET_KEY) // ❌ undefined (没有被暴露)
📌 总结
loadEnv(..., prefix)
→ 影响 vite.config.js 构建阶段能读到哪些变量。envPrefix
→ 影响 import.meta.env 暴露到客户端的变量范围。- 两者不冲突,而是配合使用。
- 开发阶段你可能需要读取全部变量(
loadEnv(mode, root, '')
)。 - 但你只想暴露部分变量给前端(通过
envPrefix
控制)。
- 开发阶段你可能需要读取全部变量(
四、开发环境代理
推荐在开发时:
.env.development
里写VITE_API_BASE_URL=/api
- 通过 vite.config.js 代理转发到后端
.env.development
VITE_API_BASE_URL="/api"
VITE_API_PROXY_TARGET="http://localhost:8080"
vite.config.js
server: {proxy: {'/api': {target: env.VITE_API_PROXY_TARGET,changeOrigin: true,rewrite: (p) => p.replace(/^\/api/, ''),},},
}
👉 前端请求 /api/user
→ 开发时代理到 http://localhost:8080/user
。
👉 生产环境直接用 .env.production
里的完整地址。
五、运行时配置(config.js 覆盖)
打包后,.env
已经固化,想“一份包跑多环境”就要在 运行时覆盖。
public/config.js
window.__APP_CONFIG__ = {API_BASE_URL: "https://runtime-api.example.com",APP_TITLE: "My App (Runtime)"
}
在 index.html
中尽早引入:
<!doctype html>
<html><head><meta charset="utf-8" /><title>My App</title><script src="/config.js"></script> <!-- 必须在应用脚本之前加载 --></head><body><div id="app"></div><script type="module" src="/src/main.js"></script></body>
</html>
在项目中统一封装:
// src/config/appConfig.js
const runtime = window.__APP_CONFIG__ || {}export const APP_CONFIG = {API_BASE_URL: runtime.API_BASE_URL || import.meta.env.VITE_API_BASE_URL,APP_TITLE: runtime.APP_TITLE || import.meta.env.VITE_APP_TITLE,
}
六、Axios 封装
import axios from 'axios'
import { APP_CONFIG } from '@/config/appConfig'const request = axios.create({baseURL: APP_CONFIG.API_BASE_URL,timeout: 10000,
})request.interceptors.response.use((res) => res.data,(err) => {console.error('请求失败:', err.message)return Promise.reject(err)}
)export default request
七、推荐项目结构(示例)与完整代码片段
project/
├─ public/
│ └─ config.js
├─ src/
│ ├─ config/
│ │ └─ appConfig.js
│ ├─ utils/
│ │ └─ request.js
│ └─ main.js
├─ .env
├─ .env.development
├─ .env.production
├─ .env.staging
├─ package.json
└─ vite.config.js
package.json scripts
{"scripts": {"dev": "vite","build": "vite build","build:staging": "vite build --mode staging","preview": "vite preview"}
}
示例 .env.development
VITE_API_BASE_URL="/api"
VITE_API_PROXY_TARGET="http://localhost:8080"
VITE_APP_TITLE="My App (Dev)"
示例 .env.production
VITE_API_BASE_URL="https://api.example.com"
VITE_APP_TITLE="My App (Prod)"
public/config.js(部署时可覆盖)
window.__APP_CONFIG__ = {API_BASE_URL: "https://staging-api.example.com",APP_TITLE: "My App (Staging)"
}
src/config/appConfig.js
const runtime = (window && window.__APP_CONFIG__) || {}export const APP_CONFIG = {API_BASE_URL: runtime.API_BASE_URL || import.meta.env.VITE_API_BASE_URL,APP_TITLE: runtime.APP_TITLE || import.meta.env.VITE_APP_TITLE,
}
src/utils/request.js
import axios from 'axios'
import { APP_CONFIG } from '@/config/appConfig'
import { ElMessage } from 'element-plus'const request = axios.create({baseURL: APP_CONFIG.API_BASE_URL,timeout: 10000,headers: { 'Content-Type': 'application/json' },
})request.interceptors.response.use((res) => res.data,(err) => {ElMessage.error(err.response?.data?.message || err.message || '请求失败')return Promise.reject(err)}
)export default request
八、流程图
请求流转过程:
.env
→ vite.config.js
(dev 代理) → axios(request.js)
→ config.js 覆盖
→ 后端服务
九、常见坑 & 总结
✅ .env
中的所有值都是字符串,需要自己转换
✅ 只有 VITE_
开头的才会注入前端
✅ .local
文件不要提交到 git
✅ vite preview
不会重新读取 .env
✅ 可以用 ${VAR}
引用已有变量(dotenv-expand)
✅ 敏感信息不要放到 VITE_
📌 最终方案
- 开发:
.env.development
+ Vite 代理 - 生产:
.env.production
打包 - 运行时:
config.js
覆盖 → 实现“一份包跑多环境”