配置管理: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 joitypescript
// 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=info3. 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}/orders7.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_here8. 总结
NestJS 配置管理系统的核心特性:
- 环境变量加载:自动加载
.env文件和系统环境变量 - 配置验证:使用 Joi 等库进行配置验证
- 类型安全:提供 TypeScript 类型支持
- 多环境支持:支持不同环境的配置文件
- 配置分层:支持自定义配置加载函数
- 变量展开:支持环境变量之间的引用和展开
配置管理的最佳实践:
- 分离敏感信息:敏感配置不应该提交到版本控制系统
- 环境特定配置:为不同环境使用不同的配置文件
- 配置验证:使用验证确保配置的正确性
- 默认值设置:为配置项提供合理的默认值
- 类型安全:使用 TypeScript 提供类型安全的配置访问
- 文档化:为配置项提供清晰的文档说明
通过合理使用 NestJS 的配置管理系统,我们可以:
- 提高安全性:安全地管理敏感配置信息
- 增强灵活性:在不修改代码的情况下调整配置
- 简化部署:通过环境变量轻松配置不同环境
- 减少错误:通过验证减少配置错误
- 改善维护性:集中管理配置,便于维护
在下一篇文章中,我们将探讨测试金字塔:E2E 测试如何启动完整应用?了解 Test.createTestingModule() 与 supertest 的协作。