Skip to content

环境变量管理

课程概述

本章节深入探讨前端项目中的环境变量管理,学习如何在不同环境(开发、测试、生产)中使用和管理配置信息。合理的环境变量管理能够提高项目的可维护性和安全性。

学习目标

  • 理解环境变量的概念和作用
  • 掌握不同构建工具中的环境变量配置
  • 学习环境变量的最佳实践
  • 了解敏感信息的安全管理
  • 掌握多环境配置策略
  • 学习环境变量的类型安全

第一部分:环境变量基础

1.1 什么是环境变量

环境变量是在应用程序运行时可以访问的键值对配置,用于存储不同环境下的配置信息。

作用:

javascript
1. 区分不同环境(开发、测试、生产)
2. 存储 API 端点、密钥等配置
3. 控制功能开关
4. 配置第三方服务
5. 管理构建选项

常见用途:

javascript
// API 端点
API_URL=https://api.example.com

// 功能开关
ENABLE_ANALYTICS=true
ENABLE_DEBUG=false

// 第三方服务
STRIPE_API_KEY=pk_live_xxx
GOOGLE_ANALYTICS_ID=UA-xxx

// 构建配置
PUBLIC_PATH=/app/

1.2 环境变量的类型

1. 构建时环境变量:

javascript
// 在构建过程中被替换
const apiUrl = process.env.REACT_APP_API_URL

// 构建后代码
const apiUrl = "https://api.example.com"

2. 运行时环境变量:

javascript
// 服务器端环境变量
const dbUrl = process.env.DATABASE_URL  // Node.js

// 客户端无法访问

3. 公开环境变量:

javascript
// 打包到客户端代码中
VITE_API_URL=https://api.example.com
REACT_APP_VERSION=1.0.0

4. 私密环境变量:

javascript
// 仅在服务器端使用
DATABASE_URL=postgresql://...
SECRET_KEY=xxx
API_SECRET=yyy

1.3 安全注意事项

javascript
// ❌ 错误:暴露敏感信息
VITE_SECRET_KEY=abc123
REACT_APP_API_SECRET=secret123

// ✅ 正确:仅暴露公开信息
VITE_API_URL=https://api.example.com
REACT_APP_APP_NAME=MyApp

// ✅ 私密信息仅在服务器端
DATABASE_URL=postgresql://...  // 不要用 VITE_ 或 REACT_APP_ 前缀

第二部分:Vite 环境变量

2.1 环境变量文件

Vite 使用 dotenv 加载环境变量:

bash
.env                # 所有环境
.env.local          # 所有环境,git 忽略
.env.development    # 开发环境
.env.development.local
.env.production     # 生产环境
.env.production.local
.env.test           # 测试环境
.env.test.local

加载优先级:

.env.[mode].local > .env.[mode] > .env.local > .env

2.2 定义环境变量

bash
# .env
VITE_APP_TITLE=My App
VITE_API_BASE_URL=https://api.example.com

# .env.development
VITE_API_BASE_URL=http://localhost:3001
VITE_ENABLE_MOCK=true
VITE_LOG_LEVEL=debug

# .env.production
VITE_API_BASE_URL=https://api.production.com
VITE_ENABLE_MOCK=false
VITE_LOG_LEVEL=error

# .env.staging
VITE_API_BASE_URL=https://api.staging.com
VITE_ENABLE_MOCK=false
VITE_LOG_LEVEL=warn

注意事项:

bash
# ✅ 会暴露给客户端(以 VITE_ 开头)
VITE_APP_NAME=MyApp

# ❌ 不会暴露给客户端
APP_NAME=MyApp
SECRET_KEY=xxx

# ✅ 特殊变量
MODE=development      # Vite 内置
BASE_URL=/           # Vite 内置
PROD=false           # Vite 内置
DEV=true             # Vite 内置

2.3 使用环境变量

typescript
// 在代码中访问
const apiUrl = import.meta.env.VITE_API_BASE_URL
const appTitle = import.meta.env.VITE_APP_TITLE
const mode = import.meta.env.MODE
const isDev = import.meta.env.DEV
const isProd = import.meta.env.PROD

console.log('API URL:', apiUrl)
console.log('App Title:', appTitle)
console.log('Mode:', mode)
console.log('Is Dev:', isDev)

内置环境变量:

