Skip to content

配置管理

一、为什么需要集中配置管理

硬编码配置的问题:

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 joi
typescript
// 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.development
bash
# .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_URL
typescript
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

NestJS 深度学习体系