Skip to content

多环境配置

课程概述

本章节深入探讨前端项目的多环境配置策略,学习如何管理开发、测试、预发布、生产等多个环境的配置。掌握多环境配置能够提高项目的灵活性和可维护性。

学习目标

  • 理解多环境配置的必要性
  • 掌握环境配置文件的组织方式
  • 学习动态环境切换技巧
  • 了解环境特定的构建策略
  • 掌握CI/CD中的环境管理
  • 学习配置的安全和最佳实践

第一部分:多环境概述

1.1 常见环境类型

javascript
1. Local (本地)         - 开发人员本地环境
2. Development (开发)   - 开发服务器环境
3. Test/QA (测试)      - 测试环境
4. Staging (预发布)     - 预生产环境
5. Production (生产)    - 正式生产环境
6. DR (灾备)           - 灾难恢复环境

1.2 环境特征对比

环境API端点数据库日志级别监控缓存
Locallocalhost本地debug禁用
Developmentdev-api开发库debug基础禁用
Testtest-api测试库info基础启用
Stagingstaging-api预发布库warn完整启用
Productionapi生产库error完整启用

1.3 为什么需要多环境

javascript
1. 隔离性 - 不同环境互不影响
2. 测试性 - 在接近生产的环境中测试
3. 安全性 - 保护生产环境数据
4. 灵活性 - 针对不同环境优化配置
5. 可追溯 - 问题定位和回滚

第二部分:环境配置文件

2.1 基础配置结构

bash
# .env 文件结构
.env                    # 默认配置(所有环境)
.env.local             # 本地覆盖(不提交)
.env.development       # 开发环境
.env.test              # 测试环境
.env.staging           # 预发布环境
.env.production        # 生产环境
.env.example           # 示例文件(提交)

2.2 开发环境配置

bash
# .env.development
# 应用配置
VITE_APP_NAME=MyApp Dev
VITE_APP_VERSION=1.0.0-dev

# API配置
VITE_API_BASE_URL=http://localhost:8080/api
VITE_API_TIMEOUT=30000
VITE_WS_URL=ws://localhost:8080/ws

# 功能开关
VITE_ENABLE_MOCK=true
VITE_ENABLE_DEBUG=true
VITE_ENABLE_ANALYTICS=false

# 第三方服务(测试密钥)
VITE_STRIPE_KEY=pk_test_xxx
VITE_GA_ID=

# 其他配置
VITE_LOG_LEVEL=debug
VITE_CDN_URL=http://localhost:3000/static

2.3 测试环境配置

bash
# .env.test
# 应用配置
VITE_APP_NAME=MyApp Test
VITE_APP_VERSION=1.0.0-test

# API配置
VITE_API_BASE_URL=https://test-api.example.com/api
VITE_API_TIMEOUT=15000
VITE_WS_URL=wss://test-api.example.com/ws

# 功能开关
VITE_ENABLE_MOCK=false
VITE_ENABLE_DEBUG=true
VITE_ENABLE_ANALYTICS=false

# 第三方服务
VITE_STRIPE_KEY=pk_test_xxx
VITE_GA_ID=UA-test-xxx

# 其他配置
VITE_LOG_LEVEL=info
VITE_CDN_URL=https://test-cdn.example.com

2.4 预发布环境配置

bash
# .env.staging
# 应用配置
VITE_APP_NAME=MyApp Staging
VITE_APP_VERSION=1.0.0-staging

# API配置
VITE_API_BASE_URL=https://staging-api.example.com/api
VITE_API_TIMEOUT=10000
VITE_WS_URL=wss://staging-api.example.com/ws

# 功能开关
VITE_ENABLE_MOCK=false
VITE_ENABLE_DEBUG=false
VITE_ENABLE_ANALYTICS=true

# 第三方服务
VITE_STRIPE_KEY=pk_test_xxx
VITE_GA_ID=UA-staging-xxx

# 其他配置
VITE_LOG_LEVEL=warn
VITE_CDN_URL=https://staging-cdn.example.com

2.5 生产环境配置

bash
# .env.production
# 应用配置
VITE_APP_NAME=MyApp
VITE_APP_VERSION=1.0.0

# API配置
VITE_API_BASE_URL=https://api.example.com/api
VITE_API_TIMEOUT=10000
VITE_WS_URL=wss://api.example.com/ws

# 功能开关
VITE_ENABLE_MOCK=false
VITE_ENABLE_DEBUG=false
VITE_ENABLE_ANALYTICS=true

# 第三方服务(生产密钥)
VITE_STRIPE_KEY=pk_live_xxx
VITE_GA_ID=UA-prod-xxx