typescript
import.meta.env.MODE          // 应用运行模式
import.meta.env.BASE_URL      // 部署基础路径
import.meta.env.PROD          // 是否生产环境
import.meta.env.DEV           // 是否开发环境
import.meta.env.SSR           // 是否服务端渲染

2.4 TypeScript 类型定义

typescript
// src/vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  readonly VITE_API_BASE_URL: string
  readonly VITE_ENABLE_MOCK: string
  readonly VITE_LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error'
  readonly VITE_STRIPE_KEY: string
  readonly VITE_GA_ID: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

2.5 配置文件中使用

typescript
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {
  // 加载环境变量
  const env = loadEnv(mode, process.cwd(), '')
  
  return {
    // 使用环境变量
    base: env.VITE_BASE_URL || '/',
    
    server: {
      port: parseInt(env.VITE_PORT) || 3000,
      proxy: {
        '/api': {
          target: env.VITE_API_BASE_URL,
          changeOrigin: true
        }
      }
    },
    
    define: {
      __APP_VERSION__: JSON.stringify(env.npm_package_version)
    }
  }
})

2.6 自定义环境变量前缀

typescript
// vite.config.ts
export default defineConfig({
  envPrefix: ['VITE_', 'APP_'],  // 默认只有 VITE_
})
bash
# .env
VITE_API_URL=xxx    # 可访问
APP_NAME=xxx        # 可访问
OTHER_VAR=xxx       # 不可访问

第三部分:Create React App 环境变量

3.1 CRA 环境变量规则

bash
# .env
REACT_APP_API_URL=https://api.example.com
REACT_APP_APP_NAME=MyApp

# .env.development
REACT_APP_API_URL=http://localhost:3001

# .env.production
REACT_APP_API_URL=https://api.production.com

访问环境变量:

javascript
const apiUrl = process.env.REACT_APP_API_URL
const appName = process.env.REACT_APP_APP_NAME
const nodeEnv = process.env.NODE_ENV

console.log(apiUrl, appName, nodeEnv)

3.2 内置环境变量

javascript
process.env.NODE_ENV        // 'development' | 'production' | 'test'
process.env.PUBLIC_URL      // public 目录的 URL

3.3 扩展环境变量

bash
npm install --save-dev dotenv-expand
bash
# .env
REACT_APP_BASE_URL=https://api.example.com
REACT_APP_API_URL=${REACT_APP_BASE_URL}/v1
REACT_APP_WEBSOCKET_URL=wss://${REACT_APP_BASE_URL}/ws

第四部分:Next.js 环境变量

4.1 环境变量类型

1. 浏览器环境变量(公开):

bash
# .env
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_GA_ID=UA-xxxxx
typescript
// 客户端和服务端都可访问
const apiUrl = process.env.NEXT_PUBLIC_API_URL

2. 服务器环境变量(私密):

bash
# .env
DATABASE_URL=postgresql://...
SECRET_KEY=xxx
API_SECRET=yyy
typescript
// 仅服务端可访问
const dbUrl = process.env.DATABASE_URL

4.2 环境变量文件

bash
.env                  # 所有环境
.env.local           # 所有环境,git 忽略
.env.development     # 开发环境
.env.development.local
.env.production      # 生产环境
.env.production.local
.env.test            # 测试环境(jest)
.env.test.local

4.3 使用示例

typescript
// pages/api/data.ts - 服务端 API
export default async function handler(req, res) {
  // 可以访问所有环境变量
  const dbUrl = process.env.DATABASE_URL
  const apiKey = process.env.API_SECRET
  
  // ...
}

// components/App.tsx - 客户端组件
export function App() {
  // 只能访问 NEXT_PUBLIC_ 开头的变量
  const apiUrl = process.env.NEXT_PUBLIC_API_URL
  
  return <div>API: {apiUrl}</div>
}

4.4 运行时配置

typescript
// next.config.js
module.exports = {
  env: {
    customKey: 'customValue',
  },
  
  // 公开运行时配置
  publicRuntimeConfig: {
    apiUrl: process.env.API_URL,
  },
  
  // 服务端运行时配置
  serverRuntimeConfig: {
    secretKey: process.env.SECRET_KEY,
  },
}
typescript
// 使用运行时配置
import getConfig from 'next/config'

const { publicRuntimeConfig, serverRuntimeConfig } = getConfig()

