Skip to content

配置管理:ConfigService 如何加载 .env 文件?

在现代应用程序开发中,配置管理是一个至关重要的方面。不同的环境(开发、测试、生产)需要不同的配置,而敏感信息(如数据库密码、API密钥)需要安全地管理。NestJS 通过 @nestjs/config 包提供了强大的配置管理功能,支持 .env 文件加载、环境变量验证、配置分层等特性。本文将深入探讨 ConfigService 如何加载 .env 文件,以及多环境配置、验证和注入的最佳实践。

1. 配置管理基础概念

1.1 为什么需要配置管理?

现代应用程序面临多种配置需求:

typescript
// 配置管理的挑战
// 1. 环境差异:不同环境需要不同的配置
// 2. 安全性:敏感信息不能硬编码在代码中
// 3. 灵活性:配置需要在不修改代码的情况下可调整
// 4. 可维护性:配置需要集中管理和版本控制

// 传统方式的问题
const databaseConfig = {
  host: 'localhost',     // 生产环境应该是不同的主机
  port: 5432,           // 可能需要根据不同环境调整
  username: 'admin',    // 敏感信息不应该硬编码
  password: 'password', // 安全风险
};

// 更好的方式
const databaseConfig = {
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT) || 5432,
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
};

1.2 NestJS 配置系统

NestJS 配置系统的核心组件:

typescript
// @nestjs/config 的主要功能
// 1. 环境变量加载:自动加载 .env 文件
// 2. 配置验证:使用 Joi 等库验证配置
// 3. 配置分层:支持多环境配置
// 4. 类型安全:提供 TypeScript 支持
// 5. 模块集成:与 NestJS 模块系统无缝集成

2. 基础配置设置

2.1 安装和配置

bash
# 安装 @nestjs/config
npm install @nestjs/config

# 安装 Joi 用于配置验证(可选但推荐)
npm install joi
typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true, // 全局模块,无需在其他模块中导入
      envFilePath: '.env', // 环境文件路径
    }),
  ],
})
export class AppModule {}

2.2 环境文件组织

bash
# 环境文件结构
project/
├── .env                 # 默认环境变量
├── .env.development     # 开发环境
├── .env.staging         # 预发布环境
├── .env.production      # 生产环境
├── .env.test           # 测试环境
└── .env.local          # 本地覆盖(通常加入 .gitignore)
bash
# .env 文件示例
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=admin
DB_PASSWORD=password
DB_NAME=myapp

# Redis 配置
REDIS_HOST=localhost
REDIS_PORT=6379

# JWT 配置
JWT_SECRET=my-secret-key
JWT_EXPIRES_IN=3600

# 外部服务配置
API_BASE_URL=https://api.example.com
API_KEY=your-api-key

# 应用配置
PORT=3000
NODE_ENV=development
LOG_LEVEL=info

3. ConfigService 使用

3.1 基本使用

typescript
// 基本配置获取
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class DatabaseService {
  constructor(private readonly configService: ConfigService) {}
  
  getDatabaseConfig() {
    return {
      host: this.configService.get('DB_HOST'),
      port: this.configService.get('DB_PORT'),
      username: this.configService.get('DB_USERNAME'),
      password: this.configService.get('DB_PASSWORD'),
      database: this.configService.get('DB_NAME'),
    };
  }
  
  getDatabaseUrl() {
    // 使用 getOrThrow 确保必需配置存在
    const host = this.configService.getOrThrow('DB_HOST');
    const port = this.configService.getOrThrow('DB_PORT');
    const username = this.configService.getOrThrow('DB_USERNAME');
    const password = this.configService.getOrThrow('DB_PASSWORD');
    const database = this.configService.getOrThrow('DB_NAME');
    
    return `postgresql://${username}:${password}@${host}:${port}/${database}`;
  }
}

3.2 类型安全的配置获取

typescript
// 类型安全的配置获取
interface DatabaseConfig {
  host: string;
  port: number;
  username: string;
  password: string;
  database: string;
}

interface AppConfig {
  port: number;
  nodeEnv: string;
  logLevel: string;
}

@Injectable()
export class TypedConfigService {
  constructor(private readonly configService: ConfigService) {}
  
  getDatabaseConfig(): DatabaseConfig {
    return {
      host: this.configService.get<string>('DB_HOST', 'localhost'),
      port: this.configService.get<number>('DB_PORT', 5432),
      username: this.configService.get<string>('DB_USERNAME', 'admin'),
      password: this.configService.get<string>('DB_PASSWORD', 'password'),
      database: this.configService.get<string>('DB_NAME', 'myapp'),
    };
  }
  
  getAppConfig(): AppConfig {
    return {
      port: this.configService.get<number>('PORT', 3000),
      nodeEnv: this.configService.get<string>('NODE_ENV', 'development'),
      logLevel: this.configService.get<string>('LOG_LEVEL', 'info'),
    };
  }
}

4. 配置验证

4.1 使用 Joi 进行验证

typescript
// 配置验证
import * as Joi from 'joi';