# 其他配置
VITE_LOG_LEVEL=error
VITE_CDN_URL=https://cdn.example.com

第三部分:配置管理系统

3.1 类型定义

typescript
// src/config/types.ts
export type Environment = 'local' | 'development' | 'test' | 'staging' | 'production'

export interface ApiConfig {
  baseUrl: string
  timeout: number
  wsUrl: string
  retryAttempts: number
  retryDelay: number
}

export interface FeatureFlags {
  mock: boolean
  debug: boolean
  analytics: boolean
  betaFeatures: boolean
}

export interface ExternalServices {
  stripe: {
    publicKey: string
  }
  analytics: {
    gaId?: string
    sentryDsn?: string
  }
  cdn: {
    url: string
  }
}

export interface AppConfig {
  env: Environment
  app: {
    name: string
    version: string
  }
  api: ApiConfig
  features: FeatureFlags
  external: ExternalServices
  logging: {
    level: 'debug' | 'info' | 'warn' | 'error'
    remote: boolean
  }
}

3.2 环境配置实现

typescript
// src/config/environments/development.ts
import { AppConfig } from '../types'

export const development: AppConfig = {
  env: 'development',
  
  app: {
    name: import.meta.env.VITE_APP_NAME || 'MyApp Dev',
    version: import.meta.env.VITE_APP_VERSION || '1.0.0-dev',
  },
  
  api: {
    baseUrl: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api',
    timeout: 30000,
    wsUrl: import.meta.env.VITE_WS_URL || 'ws://localhost:8080/ws',
    retryAttempts: 1,
    retryDelay: 1000,
  },
  
  features: {
    mock: true,
    debug: true,
    analytics: false,
    betaFeatures: true,
  },
  
  external: {
    stripe: {
      publicKey: import.meta.env.VITE_STRIPE_KEY || '',
    },
    analytics: {
      gaId: import.meta.env.VITE_GA_ID,
      sentryDsn: import.meta.env.VITE_SENTRY_DSN,
    },
    cdn: {
      url: 'http://localhost:3000/static',
    },
  },
  
  logging: {
    level: 'debug',
    remote: false,
  },
}
typescript
// src/config/environments/production.ts
import { AppConfig } from '../types'

export const production: AppConfig = {
  env: 'production',
  
  app: {
    name: import.meta.env.VITE_APP_NAME || 'MyApp',
    version: import.meta.env.VITE_APP_VERSION || '1.0.0',
  },
  
  api: {
    baseUrl: import.meta.env.VITE_API_BASE_URL || 'https://api.example.com/api',
    timeout: 10000,
    wsUrl: import.meta.env.VITE_WS_URL || 'wss://api.example.com/ws',
    retryAttempts: 3,
    retryDelay: 1000,
  },
  
  features: {
    mock: false,
    debug: false,
    analytics: true,
    betaFeatures: false,
  },
  
  external: {
    stripe: {
      publicKey: import.meta.env.VITE_STRIPE_KEY || '',
    },
    analytics: {
      gaId: import.meta.env.VITE_GA_ID,
      sentryDsn: import.meta.env.VITE_SENTRY_DSN,
    },
    cdn: {
      url: 'https://cdn.example.com',
    },
  },
  
  logging: {
    level: 'error',
    remote: true,
  },
}

3.3 配置加载器

typescript
// src/config/index.ts
import { z } from 'zod'
import { AppConfig, Environment } from './types'
import { development } from './environments/development'
import { test } from './environments/test'
import { staging } from './environments/staging'
import { production } from './environments/production'

// 配置验证 Schema
const configSchema = z.object({
  env: z.enum(['local', 'development', 'test', 'staging', 'production']),
  app: z.object({
    name: z.string().min(1),
    version: z.string().min(1),
  }),
  api: z.object({
    baseUrl: z.string().url(),
    timeout: z.number().positive(),
    wsUrl: z.string(),
    retryAttempts: z.number().min(0),
    retryDelay: z.number().positive(),
  }),
  features: z.object({
    mock: z.boolean(),
    debug: z.boolean(),
    analytics: z.boolean(),
    betaFeatures: z.boolean(),
  }),
  external: z.object({
    stripe: z.object({
      publicKey: z.string(),
    }),
    analytics: z.object({
      gaId: z.string().optional(),
      sentryDsn: z.string().optional(),
    }),
    cdn: z.object({
      url: z.string().url(),
    }),
  }),
  logging: z.object({
    level: z.enum(['debug', 'info', 'warn', 'error']),
    remote: z.boolean(),
  }),
})

// 环境配置映射
const configs: Record<Environment, AppConfig> = {
  local: development,
  development,
  test,
  staging,
  production,
}