console.log(publicRuntimeConfig.apiUrl)

第五部分:Webpack 环境变量

5.1 DefinePlugin

javascript
// webpack.config.js
const webpack = require('webpack')

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'process.env.API_URL': JSON.stringify(process.env.API_URL),
      '__DEV__': JSON.stringify(true),
      '__VERSION__': JSON.stringify('1.0.0'),
    })
  ]
}

5.2 EnvironmentPlugin

javascript
// webpack.config.js
const webpack = require('webpack')

module.exports = {
  plugins: [
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'development',
      API_URL: 'http://localhost:3001',
      DEBUG: false
    })
  ]
}

等同于:

javascript
new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
  'process.env.API_URL': JSON.stringify(process.env.API_URL || 'http://localhost:3001'),
  'process.env.DEBUG': JSON.stringify(process.env.DEBUG || false),
})

5.3 dotenv-webpack

bash
npm install -D dotenv-webpack
javascript
// webpack.config.js
const Dotenv = require('dotenv-webpack')

module.exports = {
  plugins: [
    new Dotenv({
      path: './.env',
      safe: true,        // 加载 .env.example
      systemvars: true,  // 加载系统环境变量
      silent: false,     // 隐藏错误
      defaults: false    // 加载 .env.defaults
    })
  ]
}

第六部分:环境变量最佳实践

6.1 环境变量结构

推荐的文件结构:

bash
.env                    # 默认配置(提交到 git)
.env.local             # 本地覆盖(不提交)
.env.development       # 开发环境(提交到 git)
.env.development.local # 开发本地(不提交)
.env.production        # 生产环境(提交到 git)
.env.production.local  # 生产本地(不提交)
.env.example           # 示例文件(提交到 git)

.env.example 示例:

bash
# .env.example
# API Configuration
VITE_API_BASE_URL=
VITE_API_TIMEOUT=

# Feature Flags
VITE_ENABLE_ANALYTICS=
VITE_ENABLE_DEBUG=

# Third-party Services
VITE_STRIPE_KEY=
VITE_GA_ID=

.gitignore 配置:

bash
# .gitignore
.env.local
.env.development.local
.env.test.local
.env.production.local
.env*.local

6.2 环境变量封装

创建统一的配置管理:

typescript
// src/config/env.ts
interface EnvConfig {
  apiBaseUrl: string
  apiTimeout: number
  enableAnalytics: boolean
  enableDebug: boolean
  stripeKey: string
  gaId: string
}

class Environment {
  private config: EnvConfig

  constructor() {
    this.config = this.loadConfig()
    this.validate()
  }

  private loadConfig(): EnvConfig {
    return {
      apiBaseUrl: import.meta.env.VITE_API_BASE_URL || '',
      apiTimeout: parseInt(import.meta.env.VITE_API_TIMEOUT || '30000'),
      enableAnalytics: import.meta.env.VITE_ENABLE_ANALYTICS === 'true',
      enableDebug: import.meta.env.VITE_ENABLE_DEBUG === 'true',
      stripeKey: import.meta.env.VITE_STRIPE_KEY || '',
      gaId: import.meta.env.VITE_GA_ID || '',
    }
  }

  private validate() {
    const required = ['apiBaseUrl', 'stripeKey']
    
    for (const key of required) {
      if (!this.config[key as keyof EnvConfig]) {
        throw new Error(`Missing required environment variable: ${key}`)
      }
    }
  }

  get<K extends keyof EnvConfig>(key: K): EnvConfig[K] {
    return this.config[key]
  }

  isDevelopment(): boolean {
    return import.meta.env.DEV
  }

  isProduction(): boolean {
    return import.meta.env.PROD
  }

  getMode(): string {
    return import.meta.env.MODE
  }
}

export const env = new Environment()

使用封装的配置:

typescript
// 使用
import { env } from '@/config/env'

const apiUrl = env.get('apiBaseUrl')
const timeout = env.get('apiTimeout')

if (env.isDevelopment()) {
  console.log('Development mode')
}

6.3 类型安全的环境变量

typescript
// src/config/env.config.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_ENABLE_ANALYTICS: z.enum(['true', 'false']).transform(val => val === 'true'),
  VITE_ENABLE_DEBUG: z.enum(['true', 'false']).transform(val => val === 'true'),
  VITE_STRIPE_KEY: z.string().min(1),
  VITE_GA_ID: z.string().optional(),
})

