一、背景

在前端项目开发中,我们经常需要针对不同环境(开发 / 测试 / 预发 / 生产)切换 API 地址或应用配置。
Vite 提供了 .env 文件机制在构建时注入环境变量到 import.meta.env,但构建后的产物配置是静态的、不可再修改

在实际生产部署中,常常需要“一份包跑多个环境”,例如相同构建产物要部署到 测试 / 预发 / 生产
这就需要 运行时配置覆盖 来实现灵活性。

本文结合 .envconfig.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
    当前运行模式,例如 developmentproductionstaging
    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 的区别

它们看起来相似,但作用范围不同

  1. envPrefix(vite.config.js 配置项)

    • 控制哪些变量可以被注入到 客户端代码(import.meta.env) 中。
    • 默认只允许 VITE_ 前缀的变量暴露给前端。
    • 修改 envPrefix 可以增加其它前缀,例如 APP_
    • 影响:运行时前端能不能访问。
  2. 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) // 123456
console.log(env.APP_TITLE) // 我的应用
console.log(env.VITE_API_BASE_URL) // http://localhost:3000/api
return {
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

八、流程图

请添加图片描述

请求流转过程:

.envvite.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 覆盖 → 实现“一份包跑多环境”
posted on 2025-09-22 20:04  lxjshuju  阅读(120)  评论(0)    收藏  举报