Appearance
多环境配置
课程概述
本章节深入探讨前端项目的多环境配置策略,学习如何管理开发、测试、预发布、生产等多个环境的配置。掌握多环境配置能够提高项目的灵活性和可维护性。
学习目标
- 理解多环境配置的必要性
- 掌握环境配置文件的组织方式
- 学习动态环境切换技巧
- 了解环境特定的构建策略
- 掌握CI/CD中的环境管理
- 学习配置的安全和最佳实践
第一部分:多环境概述
1.1 常见环境类型
javascript
1. Local (本地) - 开发人员本地环境
2. Development (开发) - 开发服务器环境
3. Test/QA (测试) - 测试环境
4. Staging (预发布) - 预生产环境
5. Production (生产) - 正式生产环境
6. DR (灾备) - 灾难恢复环境1.2 环境特征对比
| 环境 | API端点 | 数据库 | 日志级别 | 监控 | 缓存 |
|---|---|---|---|---|---|
| Local | localhost | 本地 | debug | 否 | 禁用 |
| Development | dev-api | 开发库 | debug | 基础 | 禁用 |
| Test | test-api | 测试库 | info | 基础 | 启用 |
| Staging | staging-api | 预发布库 | warn | 完整 | 启用 |
| Production | api | 生产库 | 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/static2.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.com2.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.com2.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:
- main5.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 valuesbash
# .gitignore
.env.local
.env.*.local
.env.development.local
.env.test.local
.env.staging.local
.env.production.local7.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',
}总结
本章全面介绍了多环境配置:
- 环境概述 - 理解多环境的必要性和特征
- 配置文件 - 组织和管理环境配置
- 配置系统 - 实现类型安全的配置管理
- 构建配置 - 多环境构建策略
- CI/CD - 持续集成和部署中的环境管理
- 调试工具 - 环境切换和信息面板
- 安全实践 - 敏感信息管理和验证
掌握多环境配置是构建专业级应用的关键能力。