type EnvConfig = z.infer<typeof envSchema>

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

export const env = validateEnv()

6.4 功能开关管理

typescript
// src/config/features.ts
import { env } from './env'

export const features = {
  analytics: {
    enabled: env.get('enableAnalytics'),
    id: env.get('gaId'),
  },
  
  debug: {
    enabled: env.get('enableDebug'),
    logLevel: import.meta.env.VITE_LOG_LEVEL || 'info',
  },
  
  experimental: {
    newUI: import.meta.env.VITE_FEATURE_NEW_UI === 'true',
    betaFeatures: import.meta.env.VITE_FEATURE_BETA === 'true',
  },
  
  integrations: {
    stripe: {
      enabled: !!env.get('stripeKey'),
      key: env.get('stripeKey'),
    },
  },
}

使用功能开关:

typescript
import { features } from '@/config/features'

function App() {
  return (
    <div>
      {features.analytics.enabled && (
        <Analytics id={features.analytics.id} />
      )}
      
      {features.experimental.newUI && (
        <NewUI />
      )}
      
      {features.integrations.stripe.enabled && (
        <StripeProvider apiKey={features.integrations.stripe.key}>
          <PaymentForm />
        </StripeProvider>
      )}
    </div>
  )
}

6.5 多环境配置

typescript
// src/config/environments.ts
type Environment = 'development' | 'staging' | 'production'

interface EnvironmentConfig {
  apiUrl: string
  wsUrl: string
  cdnUrl: string
  logLevel: 'debug' | 'info' | 'warn' | 'error'
}

const configs: Record<Environment, EnvironmentConfig> = {
  development: {
    apiUrl: 'http://localhost:3001',
    wsUrl: 'ws://localhost:3001',
    cdnUrl: 'http://localhost:3001/static',
    logLevel: 'debug',
  },
  
  staging: {
    apiUrl: 'https://api.staging.example.com',
    wsUrl: 'wss://api.staging.example.com',
    cdnUrl: 'https://cdn.staging.example.com',
    logLevel: 'info',
  },
  
  production: {
    apiUrl: 'https://api.example.com',
    wsUrl: 'wss://api.example.com',
    cdnUrl: 'https://cdn.example.com',
    logLevel: 'error',
  },
}

export function getConfig(): EnvironmentConfig {
  const env = (import.meta.env.MODE || 'development') as Environment
  return configs[env] || configs.development
}

第七部分:安全管理

7.1 敏感信息处理

不要在客户端暴露:

bash
# ❌ 错误
VITE_DATABASE_URL=postgresql://...
VITE_SECRET_KEY=xxx
REACT_APP_API_SECRET=yyy

# ✅ 正确
DATABASE_URL=postgresql://...
SECRET_KEY=xxx
API_SECRET=yyy

使用环境变量服务:

typescript
// 使用 AWS Secrets Manager
import { SecretsManager } from '@aws-sdk/client-secrets-manager'

async function getSecret(secretName: string) {
  const client = new SecretsManager({ region: 'us-east-1' })
  
  const response = await client.getSecretValue({
    SecretId: secretName,
  })
  
  return JSON.parse(response.SecretString || '{}')
}

7.2 本地开发安全

bash
# .env.local (不提交到 git)
VITE_API_KEY=local_dev_key
VITE_STRIPE_TEST_KEY=pk_test_xxx

# .env.production.local (部署时注入)
VITE_API_KEY=prod_key
VITE_STRIPE_LIVE_KEY=pk_live_xxx

7.3 CI/CD 环境变量

GitHub Actions:

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

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Build
        env:
          VITE_API_URL: ${{ secrets.API_URL }}
          VITE_STRIPE_KEY: ${{ secrets.STRIPE_KEY }}
        run: npm run build
      
      - name: Deploy
        run: npm run deploy

Vercel:

bash
# 添加环境变量
vercel env add VITE_API_URL production
vercel env add VITE_STRIPE_KEY production

Netlify:

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

[build.environment]
  VITE_API_URL = "https://api.example.com"
  
[[context.production.environment]]
  VITE_STRIPE_KEY = "pk_live_xxx"
  
[[context.deploy-preview.environment]]
  VITE_STRIPE_KEY = "pk_test_xxx"

