多环境配置
(1) 创建三个文件
在项目根目录新建 env/ 文件夹统一存放配置,按 .env.{环境} 格式命名:
env.development
# ========== 开发环境配置 ==========
# 环境标识(必填,白名单校验字段之一)
VITE_APP_ENV=development
# 环境名称(页面环境标识展示用)
VITE_APP_TITLE=开发环境
# 接口地址
VITE_API_BASE_URL=http://localhost:8080/api
# 服务端口(本地开发服务端口)
VITE_PORT=5173
# 密钥参数(示例,真实密钥请通过 .env.development.local 本地注入,勿提交代码库)
VITE_APP_SECRET=dev-secret-placeholder
# 是否开启 Mock
VITE_USE_MOCK=true.env.production
# ========== 生产环境配置 ==========
# 环境标识(必填,白名单校验字段之一)
VITE_APP_ENV=production
# 环境名称(页面环境标识展示用)
VITE_APP_TITLE=生产环境
# 接口地址
VITE_API_BASE_URL=https://api.example.com/api
# 服务端口(预览服务端口)
VITE_PORT=8080
# 密钥参数(示例,真实密钥请通过 CI/CD 环境变量注入,勿提交代码库)
VITE_APP_SECRET=prod-secret-placeholder
# 是否开启 Mock
VITE_USE_MOCK=false.env.test
# ========== 测试环境配置 ==========
# 环境标识(必填,白名单校验字段之一)
VITE_APP_ENV=test
# 环境名称(页面环境标识展示用)
VITE_APP_TITLE=测试环境
# 接口地址
VITE_API_BASE_URL=https://test-api.example.com/api
# 服务端口(预览服务端口)
VITE_PORT=4173
# 密钥参数(示例,真实密钥请通过 CI/CD 环境变量注入,勿提交代码库)
VITE_APP_SECRET=test-secret-placeholder
# 是否开启 Mock
VITE_USE_MOCK=false备注
WARNING
个文件包含 6 个统一结构的参数: VITE_APP_ENV (环境标识)、 VITE_APP_TITLE (环境名称)、 VITE_API_BASE_URL (接口地址)、 VITE_PORT (端口)、 VITE_APP_SECRET (密钥)、 VITE_USE_MOCK (Mock 开关)。
敏感信息保护 :密钥只写占位符,真实密钥通过 env/.env..local 本地文件注入(Vite 自动加载 .local 文件且优先级更高),并已在 .gitignore 中添加 env/.env..local 规则,保证密钥不进代码库。
(2) 新增白名单校验模块与类型声明
env.ts
在config/env.ts 中新增白名单校验模块与类型声明:
/**
* 环境配置统一入口 + 白名单校验
* 所有环境变量必须通过本模块读取,禁止在业务代码中直接使用 import.meta.env
*/
/** 环境变量白名单:key -> 是否必填 + 值校验规则 */
const ENV_WHITELIST: Record<
string,
{ required: boolean; validate?: (v: string) => boolean }
> = {
VITE_APP_ENV: {
required: true,
validate: (v) => ["development", "test", "production"].includes(v),
},
VITE_APP_TITLE: { required: true },
VITE_API_BASE_URL: {
required: true,
validate: (v) => /^https?:\/\//.test(v),
},
VITE_PORT: { required: true, validate: (v) => /^\d+$/.test(v) },
VITE_APP_SECRET: { required: true },
VITE_USE_MOCK: {
required: false,
validate: (v) => ["true", "false"].includes(v),
},
};
/** 校验环境变量,存在非法配置时直接抛出错误,避免运行异常 */
function validateEnv(): void {
const errors: string[] = [];
// 校验白名单内参数
for (const [key, rule] of Object.entries(ENV_WHITELIST)) {
const value = import.meta.env[key];
if (rule.required && (value === undefined || value === "")) {
errors.push(`缺少必填环境变量: ${key}`);
} else if (value !== undefined && rule.validate && !rule.validate(value)) {
errors.push(`环境变量 ${key} 的值非法: ${value}`);
}
}
// 校验是否存在白名单外的 VITE_ 变量
for (const key of Object.keys(import.meta.env)) {
if (key.startsWith("VITE_") && !(key in ENV_WHITELIST)) {
errors.push(
`存在白名单外的环境变量: ${key},请先在 env.ts 的 ENV_WHITELIST 中登记`,
);
}
}
if (errors.length > 0) {
throw new Error(`环境变量校验失败:\n${errors.join("\n")}`);
}
}
validateEnv();
/** 当前环境标识 */
export const APP_ENV = import.meta.env.VITE_APP_ENV as
| "development"
| "test"
| "production";
/** 环境名称 */
export const APP_TITLE = import.meta.env.VITE_APP_TITLE;
/** 接口地址 */
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
/** 密钥参数 */
export const APP_SECRET = import.meta.env.VITE_APP_SECRET;
/** 是否开启 Mock */
export const USE_MOCK = import.meta.env.VITE_USE_MOCK === "true";
/** 是否开发环境 */
export const IS_DEV = APP_ENV === "development";
/** 是否测试环境 */
export const IS_TEST = APP_ENV === "test";
/** 是否生产环境 */
export const IS_PROD = APP_ENV === "production";备注
WARNING
配置统一入口。内部定义 ENV_WHITELIST 白名单表,登记每个变量的 是否必填 和 值校验规则 (如 VITE_APP_ENV 只能是 development/test/production 三选一、 VITE_API_BASE_URL 必须是 http(s) 开头、 VITE_PORT 必须是纯数字)。模块加载时执行 validateEnv()
校验通过后导出 APP_ENV 、 API_BASE_URL 、 IS_DEV / IS_TEST / IS_PROD 等常量,业务代码统一从这里读取,不再直接碰 import.meta.env 。
在types新增env.ts类型声明
- 新建 types/env.d.ts 文件
/// <reference types="vite/client" />
interface ImportMetaEnv {
/** 环境标识:development | test | production */
readonly VITE_APP_ENV: "development" | "test" | "production";
/** 环境名称(页面环境标识展示用) */
readonly VITE_APP_TITLE: string;
/** 接口地址 */
readonly VITE_API_BASE_URL: string;
/** 服务端口 */
readonly VITE_PORT: string;
/** 密钥参数(通过环境变量注入,禁止硬编码) */
readonly VITE_APP_SECRET: string;
/** 是否开启 Mock:true | false */
readonly VITE_USE_MOCK: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}补充
注意
给 ImportMetaEnv 接口补充类型声明,让所有环境变量有 TypeScript 类型提示和编译期检查。
(3) 改造vite.config.ts
注意
把原来静态的 defineConfig({...}) 改为函数形式 defineConfig(({ mode }) => {...}) ,通过 --mode 参数接收目标环境,用 loadEnv() 从 env/ 目录加载对应配置,并设置 envDir 指向该目录。
- 代码如下
import { fileURLToPath, URL } from "node:url";
import { defineConfig, loadEnv } from "vite";
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
import babel from "@rolldown/plugin-babel";
// https://vite.dev/config/
// 通过 --mode 指定目标环境:development | test | production
export default defineConfig(({ mode }) => {
// 加载 env/ 目录下对应环境的配置文件
const env = loadEnv(
mode,
fileURLToPath(new URL("./env", import.meta.url)),
"",
);
const port = Number(env.VITE_PORT) || 5173;
const isProd = mode === "production";
const isTest = mode === "test";
return {
// 指定环境变量文件存放目录
envDir: fileURLToPath(new URL("./env", import.meta.url)),
plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
"~": fileURLToPath(new URL("./src/assets", import.meta.url)),
},
},
server: {
port,
// 开发环境热更新
hmr: true,
open: true,
},
preview: {
port,
},
build: {
// 测试与生产环境输出到独立目录,避免互相覆盖
outDir: `dist/${mode}`,
// 生产环境关闭 sourcemap,测试环境保留便于排查
sourcemap: !isProd,
// 生产环境开启压缩(rolldown 默认使用 oxc 压缩器),其他环境关闭加速构建
minify: isProd,
rollupOptions: {
output: {
// 生产环境代码分割优化:第三方依赖单独分包
manualChunks: isProd
? (id: string) => {
if (id.includes("node_modules")) {
if (
/[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/.test(
id,
)
) {
return "react";
}
if (id.includes("antd") || id.includes("@ant-design")) {
return "antd";
}
return "vendor";
}
}
: undefined,
},
},
// 测试环境构建产物打印额外信息,便于自动化部署校验
reportCompressedSize: isProd || isTest,
},
};
});环境对比
| 配置项 | 开发环境 | 测试环境 | 生产环境 |
|---|---|---|---|
| 端口 | 5173 | 4173 | 8080 |
| 热更新 HMR | 开启 | — | — |
| sourcemap | 保留 | 保留(便于排查) | 关闭 |
| 代码压缩 | 关闭 | 关闭 | 开启 |
| 代码分包 | 无 | 无 | react/antd/vendor 独立分包 |
| 输出目录 | — | dist/test | dist/production |
(4) 修改package.json
- dev → vite --mode development (开发热更新)
- build:test / build:prod → tsc -b && vite build --mode {环境} (类型检查 + 打包)
- deploy:test → 封装测试环境自动化部署命令(当前执行构建,后续可在此追加上传服务器等动作)
- preview:test / preview:prod → 本地预览对应环境产物
"scripts": {
"dev": "vite --mode development",
"build": "tsc -b && vite build --mode production",
"build:test": "tsc -b && vite build --mode test",
"build:prod": "tsc -b && vite build --mode production",
"deploy:test": "npm run build:test",
"preview": "vite preview",
"preview:test": "vite preview --mode test",
"preview:prod": "vite preview --mode production",
"lint": "eslint .",
"prepare": "husky install",
"commit": "git-cz"
},(5) 修改头部环境标识
修改request.ts
import axios, { type AxiosRequestConfig } from "axios";
import { message } from "antd";
import {
mountLoading,
unmountLoading,
} from "@/components/common/Loading/loading";
import { API_BASE_URL, APP_ENV } from "@/config/env";
const Axios = axios.create({
baseURL: API_BASE_URL, // 设置请求的 base url
timeout: 20000, // 设置超时时长
});
// 所有请求默认携带环境标识,便于日志排查与流量区分
Axios.defaults.headers.common["X-App-Env"] = APP_ENV;
// 设置 post 请求头
Axios.defaults.headers.post["Content-Type"] =
"application/x-www-form-urlencoded;charset=UTF-8";
// 当前正在请求的数量
let requestCount = 0;
// 显示 loading
function showLoading() {
if (requestCount === 0) {
mountLoading();
}
requestCount++;
}
// 隐藏 loading
function hideLoading() {
requestCount--;
if (requestCount <= 0) {
requestCount = 0;
unmountLoading();
}
}
// 请求前拦截
Axios.interceptors.request.use(
(config) => {
// requestCount 为 0 才创建 loading,避免重复创建
if (config.headers.isLoading !== false) {
showLoading();
}
return config;
},
(err) => {
if (err.config?.headers?.isLoading !== false) {
hideLoading();
}
return Promise.reject(err);
},
);
// 返回后拦截
Axios.interceptors.response.use(
(res) => {
if (res.config.headers.isLoading !== false) {
hideLoading();
}
return res;
},
(err) => {
if (err.config?.headers?.isLoading !== false) {
hideLoading();
}
if (err.message === "Network Error") {
message.warning("网络连接异常!");
}
if (err.code === "ECONNABORTED") {
message.warning("请求超时,请重试");
}
return Promise.reject(err);
},
);
// GET 请求
export function get<T = unknown>(
url: string,
params?: object,
config?: AxiosRequestConfig,
) {
return Axios.get<T>(url, { params, ...config }).then((res) => res.data);
}
// POST 请求
export function post<T = unknown>(
url: string,
data?: object,
config?: AxiosRequestConfig,
) {
return Axios.post<T>(url, data, config).then((res) => res.data);
}
export default Axios;(6)演示使用
新建提示组件
- 在components/common/EnvBadge 新建一个组件index.tsx
import { APP_ENV, APP_TITLE, IS_PROD } from "@/config/env";
const ENV_COLORS: Record<string, string> = {
development: "#52c41a",
test: "#faad14",
production: "#ff4d4f",
};
/**
* 页面环境标识(仅内部可见):
* 固定在页面右上角的小标签,生产环境默认隐藏,
* 可通过 localStorage.setItem('__SHOW_ENV_TAG__', '1') 手动开启
*/
export default function EnvBadge() {
const forceShow =
typeof window !== "undefined" &&
window.localStorage.getItem("__SHOW_ENV_TAG__") === "1";
if (IS_PROD && !forceShow) {
return null;
}
return (
<div
style={{
position: "fixed",
top: 8,
right: 8,
zIndex: 9999,
padding: "2px 10px",
borderRadius: 4,
fontSize: 12,
color: "#fff",
background: ENV_COLORS[APP_ENV] ?? "#1677ff",
opacity: 0.85,
pointerEvents: "none",
userSelect: "none",
}}
>
{APP_TITLE}({APP_ENV})
</div>
);
}App.ts 中引入 EnvBadge 组件
import { RouterProvider } from 'react-router-dom';
import router from '@/router/routers';
import EnvBadge from '@/components/common/EnvBadge';
export default function App() {
return (
<>
<EnvBadge />
<RouterProvider router={router} />
</>
);
}