// 获取当前环境
function getCurrentEnvironment(): Environment {
  const mode = import.meta.env.MODE as Environment
  return mode || 'development'
}

// 加载并验证配置
function loadConfig(): AppConfig {
  const env = getCurrentEnvironment()
  const rawConfig = configs[env]
  
  try {
    return configSchema.parse(rawConfig)
  } catch (error) {
    console.error('Configuration validation failed:', error)
    console.error('Using development config as fallback')
    return development
  }
}

export const config = loadConfig()

// 导出环境检测工具
export const env = {
  current: config.env,
  isLocal: config.env === 'local',
  isDev: config.env === 'development',
  isTest: config.env === 'test',
  isStaging: config.env === 'staging',
  isProd: config.env === 'production',
}

// 调试输出
if (config.features.debug) {
  console.group('🔧 App Configuration')
  console.log('Environment:', config.env)
  console.log('API Base URL:', config.api.baseUrl)
  console.log('Features:', config.features)
  console.groupEnd()
}

3.4 配置使用示例

typescript
// src/services/api.ts
import axios from 'axios'
import { config } from '@/config'

const api = axios.create({
  baseURL: config.api.baseUrl,
  timeout: config.api.timeout,
})

// 请求拦截器
api.interceptors.request.use(
  (config) => {
    // 开发环境日志
    if (env.isDev || env.isTest) {
      console.log('API Request:', config)
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
api.interceptors.response.use(
  (response) => {
    return response
  },
  async (error) => {
    const { retryAttempts, retryDelay } = config.api
    
    // 生产环境自动重试
    if (env.isProd && error.config && !error.config.__retryCount) {
      error.config.__retryCount = 0
      
      while (error.config.__retryCount < retryAttempts) {
        error.config.__retryCount++
        
        await new Promise(resolve => setTimeout(resolve, retryDelay))
        
        try {
          return await api.request(error.config)
        } catch (err) {
          error = err
        }
      }
    }
    
    return Promise.reject(error)
  }
)

export default api

第四部分:构建配置

4.1 Vite 多环境构建

typescript
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { resolve } from 'path'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')
  const isDev = mode === 'development'
  const isProd = mode === 'production'
  
  return {
    plugins: [react()],
    
    resolve: {
      alias: {
        '@': resolve(__dirname, 'src'),
      },
    },
    
    server: {
      port: parseInt(env.VITE_PORT) || 3000,
      proxy: {
        '/api': {
          target: env.VITE_API_BASE_URL,
          changeOrigin: true,
        },
      },
    },
    
    build: {
      outDir: `dist-${mode}`,
      sourcemap: mode === 'staging' || isDev,
      
      rollupOptions: {
        output: {
          manualChunks: isProd ? {
            'react-vendor': ['react', 'react-dom'],
            'router': ['react-router-dom'],
          } : undefined,
        },
      },
      
      minify: isProd ? 'terser' : false,
      terserOptions: isProd ? {
        compress: {
          drop_console: true,
          drop_debugger: true,
        },
      } : undefined,
    },
  }
})

4.2 构建脚本

json
// package.json
{
  "scripts": {
    "dev": "vite --mode development",
    "dev:test": "vite --mode test",
    "dev:staging": "vite --mode staging",
    
    "build": "vite build --mode production",
    "build:dev": "vite build --mode development",
    "build:test": "vite build --mode test",
    "build:staging": "vite build --mode staging",
    "build:all": "npm run build:dev && npm run build:test && npm run build:staging && npm run build",
    
    "preview": "vite preview",
    "preview:test": "vite preview --mode test",
    "preview:staging": "vite preview --mode staging",
  }
}

4.3 环境特定构建脚本

typescript
// scripts/build.ts
import { build } from 'vite'
import { resolve } from 'path'
import fs from 'fs-extra'

interface BuildOptions {
  mode: string
  outDir: string
}

async function buildForEnvironment(options: BuildOptions) {
  const { mode, outDir } = options
  
  console.log(`🏗️  Building for ${mode}...`)
  
  // 清空输出目录
  await fs.emptyDir(outDir)
  
  // 构建
  await build({
    mode,
    root: resolve(__dirname, '..'),
    build: {
      outDir,
      emptyOutDir: false,
    },
  })
  
  // 复制环境特定文件
  await fs.copy(
    resolve(__dirname, `../public/${mode}`),
    resolve(__dirname, `../${outDir}`),
    { overwrite: true }
  )
  
  console.log(`✅ Build complete for ${mode}`)
}

// 批量构建
async function buildAll() {
  const environments = [
    { mode: 'development', outDir: 'dist-dev' },
    { mode: 'test', outDir: 'dist-test' },
    { mode: 'staging', outDir: 'dist-staging' },
    { mode: 'production', outDir: 'dist' },
  ]
  
  for (const env of environments) {
    await buildForEnvironment(env)
  }
}

buildAll().catch(console.error)

第五部分:CI/CD 集成

5.1 GitHub Actions

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches:
      - main        # 生产环境
      - staging     # 预发布环境
      - develop     # 开发环境
  pull_request:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Determine environment
        id: env
        run: |
          if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
            echo "ENV=production" >> $GITHUB_OUTPUT
          elif [[ "${{ github.ref }}" == "refs/heads/staging" ]]; then
            echo "ENV=staging" >> $GITHUB_OUTPUT
          else
            echo "ENV=development" >> $GITHUB_OUTPUT
          fi
      
      - name: Build
        run: npm run build:${{ steps.env.outputs.ENV }}
        env:
          VITE_API_BASE_URL: ${{ secrets[format('{0}_API_URL', steps.env.outputs.ENV)] }}
          VITE_STRIPE_KEY: ${{ secrets[format('{0}_STRIPE_KEY', steps.env.outputs.ENV)] }}
      
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-args: '--prod --env=${{ steps.env.outputs.ENV }}'
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}

