覆盖文档:Configuration, Logger, Events, Cookies, Session 前置知识:第9课(Dynamic Modules、注入作用域与生命周期) 源码重点:
packages/config/,packages/common/services/logger.service.ts,packages/common/services/console-logger.service.ts
一、配置管理
1.1 @nestjs/config 基础
NestJS 推荐使用 @nestjs/config 管理配置,它基于 dotenv,将 .env 文件与环境变量合并为统一的配置源。
npm i --save @nestjs/config
注册 ConfigModule
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 全局可用,无需在每个模块中 import
}),
],
})
export class AppModule {}
forRoot() 默认加载项目根目录下的 .env 文件。
.env 文件
# .env
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=nestdb
APP_PORT=3000
重要:
.env文件必须加入.gitignore,不要将敏感信息提交到版本控制。
使用 ConfigService 读取配置
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class DatabaseService {
constructor(private readonly configService: ConfigService) {}
connect() {
const host = this.configService.get<string>('DATABASE_HOST');
const port = this.configService.get<number>('DATABASE_PORT');
// host = 'localhost', port = 5432
}
}
1.2 类型安全推断
使用 infer: true 选项,ConfigService 可以根据默认值自动推断返回类型:
// 返回类型自动推断为 number(基于默认值)
const port = this.configService.get('APP_PORT', { infer: true });
// 提供默认值,当配置不存在时使用
const timeout = this.configService.getOrThrow<number>('TIMEOUT');
// 如果 TIMEOUT 未定义,直接抛出异常
1.3 环境变量优先级
运行时环境变量(process.env) ← 最高优先级
↓
.env 文件中的变量 ← 较低优先级
# 运行时环境变量覆盖 .env 文件
DATABASE_HOST=production-db npm run start:prod
这意味着:生产环境通过系统环境变量或容器编排工具注入配置,.env 文件仅用于本地开发。
1.4 多环境 .env 文件
ConfigModule.forRoot({
envFilePath: ['.env.local', '.env'], // 按顺序加载,先找到的优先
// 或指定单个文件
// envFilePath: `.env.${process.env.NODE_ENV}`,
})
如果不需要 .env 文件(例如纯容器环境):
ConfigModule.forRoot({
ignoreEnvFile: true, // 完全忽略 .env,只从 process.env 读取
})
二、配置进阶
2.1 命名空间配置(registerAs)
当配置项增多时,使用命名空间分组管理:
// config/database.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
name: process.env.DATABASE_NAME || 'nestdb',
}));
// config/redis.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('redis', () => ({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
ttl: parseInt(process.env.REDIS_TTL, 10) || 300,
}));
注册命名空间配置:
import databaseConfig from './config/database.config';
import redisConfig from './config/redis.config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig, redisConfig], // 加载命名空间配置
}),
],
})
export class AppModule {}
读取命名空间配置
@Injectable()
export class DatabaseService {
constructor(private readonly configService: ConfigService) {}
getConfig() {
// 方式1:点号分隔路径
const host = this.configService.get<string>('database.host');
// 方式2:获取整个命名空间对象
const dbConfig = this.configService.get('database');
// dbConfig = { host: 'localhost', port: 5432, name: 'nestdb' }
}
}
类型安全的命名空间注入
import { Inject, Injectable } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import databaseConfig from './config/database.config';
@Injectable()
export class DatabaseService {
constructor(
@Inject(databaseConfig.KEY)
private readonly dbConfig: ConfigType<typeof databaseConfig>,
) {
// dbConfig.host — 完全类型安全,IDE 自动补全
// dbConfig.port — number 类型
}
}
2.2 模块局部配置(forFeature)
功能模块可以只加载自己需要的配置命名空间:
@Module({
imports: [ConfigModule.forFeature(databaseConfig)],
providers: [DatabaseService],
})
export class DatabaseModule {}
forFeature() 适用于非全局 ConfigModule 的场景,让模块声明自己的配置依赖。
2.3 Joi Schema 验证
在应用启动时验证配置的完整性和正确性,避免运行时才发现配置缺失:
npm i --save joi
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
APP_PORT: Joi.number().default(3000),
DATABASE_HOST: Joi.string().required(),
DATABASE_PORT: Joi.number().default(5432),
DATABASE_NAME: Joi.string().required(),
}),
validationOptions: {
allowUnknown: true, // 允许 .env 中有未定义的变量
abortEarly: false, // 收集所有验证错误再报告
},
}),
],
})
export class AppModule {}
如果必填配置缺失,应用启动时会直接抛出错误并给出明确提示。
2.4 自定义 YAML 配置
npm i --save js-yaml
npm i --save-dev @types/js-yaml
# config.yaml
http:
host: localhost
port: 3000
database:
host: localhost
port: 5432
name: nestdb
// config/configuration.ts
import { readFileSync } from 'fs';
import { join } from 'path';
import * as yaml from 'js-yaml';
const YAML_CONFIG_FILENAME = 'config.yaml';
export default () => {
return yaml.load(
readFileSync(join(__dirname, '..', YAML_CONFIG_FILENAME), 'utf8'),
) as Record<string, any>;
};
ConfigModule.forRoot({
load: [configuration], // 加载 YAML 配置
})
2.5 变量展开
支持在 .env 文件中使用变量引用:
# .env
APP_URL=mywebsite.com
SUPPORT_EMAIL=support@${APP_URL}
# SUPPORT_EMAIL 将解析为 support@mywebsite.com
ConfigModule.forRoot({
expandVariables: true, // 启用变量展开
})
2.6 条件模块加载
根据环境变量决定是否加载某个模块:
import { ConditionalModule } from '@nestjs/config';
@Module({
imports: [
// 仅当 ENABLE_MONITORING=true 时加载 MonitoringModule
ConditionalModule.registerWhen(
MonitoringModule,
'ENABLE_MONITORING',
),
// 支持自定义判断逻辑
ConditionalModule.registerWhen(
DebugModule,
(env: NodeJS.ProcessEnv) => env.NODE_ENV === 'development',
),
],
})
export class AppModule {}
三、日志系统
3.1 内置 Logger 类
NestJS 提供了内置的 Logger 类,无需安装额外依赖:
import { Logger, Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
private readonly logger = new Logger(CatsService.name);
findAll() {
this.logger.log('查询所有猫'); // [CatsService] 查询所有猫
this.logger.error('查询失败', stack); // 红色,包含堆栈
this.logger.warn('数据量过大'); // 黄色警告
this.logger.debug('SQL: SELECT * ...'); // 调试信息
this.logger.verbose('详细请求数据...'); // 详细信息
this.logger.fatal('数据库连接中断'); // 致命错误
}
}
6 个日志级别(从低到高)
| 级别 | 方法 | 场景 | 生产环境 |
|---|---|---|---|
verbose | logger.verbose() | 最详细的跟踪信息 | 关闭 |
debug | logger.debug() | 调试用的开发信息 | 关闭 |
log | logger.log() | 常规运行信息 | 开启 |
warn | logger.warn() | 警告,可能的问题 | 开启 |
error | logger.error() | 错误,需要关注 | 开启 |
fatal | logger.fatal() | 致命,系统无法继续 | 开启 |
3.2 日志级别过滤
// 只输出 error 和 warn 级别的日志
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn'],
});
// 完全禁用日志
const app = await NestFactory.create(AppModule, {
logger: false,
});
3.3 ConsoleLogger JSON 输出
在生产环境中,JSON 格式的日志更适合被 ELK、Datadog 等日志聚合平台解析:
import { NestFactory } from '@nestjs/core';
import { ConsoleLogger } from '@nestjs/common';
const app = await NestFactory.create(AppModule, {
logger: new ConsoleLogger({
json: true, // 输出 JSON 格式
colors: false, // 生产环境关闭颜色
timestamp: true, // 包含时间戳
}),
});
JSON 输出示例:
{
"level": "log",
"message": "NestApplication successfully started",
"context": "NestApplication",
"timestamp": "2025-01-15T10:30:00.000Z"
}
四、自定义 Logger 与事件驱动
4.1 实现 LoggerService 接口
import { LoggerService, Injectable } from '@nestjs/common';
@Injectable()
export class MyLogger implements LoggerService {
log(message: any, ...optionalParams: any[]) {
// 自定义日志处理:写文件、发送到远程服务等
console.log(`[INFO] ${new Date().toISOString()} ${message}`);
}
error(message: any, ...optionalParams: any[]) {
console.error(`[ERROR] ${new Date().toISOString()} ${message}`);
}
warn(message: any, ...optionalParams: any[]) {
console.warn(`[WARN] ${new Date().toISOString()} ${message}`);
}
debug?(message: any, ...optionalParams: any[]) {
console.debug(`[DEBUG] ${new Date().toISOString()} ${message}`);
}
verbose?(message: any, ...optionalParams: any[]) {
console.log(`[VERBOSE] ${new Date().toISOString()} ${message}`);
}
fatal?(message: any, ...optionalParams: any[]) {
console.error(`[FATAL] ${new Date().toISOString()} ${message}`);
}
}
4.2 继承 ConsoleLogger
更常见的做法是继承 ConsoleLogger,在保留默认行为的基础上扩展:
import { ConsoleLogger, Injectable } from '@nestjs/common';
@Injectable()
export class CustomLogger extends ConsoleLogger {
error(message: any, stack?: string, context?: string) {
// 扩展:将错误日志写入文件
this.writeToFile('error', message, stack);
// 保留原始控制台输出
super.error(message, stack, context);
}
private writeToFile(level: string, message: string, stack?: string) {
// 实际项目中使用 winston、pino 等专业日志库
const logEntry = {
level,
message,
stack,
timestamp: new Date().toISOString(),
};
// fs.appendFileSync('logs/error.log', JSON.stringify(logEntry) + '\n');
}
}
4.3 DI 注入自定义 Logger
让 NestJS 框架本身也使用自定义 Logger:
// logger.module.ts
import { Module } from '@nestjs/common';
import { CustomLogger } from './custom-logger.service';
@Module({
providers: [CustomLogger],
exports: [CustomLogger],
})
export class LoggerModule {}
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bufferLogs: true, // 缓冲启动期间的日志,直到自定义 Logger 就绪
});
// 获取 DI 管理的 Logger 实例
app.useLogger(app.get(CustomLogger));
await app.listen(3000);
}
bufferLogs: true 确保框架启动过程中的日志不会丢失——它们会被暂存,直到自定义 Logger 准备好后统一输出。
4.4 Transient Scope 实现每服务独立 Logger
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.TRANSIENT })
export class PerServiceLogger extends ConsoleLogger {
// 每次注入都会创建新实例
// 可以通过 setContext() 设置不同的上下文名称
}
@Injectable()
export class CatsService {
constructor(private readonly logger: PerServiceLogger) {
this.logger.setContext(CatsService.name);
}
findAll() {
this.logger.log('查询所有猫'); // [CatsService] 查询所有猫
}
}
4.5 事件驱动(Event Emitter)
事件驱动模式用于解耦模块间的通信,发布者不需要知道谁在监听。
npm i --save @nestjs/event-emitter
注册 EventEmitterModule
import { Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';
@Module({
imports: [
EventEmitterModule.forRoot({
// 可选配置
wildcard: true, // 启用通配符事件
delimiter: '.', // 事件名分隔符
maxListeners: 20, // 最大监听器数
verboseMemoryLeak: true, // 内存泄漏警告
}),
],
})
export class AppModule {}
定义事件
// events/order-created.event.ts
export class OrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly totalAmount: number,
) {}
}
发射事件
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { OrderCreatedEvent } from './events/order-created.event';
@Injectable()
export class OrdersService {
constructor(private readonly eventEmitter: EventEmitter2) {}
async createOrder(data: CreateOrderDto) {
const order = await this.saveOrder(data);
// 发射事件——不阻塞主流程
this.eventEmitter.emit(
'order.created',
new OrderCreatedEvent(order.id, data.userId, data.totalAmount),
);
return order;
}
}
监听事件
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { OrderCreatedEvent } from './events/order-created.event';
@Injectable()
export class NotificationService {
@OnEvent('order.created')
handleOrderCreated(event: OrderCreatedEvent) {
// 发送订单确认邮件
console.log(`发送邮件: 订单 ${event.orderId} 已创建`);
}
}
@Injectable()
export class AnalyticsService {
// 通配符监听所有 order.* 事件
@OnEvent('order.*')
handleOrderEvents(event: any) {
console.log('记录订单分析数据');
}
}
事件监听选项
@OnEvent('order.created', {
async: true, // 异步执行(不阻塞发射者)
prependListener: false, // 是否添加到监听器列表头部
suppressErrors: false, // 是否静默处理错误
})
handleOrderCreated(event: OrderCreatedEvent) {
// ...
}
五、Cookie 与 Session
5.1 Cookie
Express 平台
npm i --save cookie-parser
npm i --save-dev @types/cookie-parser
// main.ts
import * as cookieParser from 'cookie-parser';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser());
await app.listen(3000);
}
import { Controller, Get, Res, Req } from '@nestjs/common';
import { Request, Response } from 'express';
@Controller('auth')
export class AuthController {
@Get('login')
login(@Res({ passthrough: true }) res: Response) {
// 设置 Cookie
res.cookie('token', 'abc123', {
httpOnly: true, // 禁止 JS 访问
secure: true, // 仅 HTTPS
sameSite: 'lax', // CSRF 防护
maxAge: 3600000, // 1 小时
});
return { message: '登录成功' };
}
@Get('profile')
getProfile(@Req() req: Request) {
// 读取 Cookie
const token = req.cookies['token'];
return { token };
}
}
Fastify 平台
npm i --save @fastify/cookie
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import fastifyCookie from '@fastify/cookie';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.register(fastifyCookie, {
secret: 'my-cookie-secret', // 签名密钥
});
await app.listen(3000, '0.0.0.0');
}
5.2 Session
Express 平台
npm i --save express-session
npm i --save-dev @types/express-session
// main.ts
import * as session from 'express-session';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(
session({
secret: 'my-session-secret',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 3600000, // 1 小时
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
},
}),
);
await app.listen(3000);
}
import { Controller, Get, Session } from '@nestjs/common';
@Controller()
export class AppController {
@Get('visit')
incrementVisits(@Session() session: Record<string, any>) {
session.visits = (session.visits || 0) + 1;
return { visits: session.visits };
}
}
Fastify 平台
npm i --save @fastify/secure-session
import secureSession from '@fastify/secure-session';
import { readFileSync } from 'fs';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.register(secureSession, {
key: readFileSync('secret-key'),
cookie: { path: '/', httpOnly: true },
});
await app.listen(3000, '0.0.0.0');
}
5.3 生产环境 Session 存储
关键:默认的内存 Session 存储不适合生产环境——进程重启会丢失所有会话,多实例部署也无法共享。
npm i --save connect-redis ioredis
import RedisStore from 'connect-redis';
import { Redis } from 'ioredis';
const redisClient = new Redis({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT, 10),
});
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 86400000, // 24 小时
httpOnly: true,
secure: true,
},
}),
);
六、源码解读:ConfigModule 实现
[资深] 本节面向希望深入理解框架内部机制的读者。
6.1 ConfigurableModuleBuilder 的典型应用
@nestjs/config 的 ConfigModule 是 ConfigurableModuleBuilder 的一个典型实践。理解它的源码有助于创建自己的可配置模块。
ConfigModule
│
├── forRoot(options) → 静态配置,注册全局 ConfigService
├── forFeature(config) → 注册命名空间配置到当前模块
└── ConfigService → 统一的配置读取接口
6.2 registerAs() 的实现原理
registerAs() 创建一个命名空间配置工厂,其核心是生成唯一的注入 token:
// @nestjs/config 内部简化逻辑
export function registerAs<T>(
namespace: string,
configFactory: () => T,
): ConfigFactory & { KEY: string } {
// 1. 为命名空间生成唯一 token
const configToken = `CONFIGURATION(${namespace})`;
// 2. 将 token 附加到工厂函数
Object.defineProperty(configFactory, 'KEY', { value: configToken });
// 3. 返回增强后的工厂函数
return configFactory as any;
}
这就是为什么可以通过 @Inject(databaseConfig.KEY) 注入特定命名空间的配置。
6.3 ConfigService 的类型推断机制
ConfigService 使用 TypeScript 的条件类型和泛型来实现 get() 的类型推断:
// 简化的 ConfigService 类型签名
class ConfigService<K = Record<string, unknown>> {
get<T = any>(propertyPath: KeyOf<K>): T | undefined;
get<T = any>(propertyPath: KeyOf<K>, defaultValue: T): T;
get<T = any>(
propertyPath: KeyOf<K>,
options: { infer: true },
): T | undefined;
getOrThrow<T = any>(propertyPath: KeyOf<K>): T;
}
当使用 infer: true 时,TypeScript 会根据 ConfigModule.forRoot() 的 load 配置推断出完整的配置类型,实现编译时类型检查。
6.4 配置加载流程
应用启动
│
├─→ ConfigModule.forRoot() 注册为动态模块
│ │
│ ├─→ 加载 .env 文件(dotenv.parse)
│ ├─→ 合并 process.env
│ ├─→ 执行 load[] 中的配置工厂
│ ├─→ 执行 validationSchema 验证
│ └─→ 注册 ConfigService 为全局 provider
│
└─→ ConfigModule.forFeature(config)
│
└─→ 执行配置工厂并注册为命名空间 provider
七、配置与日志架构设计
[架构] 本节面向技术负责人和架构师。
7.1 12-Factor App 配置原则
NestJS 的配置管理遵循 12-Factor App 的第三条原则——在环境中存储配置:
| 原则 | 实践 |
|---|---|
| 环境变量优先 | process.env 覆盖 .env 文件 |
| 不硬编码配置 | 使用 ConfigService 读取,不在代码中写死 |
| 区分构建与运行 | 同一份构建产物,通过环境变量切换环境 |
| 配置与代码分离 | .env 不入库,敏感信息用密钥管理服务 |
7.2 结构化 JSON 日志
生产环境推荐结构化 JSON 日志,便于日志聚合平台(ELK、Datadog、CloudWatch)解析:
┌────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ NestJS App │───→│ 日志收集器 │───→│ 日志聚合平台 │
│ JSON Logger │ │ (Filebeat/ │ │ (ELK/Datadog/ │
│ │ │ Fluentd) │ │ CloudWatch) │
└────────────────┘ └──────────────┘ └─────────────────┘
7.3 关联 ID(Correlation ID)
在分布式系统中,使用 Correlation ID 将同一请求在不同服务中的日志串联起来:
// correlation-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';
@Injectable()
export class CorrelationIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const correlationId = req.headers['x-correlation-id'] as string
|| randomUUID();
req['correlationId'] = correlationId;
res.setHeader('x-correlation-id', correlationId);
next();
}
}
每条日志都附带 correlationId,在日志平台中可按此 ID 搜索完整请求链路。
7.4 日志分级策略
| 环境 | 日志级别 | 输出格式 | 输出目标 |
|---|---|---|---|
| 开发 | verbose, debug, log, warn, error | 彩色文本 | 控制台 |
| 测试 | warn, error | 文本 | 控制台 |
| 预发布 | log, warn, error | JSON | 文件 + 聚合 |
| 生产 | log, warn, error | JSON | 聚合平台 |
// 根据环境动态配置日志级别
const logLevels: LogLevel[] = process.env.NODE_ENV === 'production'
? ['log', 'warn', 'error', 'fatal']
: ['verbose', 'debug', 'log', 'warn', 'error', 'fatal'];
const app = await NestFactory.create(AppModule, {
logger: logLevels,
});
7.5 事件驱动架构要点
| 场景 | 同步调用 | 事件驱动 |
|---|---|---|
| 订单创建后发邮件 | Service 直接调用 EmailService | 发射 order.created 事件 |
| 用户注册后初始化 | Controller 调用多个 Service | 发射 user.registered 事件 |
| 耦合度 | 高(直接依赖) | 低(通过事件解耦) |
| 可扩展性 | 需修改调用方 | 新增监听器即可 |
| 调试难度 | 低(调用链清晰) | 中(需跟踪事件流) |
| 适用场景 | 核心业务流程 | 副作用操作(通知、日志、统计) |
八、课后实践
练习 1:配置驱动的数据库连接(基础)
将硬编码的数据库配置改为 .env 文件驱动:
# .env
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_NAME=cats_db
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
// 使用 ConfigService 异步配置数据库
// TypeOrmModule.forRootAsync({
// inject: [ConfigService],
// useFactory: (config: ConfigService) => ({
// type: 'postgres',
// host: config.get('DATABASE_HOST'),
// port: config.get('DATABASE_PORT'),
// username: config.get('DATABASE_USER'),
// database: config.get('DATABASE_NAME'),
// }),
// }),
],
})
export class AppModule {}
练习 2:自定义 FileLogger(中阶)
继承 ConsoleLogger,将 error 级别日志同时写入文件:
import { ConsoleLogger, Injectable } from '@nestjs/common';
import { appendFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
@Injectable()
export class FileLogger extends ConsoleLogger {
private readonly logDir = join(process.cwd(), 'logs');
constructor() {
super();
if (!existsSync(this.logDir)) {
mkdirSync(this.logDir, { recursive: true });
}
}
error(message: any, stack?: string, context?: string) {
const logEntry = JSON.stringify({
level: 'error',
message,
stack,
context,
timestamp: new Date().toISOString(),
});
appendFileSync(join(this.logDir, 'error.log'), logEntry + '\n');
super.error(message, stack, context);
}
}
在 main.ts 中注册,并使用 bufferLogs: true。
练习 3:事件驱动解耦——注册后发送欢迎邮件(中阶)
// 1. 定义事件
export class UserRegisteredEvent {
constructor(
public readonly userId: string,
public readonly email: string,
) {}
}
// 2. 在 UsersService 中发射事件
@Injectable()
export class UsersService {
constructor(private readonly eventEmitter: EventEmitter2) {}
async register(dto: RegisterDto) {
const user = await this.saveUser(dto);
this.eventEmitter.emit('user.registered', new UserRegisteredEvent(user.id, dto.email));
return user;
}
}
// 3. 在 EmailService 中监听事件
@Injectable()
export class EmailService {
@OnEvent('user.registered')
async sendWelcomeEmail(event: UserRegisteredEvent) {
// 发送欢迎邮件
}
}
练习 4:使用 Joi 验证配置(中阶)
为应用添加 Joi 验证,确保必要的配置项在启动时就被检查。故意删除 .env 中的必填项,观察应用启动时的错误信息。
练习 5:阅读 ConfigModule 源码(资深)
打开 @nestjs/config 包的源码,回答:
registerAs()如何生成命名空间的注入 token?ConfigService.get()内部如何区分普通配置和命名空间配置?forRoot()和forFeature()返回的动态模块有什么结构差异?
九、本课知识点总结
| 知识点 | 要点 |
|---|---|
| ConfigModule | forRoot({ isGlobal: true }),基于 dotenv,运行时环境变量优先 |
| ConfigService | .get<T>(key),infer: true 类型推断,.getOrThrow() |
| 命名空间配置 | registerAs() + forFeature(),@Inject(config.KEY) 类型安全注入 |
| 配置验证 | Joi Schema,abortEarly: false 收集所有错误 |
| Logger | 6 级日志,ConsoleLogger JSON 输出,new Logger(Context.name) |
| 自定义 Logger | 实现 LoggerService 或继承 ConsoleLogger,bufferLogs: true |
| Event Emitter | EventEmitterModule.forRoot(),emit() + @OnEvent(),通配符 |
| Cookie | cookie-parser(Express)/ @fastify/cookie(Fastify) |
| Session | express-session / @fastify/secure-session,生产用 Redis 存储 |
| 源码入口 | registerAs() 生成命名空间 token,ConfigService 类型推断 |
下一课预告:第十四课将学习异步处理的三大工具——缓存(cache-manager)、定时任务(@nestjs/schedule)和消息队列(BullMQ),掌握提升应用性能和处理耗时任务的核心技术。