// 配置验证模式
const validationSchema = Joi.object({
  // 数据库配置
  DB_HOST: Joi.string().hostname().required(),
  DB_PORT: Joi.number().port().default(5432),
  DB_USERNAME: Joi.string().required(),
  DB_PASSWORD: Joi.string().required(),
  DB_NAME: Joi.string().required(),
  
  // Redis 配置
  REDIS_HOST: Joi.string().hostname().required(),
  REDIS_PORT: Joi.number().port().default(6379),
  
  // JWT 配置
  JWT_SECRET: Joi.string().min(32).required(),
  JWT_EXPIRES_IN: Joi.number().integer().positive().default(3600),
  
  // 应用配置
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'test', 'staging')
    .default('development'),
  PORT: Joi.number().port().default(3000),
  LOG_LEVEL: Joi.string()
    .valid('error', 'warn', 'info', 'debug')
    .default('info'),
  
  // 外部服务配置
  API_BASE_URL: Joi.string().uri().required(),
  API_KEY: Joi.string().required(),
});

// 在模块中使用验证
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema,
      validationOptions: {
        allowUnknown: true,  // 允许未知配置项
        abortEarly: false,   // 报告所有验证错误
      },
    }),
  ],
})
export class AppModule {}

4.2 自定义验证函数

typescript
// 自定义验证函数
const customValidation = (config: Record<string, any>) => {
  // 自定义验证逻辑
  if (config.NODE_ENV === 'production') {
    // 生产环境必须设置某些配置
    if (!config.SSL_CERT_PATH) {
      throw new Error('SSL_CERT_PATH is required in production');
    }
    
    if (!config.SSL_KEY_PATH) {
      throw new Error('SSL_KEY_PATH is required in production');
    }
  }
  
  // 验证数据库连接字符串
  if (config.DATABASE_URL) {
    try {
      new URL(config.DATABASE_URL);
    } catch (error) {
      throw new Error('Invalid DATABASE_URL format');
    }
  }
  
  // 返回验证后的配置
  return config;
};

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validate: customValidation,
    }),
  ],
})
export class AppModule {}

5. 多环境配置

5.1 环境特定配置

typescript
// 多环境配置加载
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: [
        `.env.${process.env.NODE_ENV}`, // 环境特定文件
        '.env',                         // 默认文件
      ],
      load: [
        // 自定义配置加载函数
        databaseConfig,
        redisConfig,
        jwtConfig,
      ],
    }),
  ],
})
export class AppModule {}

// 数据库配置加载函数
export default () => ({
  database: {
    host: process.env.DB_HOST,
    port: parseInt(process.env.DB_PORT, 10) || 5432,
    username: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD,
    name: process.env.DB_NAME,
  },
});

// Redis 配置加载函数
export default () => ({
  redis: {
    host: process.env.REDIS_HOST,
    port: parseInt(process.env.REDIS_PORT, 10) || 6379,
    ttl: parseInt(process.env.REDIS_TTL, 10) || 3600,
  },
});

// JWT 配置加载函数
export default () => ({
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '1h',
  },
});

5.2 配置分层管理

typescript
// 配置分层管理
// config/
// ├── index.ts
// ├── database.config.ts
// ├── redis.config.ts
// ├── jwt.config.ts
// └── app.config.ts

// config/database.config.ts
export interface DatabaseConfig {
  host: string;
  port: number;
  username: string;
  password: string;
  name: string;
}

export default (): DatabaseConfig => ({
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT, 10) || 5432,
  username: process.env.DB_USERNAME || 'admin',
  password: process.env.DB_PASSWORD || 'password',
  name: process.env.DB_NAME || 'myapp',
});

// config/app.config.ts
export interface AppConfig {
  port: number;
  nodeEnv: string;
  logLevel: string;
  isDevelopment: boolean;
  isProduction: boolean;
}

export default (): AppConfig => {
  const nodeEnv = process.env.NODE_ENV || 'development';
  
  return {
    port: parseInt(process.env.PORT, 10) || 3000,
    nodeEnv,
    logLevel: process.env.LOG_LEVEL || 'info',
    isDevelopment: nodeEnv === 'development',
    isProduction: nodeEnv === 'production',
  };
};

// config/index.ts
import databaseConfig from './database.config';
import appConfig from './app.config';
import redisConfig from './redis.config';
import jwtConfig from './jwt.config';

export interface AllConfigType {
  database: ReturnType<typeof databaseConfig>;
  app: ReturnType<typeof appConfig>;
  redis: ReturnType<typeof redisConfig>;
  jwt: ReturnType<typeof jwtConfig>;
}

export default (): AllConfigType => ({
  database: databaseConfig(),
  app: appConfig(),
  redis: redisConfig(),
  jwt: jwtConfig(),
});

6. 配置注入和使用

6.1 配置注入方式

typescript
// 配置注入的多种方式
@Injectable()
export class ConfiguredService {
  // 1. 构造函数注入
  constructor(private readonly configService: ConfigService) {}
  
  // 2. 属性注入
  @Inject(ConfigService)
  private readonly configService: ConfigService;
  
