Skip to content

多环境配置

(1) 创建三个文件

在项目根目录新建 env/ 文件夹统一存放配置,按 .env.{环境} 格式命名:

env.development

ts
# ========== 开发环境配置 ==========
# 环境标识(必填,白名单校验字段之一)
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

ts
# ========== 生产环境配置 ==========
# 环境标识(必填,白名单校验字段之一)
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

ts
# ========== 测试环境配置 ==========
# 环境标识(必填,白名单校验字段之一)
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 中新增白名单校验模块与类型声明:

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 文件
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 指向该目录。

  • 代码如下
ts
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,
    },
  };
});

环境对比

配置项开发环境测试环境生产环境
端口517341738080
热更新 HMR开启
sourcemap保留保留(便于排查)关闭
代码压缩关闭关闭开启
代码分包react/antd/vendor 独立分包
输出目录dist/testdist/production

(4) 修改package.json

  • dev → vite --mode development (开发热更新)
  • build:test / build:prod → tsc -b && vite build --mode {环境} (类型检查 + 打包)
  • deploy:test → 封装测试环境自动化部署命令(当前执行构建,后续可在此追加上传服务器等动作)
  • preview:test / preview:prod → 本地预览对应环境产物
ts
  "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

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
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 组件

ts
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} />
    </>
  );
}