首页 / NestJS 入门教程 / 事件机制

NestJS 入门教程

事件机制

本教程共 47 篇 · 第 32 篇 · 更新于 2026-08-09 · 约 10 分钟阅读

NestJS事件机制EventEmitter发布订阅松耦合

本节目标:学会用事件机制让模块之间”喊一嗓子”就能通知对方,不用互相引用、互相依赖。

什么是事件机制

想象一个场景:用户注册成功后,你需要发欢迎邮件、记录审计日志、送新人优惠券。

最直觉的做法是在注册方法里挨个调用这些服务。但问题来了——每加一个后续操作,你就得改一次注册方法。代码越来越长,耦合越来越重。

事件机制就是来解决这个问题的。

你可以把它理解成一个”广播站”。注册服务只管喊一句”用户注册了!“,至于谁在听、听了做什么,它完全不关心。邮件服务、审计服务、优惠券服务各自监听这个事件,各干各的活。

这就是发布-订阅模式——发布者只管发,订阅者只管收,两边互不认识。

安装和配置

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

一个事件有多个监听器时,如果 suppressErrorsfalse,某个监听器抛异常会中断后续监听器的执行。所以要么在监听器内部 try-catch,要么确保关键监听器用 prependListener 排在前面。

最佳实践总结

  1. 事件类用 class 定义,属性用 readonly,保证不可变。

  2. 事件名用点号分隔,格式 领域.动作,方便通配符过滤。

  3. 监听器里做好错误处理,别让一个监听器的失败影响其他监听器。

  4. 不要在监听器里再发同名事件,会造成无限循环。

  5. 启动阶段发事件要小心,确保监听器已注册完毕。

  6. 事件是”已发生的事实”,不要在事件处理中修改事件数据。