5.2 GitLab CI

yaml
# .gitlab-ci.yml
stages:
  - build
  - deploy

variables:
  NODE_VERSION: "20"

.build_template: &build_template
  stage: build
  image: node:${NODE_VERSION}
  cache:
    paths:
      - node_modules/
  before_script:
    - npm ci
  artifacts:
    paths:
      - dist-${CI_ENVIRONMENT_NAME}/
    expire_in: 1 day

build:development:
  <<: *build_template
  script:
    - npm run build:dev
  environment:
    name: development
  only:
    - develop

build:staging:
  <<: *build_template
  script:
    - npm run build:staging
  environment:
    name: staging
  only:
    - staging

build:production:
  <<: *build_template
  script:
    - npm run build
  environment:
    name: production
  only:
    - main

deploy:production:
  stage: deploy
  script:
    - npm run deploy
  environment:
    name: production
    url: https://example.com
  only:
    - main

5.3 环境变量管理

使用 Vercel:

bash
# 安装 Vercel CLI
npm i -g vercel

# 添加环境变量
vercel env add VITE_API_URL production
vercel env add VITE_API_URL staging
vercel env add VITE_API_URL development

# 拉取环境变量
vercel env pull

使用 Netlify:

toml
# netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

# Development
[context.develop]
  command = "npm run build:dev"
  
  [context.develop.environment]
    VITE_API_URL = "https://dev-api.example.com"
    VITE_ENV = "development"

# Staging
[context.staging]
  command = "npm run build:staging"
  
  [context.staging.environment]
    VITE_API_URL = "https://staging-api.example.com"
    VITE_ENV = "staging"

# Production
[context.production]
  command = "npm run build"
  
  [context.production.environment]
    VITE_API_URL = "https://api.example.com"
    VITE_ENV = "production"

第六部分:环境切换和调试

6.1 环境切换工具

typescript
// src/utils/env-switcher.ts
export class EnvironmentSwitcher {
  private static readonly STORAGE_KEY = '__app_environment__'
  
  static getEnvironments() {
    return ['development', 'test', 'staging', 'production'] as const
  }
  
  static getCurrentEnvironment() {
    if (typeof window === 'undefined') return null
    return localStorage.getItem(this.STORAGE_KEY) || import.meta.env.MODE
  }
  
  static switchEnvironment(env: string) {
    if (typeof window === 'undefined') return
    
    localStorage.setItem(this.STORAGE_KEY, env)
    window.location.reload()
  }
  
  static isEnabled() {
    // 仅在开发环境启用
    return import.meta.env.DEV
  }
}
typescript
// src/components/EnvSwitcher.tsx
import { useState } from 'react'
import { EnvironmentSwitcher } from '@/utils/env-switcher'

export function EnvSwitcher() {
  const [currentEnv, setCurrentEnv] = useState(
    EnvironmentSwitcher.getCurrentEnvironment()
  )
  
  if (!EnvironmentSwitcher.isEnabled()) {
    return null
  }
  
  const handleSwitch = (env: string) => {
    EnvironmentSwitcher.switchEnvironment(env)
    setCurrentEnv(env)
  }
  
  return (
    <div className="env-switcher">
      <select value={currentEnv || ''} onChange={(e) => handleSwitch(e.target.value)}>
        {EnvironmentSwitcher.getEnvironments().map((env) => (
          <option key={env} value={env}>
            {env}
          </option>
        ))}
      </select>
    </div>
  )
}

