Vite 环境特定配置:多环境构建的工程化方案
Vite 环境特定配置多环境构建的工程化方案一、前端多环境构建的配置管理挑战现代前端项目需要在多个环境中运行本地开发dev、测试环境test、预发布环境staging、生产环境prod。每个环境的 API 地址、功能开关、性能策略各不相同。传统方案使用.env文件管理环境变量但在复杂项目中存在以下问题配置分散环境变量、构建配置、运行时配置分散在多个地方类型安全缺失环境变量都是字符串容易因类型错误导致运行时问题打包策略单一不同环境可能需要不同的打包策略如生产环境需要更激进的代码分割调试困难环境差异导致的问题难以本地复现Vite 提供了灵活的多环境配置机制能够通过mode参数和配置文件实现环境特定的构建策略。// 传统 .env 文件方式存在问题 // .env.development VITE_API_URLhttp://localhost:3000/api VITE_ENABLE_MOCKtrue VITE_LOG_LEVELdebug // .env.production VITE_API_URLhttps://api.example.com VITE_ENABLE_MOCKfalse VITE_LOG_LEVELerror // 代码中直接使用缺乏类型安全 const apiUrl import.meta.env.VITE_API_URL; // 可能是 undefined const enableMock import.meta.env.VITE_ENABLE_MOCK true; // 需要手动转换类型二、Vite 多环境配置的技术架构Vite 的多环境配置体系包含三个层次环境变量层使用.env.[mode]文件定义环境变量配置文件层使用vite.config.ts的条件导出根据mode应用不同配置运行时配置层在应用中动态加载环境特定配置在实际开发中三个层次并不总是独立运作它们之间会产生很多微妙的问题尤其是当你调试一个本地正常但预发布环境异常的 Bug 时往往就是某个层次的配置覆盖了另一个。场景描述一个团队在本地开发时使用了VITE_API_URLhttp://localhost:4000测试环境通过.env.test覆盖了该变量为https://test-api.example.com。但团队成员为了调试方便在vite.config.ts的define中又硬编码了__API_URL__: JSON.stringify(http://localhost:4000)。结果测试环境的 API 请求全部发到了本地排查了两天才找到原因。这就是配置优先级混乱引发的典型案例。Vite 的配置加载顺序是从上到下的但define中的变量会直接替换代码中的引用它的优先级高于.env文件// 一个安全的做法让编译时常量和环境变量各司其职 // vite.config.ts export default defineConfig(({ mode }) { return { define: { // 编译时常量与运行环境无关的配置 __APP_VERSION__: JSON.stringify(pkg.version), __BUILD_TIME__: JSON.stringify(new Date().toISOString()), // 不要在这里定义与环境相关的变量 } }; });graph TD A[命令行 mode 参数] -- B{Vite 配置加载} B -- C[加载 .env.[mode]] B -- D[加载 vite.config.ts] C -- E[注入 import.meta.env] D -- F{根据 mode 条件配置} F --|development| G[开发环境配置br/sourcemap: truebr/minify: false] F --|production| H[生产环境配置br/sourcemap: falsebr/minify: true] F --|test| I[测试环境配置br/mock: truebr/coverage: true] F --|staging| J[预发布配置br/sourcemap: truebr/minify: true] E -- K[应用代码] G -- K H -- K I -- K J -- K K -- L[构建输出] style A fill:#e1f5fe style F fill:#fff3e0 style L fill:#e8f5e9配置文件的模块化组织推荐将不同环境的配置拆分为独立文件通过组合方式生成最终配置。// vite.config.ts - 主配置文件 import { defineConfig, UserConfig } from vite; import { resolve } from path; import baseConfig from ./vite/base; import devConfig from ./vite/dev; import prodConfig from ./vite/prod; import testConfig from ./vite/test; import stagingConfig from ./vite/staging; export default defineConfig(({ mode }) { // 基础配置所有环境共用 const configs: Recordstring, UserConfig { base: baseConfig, development: devConfig, production: prodConfig, test: testConfig, staging: stagingConfig }; // 合并基础配置和环境特定配置 const base configs.base; const envConfig configs[mode] || {}; return { ...base, ...envConfig, // 确保 plugins 正确合并 plugins: [ ...(base.plugins || []), ...(envConfig.plugins || []) ] }; }); // vite/base.ts - 基础配置 import { defineConfig } from vite; import react from vitejs/plugin-react; import path from path; export default defineConfig({ plugins: [react()], resolve: { alias: { : path.resolve(__dirname, ../src) } }, build: { target: es2020, outDir: dist, assetsDir: assets, sourcemap: false } }); // vite/dev.ts - 开发环境配置 import { defineConfig } from vite; export default defineConfig({ server: { port: 3000, open: true, proxy: { /api: { target: http://localhost:3001, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } }, build: { sourcemap: true, minify: false } }); // vite/prod.ts - 生产环境配置 import { defineConfig } from vite; import { visualizer } from rollup-plugin-visualizer; export default defineConfig({ build: { sourcemap: false, minify: terser, rollupOptions: { output: { manualChunks: { react-vendor: [react, react-dom, react-router-dom], ui-vendor: [antd, ant-design/icons], utils-vendor: [lodash, dayjs, axios] } } } }, plugins: [ visualizer({ filename: ./dist/stats.html, open: true, gzipSize: true, brotliSize: true }) as any ] }); // vite/test.ts - 测试环境配置 import { defineConfig } from vite; export default defineConfig({ define: { process.env.NODE_ENV: JSON.stringify(test) }, build: { sourcemap: true, minify: false } }); // vite/staging.ts - 预发布环境配置 import { defineConfig } from vite; export default defineConfig({ build: { sourcemap: true, // 保留 sourcemap 便于调试 minify: terser, rollupOptions: { output: { manualChunks: { react-vendor: [react, react-dom], vendor: [axios, dayjs] } } } } });三、实战类型安全的多环境配置系统为了在 TypeScript 项目中获得类型安全的配置管理可以建立环境变量验证机制和配置类型定义。环境变量类型定义src/types/env.d.ts// src/types/env.d.ts interface ImportMetaEnv { readonly VITE_APP_TITLE: string; readonly VITE_API_BASE_URL: string; readonly VITE_ENABLE_MOCK: boolean; readonly VITE_ENABLE_ANALYTICS: boolean; readonly VITE_LOG_LEVEL: debug | info | warn | error; readonly VITE_SENTRY_DSN?: string; readonly VITE_GA_TRACKING_ID?: string; } interface ImportMeta { readonly env: ImportMetaEnv; }环境变量验证src/config/envValidator.ts// src/config/envValidator.ts import { z } from zod; // 使用 zod 进行运行时验证 const envSchema z.object({ VITE_APP_TITLE: z.string().default(My App), VITE_API_BASE_URL: z.string().url(), VITE_ENABLE_MOCK: z .string() .transform(val val true) .default(false), VITE_ENABLE_ANALYTICS: z .string() .transform(val val true) .default(false), VITE_LOG_LEVEL: z .enum([debug, info, warn, error]) .default(info), VITE_SENTRY_DSN: z.string().url().optional(), VITE_GA_TRACKING_ID: z.string().optional() }); export type EnvConfig z.infertypeof envSchema; /** * 验证并解析环境变量 * throws {z.ZodError} 当环境变量不符合 schema 时抛出错误 */ export function validateEnv(): EnvConfig { const result envSchema.safeParse(import.meta.env); if (!result.success) { console.error(❌ Environment validation failed:); result.error.issues.forEach(issue { console.error( ${issue.path.join(.)}: ${issue.message}); }); throw new Error(Invalid environment configuration); } return result.data; }配置中心src/config/index.ts// src/config/index.ts import { validateEnv } from ./envValidator; // 验证后的环境配置单例 let cachedConfig: ReturnTypetypeof validateEnv | null null; export function getConfig() { if (!cachedConfig) { cachedConfig validateEnv(); } return cachedConfig; } // 应用配置接口 export interface AppConfig { app: { title: string; version: string; environment: development | production | test | staging; }; api: { baseURL: string; timeout: number; retryCount: number; }; features: { enableMock: boolean; enableAnalytics: boolean; enableSentry: boolean; }; logging: { level: debug | info | warn | error; }; } /** * 获取完整的应用配置 */ export function getAppConfig(): AppConfig { const env getConfig(); const mode import.meta.env.MODE as AppConfig[app][environment]; return { app: { title: env.VITE_APP_TITLE, version: import.meta.env.VITE_APP_VERSION || 0.0.0, environment: mode }, api: { baseURL: env.VITE_API_BASE_URL, timeout: 10000, retryCount: mode production ? 3 : 1 }, features: { enableMock: env.VITE_ENABLE_MOCK, enableAnalytics: env.VITE_ENABLE_ANALYTICS, enableSentry: !!env.VITE_SENTRY_DSN }, logging: { level: env.VITE_LOG_LEVEL } }; } // 类型安全的配置 hookReact import { useMemo } from react; export function useAppConfig(): AppConfig { return useMemo(() getAppConfig(), []); } // 配置使用示例 import { getAppConfig } from ../config; // API 客户端配置 export const apiClient axios.create({ baseURL: getAppConfig().api.baseURL, timeout: getAppConfig().api.timeout }); apiClient.interceptors.request.use(config { if (getAppConfig().features.enableMock) { // 使用 mock 数据 config.adapter require(axios-mock-adapter); } return config; }); apiClient.interceptors.response.use( response response, error { if (getAppConfig().logging.level debug) { console.error(API Error:, error); } return Promise.reject(error); } );场景描述在 type-safe 环境配置落地的过程中最大的坑不是 schema 定义而是VITE_前缀的约束。Vite 只暴露VITE_开头的环境变量给客户端代码如果你在.env.production中定义了API_SECRETxxx在import.meta.env中会得到undefined。更隐蔽的是TypeScript 类型声明ImportMetaEnv时你定义了readonly API_SECRET: string编译期不报错运行期却缺失线上直接挂了。// 踩坑定义 non-VITE_ 前缀变量编译通过但运行时 undefined // .env.production API_SECRETsk-xxxxx // ❌ 不会被暴露到客户端 VITE_API_SECRETsk-xxxxx // ✅ 会被暴露 // src/types/env.d.ts 中只声明了 API_SECRET 忘记加 VITE_ 前缀 interface ImportMetaEnv { readonly API_SECRET: string; // ❌ 永远拿不到值 }因此在 zod schema 中可以通过startsWith校验来提前发现这类问题在 CI 或 pre-commit 阶段自动扫描.env.*文件中未加前缀的变量// pre-commit 钩子扫描 const envContent fs.readFileSync(.env.production, utf-8); const nonViteKeys envContent .split(\n) .filter(line line.includes() !line.startsWith(#)) .map(line line.split()[0].trim()) .filter(key !key.startsWith(VITE_) key.length 0); if (nonViteKeys.length 0) { console.error(以下环境变量缺少 VITE_ 前缀: ${nonViteKeys.join(, )}); process.exit(1); }环境特定配置文件按环境拆分// config/environments/development.ts export const developmentConfig { api: { baseURL: http://localhost:3000/api, mockDelay: 500 // 模拟网络延迟 }, features: { enableDebugPanel: true, enableHotReload: true } }; // config/environments/production.ts export const productionConfig { api: { baseURL: https://api.example.com, mockDelay: 0 }, features: { enableDebugPanel: false, enableHotReload: false }, performance: { enableServiceWorker: true, enablePreload: true } }; // config/environments/index.ts - 根据环境导出配置 const environment import.meta.env.MODE || development; const configs { development: () import(./development), production: () import(./production), test: () import(./test), staging: () import(./staging) }; export default configs[environment]?.() || configs.development();四、多环境构建的工程化最佳实践1. 环境配置验证在构建前验证环境变量避免将错误配置部署到生产环境。// scripts/validate-env.ts - 构建前验证脚本 import { validateEnv } from ../src/config/envValidator; try { const config validateEnv(); console.log(✅ Environment validation passed); console.log( App Title:, config.VITE_APP_TITLE); console.log( API URL:, config.VITE_API_BASE_URL); process.exit(0); } catch (error) { console.error(❌ Environment validation failed); process.exit(1); }2. 构建产物差异分析对比不同环境的构建产物确保配置生效。// scripts/analyze-bundle.ts - 构建产物分析 import { execSync } from child_process; import fs from fs; import path from path; interface BundleAnalysis { environment: string; totalSize: number; gzippedSize: number; chunkCount: number; chunks: Array{ name: string; size: number; }; } function analyzeBundle(env: string): BundleAnalysis { // 构建指定环境 execSync(npm run build -- --mode ${env}, { stdio: inherit }); // 分析 dist 目录 const distPath path.resolve(__dirname, ../dist); const files fs.readdirSync(distPath); const chunks: BundleAnalysis[chunks] []; let totalSize 0; for (const file of files) { const filePath path.join(distPath, file); const stats fs.statSync(filePath); if (file.endsWith(.js) || file.endsWith(.css)) { chunks.push({ name: file, size: stats.size }); totalSize stats.size; } } return { environment: env, totalSize, gzippedSize: totalSize * 0.3, // 粗略估算 chunkCount: chunks.length, chunks: chunks.sort((a, b) b.size - a.size) }; } // 对比不同环境的构建产物 const environments [development, production, staging]; const analyses environments.map(env analyzeBundle(env)); console.table(analyses.map(a ({ Environment: a.environment, Total Size (KB): (a.totalSize / 1024).toFixed(2), Gzipped (KB): (a.gzippedSize / 1024).toFixed(2), Chunk Count: a.chunkCount })));3. CI/CD 集成在 CI/CD 流程中自动验证和构建多环境配置。# .github/workflows/build.yml - GitHub Actions 配置 name: Build and Deploy on: push: branches: [main, develop] pull_request: branches: [main] jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-nodev3 with: node-version: 18 - name: Install dependencies run: npm ci - name: Validate environment config run: npm run validate:env env: VITE_API_BASE_URL: ${{ secrets.API_BASE_URL }} build: needs: validate runs-on: ubuntu-latest strategy: matrix: environment: [development, staging, production] steps: - uses: actions/checkoutv3 - uses: actions/setup-nodev3 with: node-version: 18 - name: Install dependencies run: npm ci - name: Build for ${{ matrix.environment }} run: npm run build -- --mode ${{ matrix.environment }} env: VITE_API_BASE_URL: ${{ secrets[format(API_URL_{0}, matrix.environment)] }} - name: Upload build artifacts uses: actions/upload-artifactv3 with: name: dist-${{ matrix.environment }} path: dist/4. 运行时配置注入对于需要在部署后动态修改的配置可以使用运行时配置注入。场景描述有一次线上需要紧急切换 API 地址上游服务挂了但代码中用import.meta.env.VITE_API_URL硬读了构建时的变量值任何修改都需要重新npm run build并走完整的部署流程。从发现问题到流量切走花了 40 分钟而如果用运行时配置注入只需要修改部署脚本中的模板变量Nginx reload 即可生效全程不超过 2 分钟。这就是运行时配置注入的价值把需要重新构建才能改变的配置变成部署后可动态替换的配置。!-- index.html - 运行时配置注入 -- !DOCTYPE html html langen head meta charsetUTF-8 / link relicon typeimage/svgxml href/vite.svg / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleMy App/title !-- 运行时配置由部署脚本注入 -- script window.__RUNTIME_CONFIG__ { API_BASE_URL: {{API_BASE_URL}}, // 部署时替换 ENABLE_MOCK: {{ENABLE_MOCK}}, // 部署时替换 SENTRY_DSN: {{SENTRY_DSN}} // 部署时替换 }; /script /head body div idroot/div script typemodule src/src/main.tsx/script /body /html // src/config/runtimeConfig.ts - 读取运行时配置 export function getRuntimeConfig() { const runtimeConfig (window as any).__RUNTIME_CONFIG__; if (!runtimeConfig) { console.warn(Runtime config not found, using build-time config); return null; } return { apiBaseURL: runtimeConfig.API_BASE_URL, enableMock: runtimeConfig.ENABLE_MOCK, sentryDSN: runtimeConfig.SENTRY_DSN }; }五、总结Vite 的多环境配置系统通过.env文件、条件配置、运行时注入三层机制能够灵活支持复杂的多环境构建需求。关键实践包括类型安全的配置验证、模块化的配置组织、CI/CD 自动化、构建产物分析。这些手段能够确保配置的正确性并降低环境差异导致的问题。对于大型项目建议建立统一的配置中心将环境变量、构建配置、运行时配置纳入统一管理并通过可视化工具监控各环境的配置差异。);