配置管理
一、为什么需要集中配置管理
硬编码配置的问题:
typescript
// ❌ 散落各处的硬编码
const JWT_SECRET = 'my-secret'; // 提交到 Git 了!
const DB_URL = 'postgres://localhost/mydb';
const TIMEOUT = 5000;
// 更换环境需要改代码,生产事故的常见原因typescript
// ✅ 集中管理,通过环境变量注入
this.configService.get<string>('JWT_SECRET')
this.configService.get<string>('DATABASE_URL')
this.configService.get<number>('REQUEST_TIMEOUT', 5000) // 有默认值二、@nestjs/config 基础
bash
npm install @nestjs/config joitypescript
// app.module.ts
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 全局可用,无需每个模块 import
cache: true, // 缓存配置读取结果(性能优化)
expandVariables: true, // 支持 ${OTHER_VAR} 引用
// 按优先级加载(前面的优先级更高)
envFilePath: [
`.env.${process.env.NODE_ENV}.local`,
`.env.${process.env.NODE_ENV}`,
'.env.local',
'.env',
],
// 启动时验证,缺少必要变量立即报错
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test', 'staging')
.default('development'),
PORT: Joi.number().default(3000),
DATABASE_URL: Joi.string().uri().required(),
JWT_SECRET: Joi.string().min(32).required(),
JWT_REFRESH_SECRET: Joi.string().min(32).required(),
JWT_EXPIRES_IN: Joi.string().default('15m'),
REDIS_URL: Joi.string().optional(),
LOG_LEVEL: Joi.string()
.valid('error', 'warn', 'log', 'debug', 'verbose')
.default('log'),
}),
validationOptions: {
abortEarly: false, // 报告所有验证错误(不是第一个就停)
allowUnknown: true, // 允许 .env 中有未声明的变量
},
}),
],
})
export class AppModule {}三、命名空间配置(registerAs)
将配置按模块分组,避免所有配置混在一起:
typescript
// config/database.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
url: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production',
pool: {
max: parseInt(process.env.DB_POOL_MAX ?? '10'),
min: parseInt(process.env.DB_POOL_MIN ?? '2'),
idleTimeoutMillis: 30000,
},
logging: process.env.NODE_ENV === 'development',
}));typescript
// config/jwt.config.ts
export default registerAs('jwt', () => ({
secret: process.env.JWT_SECRET,
refreshSecret: process.env.JWT_REFRESH_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN ?? '15m',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '7d',
}));typescript
// config/redis.config.ts
export default registerAs('redis', () => {
const url = process.env.REDIS_URL;
if (url) {
const parsed = new URL(url);
return { host: parsed.hostname, port: parseInt(parsed.port), password: parsed.password };
}
return {
host: process.env.REDIS_HOST ?? 'localhost',
port: parseInt(process.env.REDIS_PORT ?? '6379'),
};
});typescript
// app.module.ts 注册所有配置
import databaseConfig from './config/database.config';
import jwtConfig from './config/jwt.config';
import redisConfig from './config/redis.config';
ConfigModule.forRoot({
load: [databaseConfig, jwtConfig, redisConfig],
// ...
})四、在服务中使用配置
基础用法
typescript
@Injectable()
export class AuthService {
constructor(private config: ConfigService) {}
sign(payload: object) {
const secret = this.config.getOrThrow<string>('jwt.secret');
// getOrThrow:不存在时抛出异常(比 get 更安全)
return this.jwtService.sign(payload, { secret });
}
}命名空间用法(类型安全)
typescript
import { ConfigType } from '@nestjs/config';
import jwtConfig from '../config/jwt.config';
@Injectable()
export class AuthService {
constructor(
@Inject(jwtConfig.KEY)
private jwtConf: ConfigType<typeof jwtConfig>, // 完整类型推断
) {}
sign(payload: object) {
return this.jwtService.sign(payload, {
secret: this.jwtConf.secret, // IDE 有完整提示
expiresIn: this.jwtConf.expiresIn,
});
}
}异步模块配置(最常见)
typescript
// database/database.module.ts
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
url: config.getOrThrow('DATABASE_URL'),
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: config.get('NODE_ENV') !== 'production',
logging: config.get('NODE_ENV') === 'development' ? ['query', 'error'] : ['error'],
ssl: config.get('NODE_ENV') === 'production' ? { rejectUnauthorized: false } : false,
extra: {
max: config.get<number>('DB_POOL_MAX', 10),
min: config.get<number>('DB_POOL_MIN', 2),
},
}),
})五、多环境配置策略
文件结构
.env # 本地默认(提交示例,不提交真实值)
.env.development # 开发环境特定配置(可提交)
.env.test # 测试环境(可提交,只有测试值)
.env.production # ❌ 不存在!生产配置通过 CI/CD 注入
.env.local # 本地覆盖(不提交,在 .gitignore 中)bash
# .gitignore
.env
.env.local
.env.*.local
# 但允许提交 .env.example 和 .env.developmentbash
# .env.example(提交到 Git,只包含键名)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://user:password@localhost:5432/mydb
JWT_SECRET=change-this-to-a-random-32-char-secret
JWT_REFRESH_SECRET=change-this-to-another-32-char-secret生产环境注入(不使用 .env 文件)
yaml
# docker-compose.yml
services:
api:
image: myapp:latest
environment:
NODE_ENV: production
DATABASE_URL: ${DATABASE_URL} # 从 CI/CD 环境变量注入
JWT_SECRET: ${JWT_SECRET}
# 不挂载 .env 文件!yaml
# GitHub Actions:使用 Secrets
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
run: npm run deploy六、配置类型安全封装
对于复杂项目,创建强类型配置服务:
typescript
// config/app-config.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AppConfigService {
constructor(private config: ConfigService) {}
get nodeEnv(): string {
return this.config.getOrThrow<string>('NODE_ENV');
}
get isProduction(): boolean {
return this.nodeEnv === 'production';
}
get isDevelopment(): boolean {
return this.nodeEnv === 'development';
}
get port(): number {
return this.config.get<number>('PORT', 3000);
}
get databaseUrl(): string {
return this.config.getOrThrow<string>('DATABASE_URL');
}
get jwtSecret(): string {
return this.config.getOrThrow<string>('JWT_SECRET');
}
get jwtExpiresIn(): string {
return this.config.get<string>('JWT_EXPIRES_IN', '15m');
}
}七、常见问题
问题 1:ConfigService 在 Module 配置中无法注入
typescript
// ❌ forRoot() 是同步的,无法注入 ConfigService
TypeOrmModule.forRoot({
url: configService.get('DATABASE_URL'), // 这里 configService 不可用
})
// ✅ 用 forRootAsync() + inject
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({ url: config.get('DATABASE_URL') }),
})问题 2:get() 返回 undefined
typescript
// 原因:变量名拼写错误、.env 文件未正确加载
const secret = this.config.get('JTW_SECRET'); // 拼写错误 JTW
// 排查方法
console.log(process.env.JWT_SECRET); // 直接看环境变量
this.config.getOrThrow('JWT_SECRET'); // 不存在时立即报错并告知原因问题 3:expandVariables 引用其他变量
bash
# .env
BASE_URL=https://api.example.com
CALLBACK_URL=${BASE_URL}/auth/callback # 引用 BASE_URLtypescript
ConfigModule.forRoot({ expandVariables: true })
// CALLBACK_URL → 'https://api.example.com/auth/callback'可运行 Demo:
practice/07-extensions— ConfigService + 环境变量注入到各模块
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
生产环境 .env 文件未加载 | envFilePath 配置路径错误 | 用绝对路径或检查 process.cwd();生产用环境变量而非文件 |
ConfigService.get() 返回 undefined | 变量名拼写错误或未在 .env 设置 | 加 Joi schema 校验,启动时报错而非运行时出错 |
| 配置在测试中硬编码 | 直接读 process.env | 注入 ConfigService,测试中 useValue: { get: jest.fn().mockReturnValue('...') } |
| 敏感配置泄漏到日志 | 日志打印了 config 对象 | 用 @Exclude() 或避免整体打印 config |