第八部分:调试和故障排除

8.1 调试环境变量

typescript
// src/utils/debug-env.ts
export function debugEnv() {
  if (import.meta.env.DEV) {
    console.group('Environment Variables')
    
    // 显示所有 VITE_ 开头的变量
    Object.entries(import.meta.env).forEach(([key, value]) => {
      if (key.startsWith('VITE_')) {
        console.log(`${key}:`, value)
      }
    })
    
    console.groupEnd()
  }
}
typescript
// main.tsx
import { debugEnv } from './utils/debug-env'

debugEnv()

8.2 环境变量检查

typescript
// src/utils/check-env.ts
export function checkRequiredEnv(vars: string[]) {
  const missing: string[] = []
  
  for (const varName of vars) {
    if (!import.meta.env[varName]) {
      missing.push(varName)
    }
  }
  
  if (missing.length > 0) {
    throw new Error(
      `Missing required environment variables:\n${missing.join('\n')}`
    )
  }
}

// 使用
checkRequiredEnv([
  'VITE_API_BASE_URL',
  'VITE_STRIPE_KEY',
])

8.3 常见问题

1. 环境变量未生效:

bash
# 确保重启开发服务器
npm run dev

# 确保前缀正确
VITE_API_URL=xxx  # Vite
REACT_APP_API_URL=xxx  # CRA
NEXT_PUBLIC_API_URL=xxx  # Next.js

2. 类型错误:

typescript
// 使用类型断言
const timeout = parseInt(import.meta.env.VITE_TIMEOUT || '3000')
const enabled = import.meta.env.VITE_ENABLED === 'true'

3. 环境变量被缓存:

bash
# 清除缓存
rm -rf node_modules/.vite
rm -rf .next
rm -rf dist

# 重新构建
npm run build

第九部分:完整示例

9.1 环境配置系统

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

// 定义 schema
const envSchema = z.object({
  // API 配置
  apiBaseUrl: z.string().url(),
  apiTimeout: z.number().positive(),
  
  // 功能开关
  enableAnalytics: z.boolean(),
  enableDebug: z.boolean(),
  
  // 第三方服务
  stripeKey: z.string().min(1),
  gaId: z.string().optional(),
  
  // 构建配置
  mode: z.enum(['development', 'staging', 'production']),
  version: z.string(),
})

type Config = z.infer<typeof envSchema>

// 加载和验证配置
function loadConfig(): Config {
  const rawConfig = {
    apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
    apiTimeout: parseInt(import.meta.env.VITE_API_TIMEOUT || '30000'),
    enableAnalytics: import.meta.env.VITE_ENABLE_ANALYTICS === 'true',
    enableDebug: import.meta.env.VITE_ENABLE_DEBUG === 'true',
    stripeKey: import.meta.env.VITE_STRIPE_KEY,
    gaId: import.meta.env.VITE_GA_ID,
    mode: import.meta.env.MODE,
    version: import.meta.env.VITE_APP_VERSION || '1.0.0',
  }
  
  try {
    return envSchema.parse(rawConfig)
  } catch (error) {
    console.error('Configuration validation failed:', error)
    throw error
  }
}

export const config = loadConfig()

// 辅助函数
export const isDevelopment = config.mode === 'development'
export const isProduction = config.mode === 'production'
export const isStaging = config.mode === 'staging'

9.2 使用示例

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

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

// src/App.tsx
import { config, isProduction } from '@/config'
import { Analytics } from '@/components/Analytics'

function App() {
  return (
    <div>
      {!isProduction && <DevTools />}
      
      {config.enableAnalytics && config.gaId && (
        <Analytics id={config.gaId} />
      )}
      
      <StripeProvider apiKey={config.stripeKey}>
        <PaymentForm />
      </StripeProvider>
    </div>
  )
}

总结

本章全面介绍了环境变量管理:

  1. 基础概念 - 环境变量的作用和类型
  2. 工具配置 - Vite、CRA、Next.js、Webpack 的环境变量
  3. 最佳实践 - 结构、封装、类型安全
  4. 安全管理 - 敏感信息处理和 CI/CD 集成
  5. 调试技巧 - 问题诊断和解决方案

合理使用环境变量能够提高项目的可维护性、安全性和灵活性。

扩展阅读