事件机制
本教程共 47 篇 · 第 32 篇 · 更新于 2026-08-09 · 约 10 分钟阅读
本节目标:学会用事件机制让模块之间”喊一嗓子”就能通知对方,不用互相引用、互相依赖。
什么是事件机制
想象一个场景:用户注册成功后,你需要发欢迎邮件、记录审计日志、送新人优惠券。
最直觉的做法是在注册方法里挨个调用这些服务。但问题来了——每加一个后续操作,你就得改一次注册方法。代码越来越长,耦合越来越重。
事件机制就是来解决这个问题的。
你可以把它理解成一个”广播站”。注册服务只管喊一句”用户注册了!“,至于谁在听、听了做什么,它完全不关心。邮件服务、审计服务、优惠券服务各自监听这个事件,各干各的活。
这就是发布-订阅模式——发布者只管发,订阅者只管收,两边互不认识。
安装和配置
NestJS 的事件模块是独立包,需要单独安装:
npm install @nestjs/event-emitter
安装好后,在根模块里引入:
import { Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';
@Module({
imports: [
EventEmitterModule.forRoot(),
],
})
export class AppModule {}
forRoot() 会初始化事件发射器,并在应用启动时自动扫描注册所有事件监听器。
如果你想精细控制,可以传配置对象:
EventEmitterModule.forRoot({
wildcard: true, // 开启通配符,后面会用到
delimiter: '.', // 命名空间分隔符,默认就是 '.'
maxListeners: 10, // 单个事件最大监听器数量
verboseMemoryLeak: true, // 监听器超限时打印警告
ignoreErrors: false, // 事件处理报错时是否忽略
});
Note底层用的是
eventemitter2这个库,功能比 Node.js 自带的 EventEmitter 强不少,支持通配符、命名空间这些特性。
定义事件类
事件本身就是一个普通的数据载体。推荐用 class 来定义,这样类型清晰、IDE 友好:
export class UserCreatedEvent {
constructor(
public readonly userId: number,
public readonly email: string,
public readonly name: string,
) {}
}
Tip事件类的属性用
readonly修饰,表示事件一旦发出就不应该被修改。这是一个好习惯——事件是”已经发生的事实”,监听器不应该去篡改它。
发布事件
在任何服务里,只要注入 EventEmitter2 就能发事件:
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UserCreatedEvent } from './events/user-created.event';
@Injectable()
export class UsersService {
constructor(private eventEmitter: EventEmitter2) {}
async create(createUserDto: CreateUserDto) {
const user = await this.userRepository.create(createUserDto);
// 用户创建好了,广播出去
this.eventEmitter.emit(
'user.created',
new UserCreatedEvent(user.id, user.email, user.name),
);
return user;
}
}
emit 是同步的——它会等所有监听器执行完才返回。如果你不想阻塞,可以用 emitAsync:
await this.eventEmitter.emitAsync(
'user.created',
new UserCreatedEvent(user.id, user.email, user.name),
);
emitAsync 会返回一个 Promise,在所有异步监听器完成后 resolve。
监听事件
监听事件用 @OnEvent() 装饰器。把它标在方法上,写上要监听的事件名就行:
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { UserCreatedEvent } from './events/user-created.event';
@Injectable()
export class EmailService {
@OnEvent('user.created')
handleUserCreated(event: UserCreatedEvent) {
console.log(`给 ${event.email} 发欢迎邮件`);
// 发邮件的逻辑...
}
}
这里有个坑要提醒一下——事件监听器不能是请求作用域的。因为监听器在应用启动时就注册了,跟具体的请求无关。
异步监听器
如果监听器里有异步操作(发邮件、写数据库),加上 async 关键字:
@OnEvent('user.created')
async handleUserCreated(event: UserCreatedEvent) {
await this.emailService.sendWelcome(event.email, event.name);
await this.auditService.log('user.created', event);
}
你也可以在装饰器里声明异步选项:
@OnEvent('user.created', { async: true })
async handleUserCreated(event: UserCreatedEvent) {
// ...
}
通配符监听
开启 wildcard: true 后,你可以用通配符来批量监听事件。
单级通配符 * 匹配一个命名层级:
@OnEvent('user.*')
handleAllUserEvents(event: any) {
// 匹配 user.created、user.updated、user.deleted
// 但不匹配 user.profile.changed(两级)
}
多级通配符 ** 匹配任意层级:
@OnEvent('**')
handleEverything(event: any) {
// 匹配所有事件,包括 user.profile.changed
}
Tip
**很适合做全局日志——所有事件都过一遍,统一记录。但生产环境慎用,事件量大的时候性能开销不小。
事件命名规范
事件名推荐用 领域.动作 的格式,用 . 分隔命名空间:
// 推荐
'user.created'
'order.completed'
'payment.failed'
// 不推荐
'userCreated' // 没有命名空间
'onUserCreated' // 带了 on 前缀,多余
'USER_CREATED' // 大写不够直观
这样命名的好处是,通配符监听时可以按领域过滤:
// 只关心订单相关的事件
@OnEvent('order.*')
handleOrderEvents(event: any) { }
监听器选项
@OnEvent 的第二个参数可以传一些配置:
@OnEvent('user.created', {
async: true, // 异步执行
prependListener: true, // 插到监听器队列最前面
suppressErrors: false, // 报错时不吞掉异常
})
async handleUserCreated(event: UserCreatedEvent) {
// ...
}
prependListener 比较实用——默认监听器是按注册顺序排的,如果你希望某个监听器优先执行,就把它设成 true。
防止事件丢失
这里有个很多人踩过的坑:如果你在 onModuleInit 或构造函数里发事件,可能会丢事件。
原因是事件监听器的注册发生在 onApplicationBootstrap 生命周期。如果事件在这之前发出,监听器还没就位,自然就收不到。
解决办法是等监听器都准备好再发:
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class SomeService implements OnApplicationBootstrap {
constructor(private eventEmitter: EventEmitter2) {}
async onApplicationBootstrap() {
// 此时所有监听器都已注册完毕,可以安全发事件了
this.eventEmitter.emit('app.started', { timestamp: Date.now() });
}
}
Note只有在你需要在启动阶段发事件时才需要注意这个问题。正常业务逻辑中的事件发布不受影响。
错误处理
事件监听器里抛异常时,默认行为取决于 suppressErrors 配置。如果设为 true(默认),异常会被静默吞掉。
建议显式设为 false,并在监听器里做好错误处理:
@OnEvent('user.created', { suppressErrors: false })
async handleUserCreated(event: UserCreatedEvent) {
try {
await this.emailService.sendWelcome(event.email);
} catch (error) {
this.logger.error(`发送欢迎邮件失败: ${event.email}`, error.stack);
// 根据业务决定要不要重新抛出
}
}
Tip一个事件有多个监听器时,如果
suppressErrors为false,某个监听器抛异常会中断后续监听器的执行。所以要么在监听器内部 try-catch,要么确保关键监听器用prependListener排在前面。
最佳实践总结
-
事件类用 class 定义,属性用
readonly,保证不可变。 -
事件名用点号分隔,格式
领域.动作,方便通配符过滤。 -
监听器里做好错误处理,别让一个监听器的失败影响其他监听器。
-
不要在监听器里再发同名事件,会造成无限循环。
-
启动阶段发事件要小心,确保监听器已注册完毕。
-
事件是”已发生的事实”,不要在事件处理中修改事件数据。