6.2 环境信息面板

typescript
// src/components/EnvInfoPanel.tsx
import { config, env } from '@/config'

export function EnvInfoPanel() {
  if (!env.isDev && !env.isStaging) {
    return null
  }
  
  return (
    <div className="env-info-panel">
      <h3>Environment Info</h3>
      <table>
        <tbody>
          <tr>
            <td>Environment:</td>
            <td>{config.env}</td>
          </tr>
          <tr>
            <td>API URL:</td>
            <td>{config.api.baseUrl}</td>
          </tr>
          <tr>
            <td>Version:</td>
            <td>{config.app.version}</td>
          </tr>
          <tr>
            <td>Debug Mode:</td>
            <td>{config.features.debug ? 'Yes' : 'No'}</td>
          </tr>
        </tbody>
      </table>
    </div>
  )
}

第七部分:安全和最佳实践

7.1 敏感信息管理

bash
# .env.example (提交到 git)
# API Configuration
VITE_API_BASE_URL=
VITE_API_TIMEOUT=

# Feature Flags
VITE_ENABLE_ANALYTICS=
VITE_ENABLE_DEBUG=

# External Services (Public Keys Only)
VITE_STRIPE_KEY=
VITE_GA_ID=

# DO NOT COMMIT ACTUAL VALUES!
# Copy this file to .env.local and fill in values
bash
# .gitignore
.env.local
.env.*.local
.env.development.local
.env.test.local
.env.staging.local
.env.production.local

7.2 配置验证

typescript
// src/config/validation.ts
import { z } from 'zod'

const envSchema = z.object({
  VITE_API_BASE_URL: z.string().url(),
  VITE_API_TIMEOUT: z.string().transform(Number).pipe(z.number().positive()),
  VITE_STRIPE_KEY: z.string().min(1),
})

export function validateEnvironment() {
  try {
    return envSchema.parse(import.meta.env)
  } catch (error) {
    console.error('Environment validation failed:', error)
    throw new Error('Invalid environment configuration')
  }
}

7.3 运行时配置

typescript
// src/config/runtime.ts
export async function loadRuntimeConfig() {
  try {
    const response = await fetch('/config.json')
    const runtimeConfig = await response.json()
    
    return {
      ...config,
      ...runtimeConfig,
    }
  } catch (error) {
    console.warn('Failed to load runtime config, using build-time config')
    return config
  }
}
json
// public/config.json
{
  "api": {
    "baseUrl": "https://api.example.com"
  },
  "features": {
    "betaFeatures": false
  }
}

第八部分:完整示例

8.1 完整配置系统

typescript
// src/config/index.ts
import { z } from 'zod'

// 类型定义
export type Environment = 'local' | 'development' | 'test' | 'staging' | 'production'

// Schema 定义
const configSchema = z.object({
  env: z.enum(['local', 'development', 'test', 'staging', 'production']),
  app: z.object({
    name: z.string(),
    version: z.string(),
  }),
  api: z.object({
    baseUrl: z.string().url(),
    timeout: z.number(),
  }),
  features: z.object({
    analytics: z.boolean(),
    debug: z.boolean(),
  }),
})

// 环境配置
const configs = {
  development: {
    env: 'development' as const,
    app: {
      name: 'MyApp Dev',
      version: '1.0.0-dev',
    },
    api: {
      baseUrl: 'http://localhost:8080',
      timeout: 30000,
    },
    features: {
      analytics: false,
      debug: true,
    },
  },
  production: {
    env: 'production' as const,
    app: {
      name: 'MyApp',
      version: '1.0.0',
    },
    api: {
      baseUrl: 'https://api.example.com',
      timeout: 10000,
    },
    features: {
      analytics: true,
      debug: false,
    },
  },
}

// 加载配置
const mode = import.meta.env.MODE as keyof typeof configs
export const config = configSchema.parse(configs[mode] || configs.development)

// 环境工具
export const env = {
  current: config.env,
  isDev: config.env === 'development',
  isProd: config.env === 'production',
}

总结

本章全面介绍了多环境配置:

  1. 环境概述 - 理解多环境的必要性和特征
  2. 配置文件 - 组织和管理环境配置
  3. 配置系统 - 实现类型安全的配置管理
  4. 构建配置 - 多环境构建策略
  5. CI/CD - 持续集成和部署中的环境管理
  6. 调试工具 - 环境切换和信息面板
  7. 安全实践 - 敏感信息管理和验证

掌握多环境配置是构建专业级应用的关键能力。

扩展阅读