  // 3. 自定义配置提供者
  @Inject('DATABASE_CONFIG')
  private readonly databaseConfig: DatabaseConfig;
  
  getDatabaseConfig() {
    // 直接使用 ConfigService
    return {
      host: this.configService.get<string>('DB_HOST'),
      port: this.configService.get<number>('DB_PORT'),
    };
  }
  
  getAppPort() {
    // 使用默认值
    return this.configService.get<number>('PORT', 3000);
  }
  
  getRequiredConfig() {
    // 获取必需配置(不存在时抛出异常)
    return this.configService.getOrThrow<string>('JWT_SECRET');
  }
}

6.2 配置提供者注册

typescript
// 配置提供者注册
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [databaseConfig, appConfig],
    }),
  ],
  providers: [
    {
      provide: 'DATABASE_CONFIG',
      useFactory: (configService: ConfigService) => ({
        host: configService.get<string>('DB_HOST'),
        port: configService.get<number>('DB_PORT'),
        username: configService.get<string>('DB_USERNAME'),
      }),
      inject: [ConfigService],
    },
    {
      provide: 'APP_CONFIG',
      useValue: {
        version: '1.0.0',
        name: 'MyApp',
      },
    },
  ],
  exports: ['DATABASE_CONFIG', 'APP_CONFIG'],
})
export class ConfiguredModule {}

7. 高级配置功能

7.1 配置热重载

typescript
// 配置热重载(开发环境)
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      cache: process.env.NODE_ENV !== 'development', // 开发环境不缓存
      expandVariables: true, // 支持环境变量展开
    }),
  ],
})
export class AppModule {}

// .env 文件中的变量展开
// API_BASE_URL=https://api.example.com
// USER_SERVICE_URL=${API_BASE_URL}/users
// ORDER_SERVICE_URL=${API_BASE_URL}/orders

7.2 配置变更监听

typescript
// 配置变更监听
@Injectable()
export class ConfigWatcherService {
  private readonly logger = new Logger(ConfigWatcherService.name);
  
  constructor(
    private readonly configService: ConfigService,
    private readonly eventEmitter: EventEmitter2,
  ) {}
  
  onModuleInit() {
    // 监听配置变更事件
    this.eventEmitter.on('config.changed', (key: string, value: any) => {
      this.logger.log(`Configuration changed: ${key} = ${value}`);
      this.handleConfigChange(key, value);
    });
  }
  
  private handleConfigChange(key: string, value: any) {
    // 处理配置变更
    switch (key) {
      case 'LOG_LEVEL':
        this.updateLogLevel(value);
        break;
      case 'DB_HOST':
        this.reconnectDatabase(value);
        break;
      // 其他配置变更处理
    }
  }
  
  private updateLogLevel(level: string) {
    // 更新日志级别
    Logger.overrideLogger([level]);
  }
  
  private reconnectDatabase(newHost: string) {
    // 重新连接数据库
    // 实际实现取决于数据库连接方式
  }
}

7.3 配置加密和解密

typescript
// 配置加密和解密
@Injectable()
export class SecureConfigService {
  constructor(
    private readonly configService: ConfigService,
    private readonly encryptionService: EncryptionService,
  ) {}
  
  getEncryptedConfig(key: string): string {
    const encryptedValue = this.configService.get<string>(key);
    if (!encryptedValue) {
      return null;
    }
    
    // 解密配置值
    return this.encryptionService.decrypt(encryptedValue);
  }
  
  getDatabasePassword(): string {
    return this.getEncryptedConfig('ENCRYPTED_DB_PASSWORD');
  }
}

// .env 文件
// ENCRYPTED_DB_PASSWORD=AES256:encrypted_password_here

8. 总结

NestJS 配置管理系统的核心特性:

  1. 环境变量加载:自动加载 .env 文件和系统环境变量
  2. 配置验证:使用 Joi 等库进行配置验证
  3. 类型安全:提供 TypeScript 类型支持
  4. 多环境支持:支持不同环境的配置文件
  5. 配置分层:支持自定义配置加载函数
  6. 变量展开:支持环境变量之间的引用和展开

配置管理的最佳实践:

  1. 分离敏感信息:敏感配置不应该提交到版本控制系统
  2. 环境特定配置:为不同环境使用不同的配置文件
  3. 配置验证:使用验证确保配置的正确性
  4. 默认值设置:为配置项提供合理的默认值
  5. 类型安全:使用 TypeScript 提供类型安全的配置访问
  6. 文档化:为配置项提供清晰的文档说明

通过合理使用 NestJS 的配置管理系统,我们可以:

  1. 提高安全性:安全地管理敏感配置信息
  2. 增强灵活性:在不修改代码的情况下调整配置
  3. 简化部署:通过环境变量轻松配置不同环境
  4. 减少错误:通过验证减少配置错误
  5. 改善维护性:集中管理配置,便于维护

在下一篇文章中,我们将探讨测试金字塔:E2E 测试如何启动完整应用?了解 Test.createTestingModule()supertest 的协作。