事件发射器

本指南介绍 AdonisJS 应用中的事件发射器。你将学到如何:

  • 定义并发出类型安全的事件
  • 使用回调或类注册监听器
  • 在测试期间处理错误并伪造事件

概述

事件发射器在 AdonisJS 应用中实现事件驱动架构。当你发出一个事件时,所有已注册的监听器会异步执行,而不会阻塞触发该事件的代码。这种模式对于将副作用与主应用逻辑解耦很有用。

一个常见的例子是用户注册:在创建用户账户后,你可能需要发送验证邮件、通过支付提供商配置资源,并为分析记录该注册。与其在控制器中按顺序执行所有这些任务,你可以发出单个 user:registered 事件,让各个独立的监听器分别处理各自的关注点。

AdonisJS 提供了两种定义事件的方式。基于字符串的事件使用 TypeScript 模块增强来实现类型安全,而基于类的事件将事件标识符和数据封装在单个类中。

Note

如果你想查看 AdonisJS 及其官方包所发出的事件列表,请参阅 事件参考指南

定义事件和事件数据

一个事件由两部分组成:一个标识符和相关联的数据。标识符通常是一个字符串,如 user:registered,而数据则是你想传递给监听器的任何载荷(例如 User 模型的一个实例)。

基于类的事件将标识符和数据封装在单个类中。类本身充当标识符,而类的实例持有事件数据。这种方式无需额外配置即可提供内置的类型安全。

基于字符串的事件

基于字符串的事件使用字符串标识符,如 user:registeredorder:shipped。为了使这些事件类型安全,你要使用 TypeScript 模块增强来定义事件名称及其载荷类型。

  1. 定义事件类型

    创建一个 types/events.ts 文件,并增强 EventsList 接口以声明你的事件及其载荷类型。

    types/events.ts
    import User from '#models/user'
    
    declare module '@adonisjs/core/types' {
      interface EventsList {
        'user:registered': User
      }
    }

    EventsList 接口将事件名称映射到它们的载荷类型。在本示例中,user:registered 事件携带一个 User 模型实例作为其载荷。当你发出事件或注册监听器时,TypeScript 会强制执行此契约。

  2. 监听事件

    创建一个预加载文件来注册你的事件监听器。运行以下命令生成该文件。

    node ace make:preload events

    这会创建 start/events.ts,它在应用启动时会被自动加载。使用 emitter.on 方法注册监听器。

    start/events.ts
    import emitter from '@adonisjs/core/services/emitter'
    
    emitter.on('user:registered', function (user) {
      console.log(user.email)
    })

    监听器回调会接收事件载荷作为其参数。由于你在 EventsList 中定义了载荷类型,TypeScript 知道 userUser 模型的一个实例。

  3. 发出事件

    使用 emitter.emit 从应用中的任何地方发出事件。第一个参数是事件名称,第二个参数是载荷。

    app/controllers/users_controller.ts
    import emitter from '@adonisjs/core/services/emitter'
    import User from '#models/user'
    
    export default class UsersController {
      async store({ request }: HttpContext) {
        const data = request.only(['email', 'password'])
        const user = await User.create(data)
    
        emitter.emit('user:registered', user)
        return user
      }
    }

    emitter.emit 方法是类型安全的。如果你传入了错误的载荷类型,或使用了 EventsList 中未定义的事件名称,TypeScript 会报错。

基于类的事件

基于类的事件无需模块增强即可提供类型安全。事件类既充当标识符,又充当事件数据的容器。

  1. 创建事件类

    使用 make:event 命令生成一个事件类。

    node ace make:event UserRegistered

    这会创建一个继承 BaseEvent 的事件类。通过构造函数接收事件数据,并将其作为实例属性暴露。

    app/events/user_registered.ts
    import { BaseEvent } from '@adonisjs/core/events'
    import User from '#models/user'
    
    export default class UserRegistered extends BaseEvent {
      constructor(public user: User) {
        super()
      }
    }

    事件类没有行为。它纯粹是一个数据容器,其中构造函数参数定义了事件携带哪些数据。

  2. 监听事件

    #generated/events barrel 文件 导入事件类,并将其作为 emitter.on 的第一个参数。

    start/events.ts
    import emitter from '@adonisjs/core/services/emitter'
    import { events } from '#generated/events'
    
    emitter.on(events.UserRegistered, function (event) {
      console.log(event.user.email)
    })

    监听器会接收事件类的一个实例。通过你在构造函数中定义的实例属性来访问事件数据。

  3. 派发事件

    基于类的事件使用静态 dispatch 方法派发,而不是 emitter.emit

    app/controllers/users_controller.ts
    import User from '#models/user'
    import { events } from '#generated/events'
    
    export default class UsersController {
      async store({ request }: HttpContext) {
        const data = request.only(['email', 'password'])
        const user = await User.create(data)
    
        events.UserRegistered.dispatch(user)
        return user
      }
    }

    dispatch 方法接受与事件类构造函数相同的参数。由于类本身提供了完整的类型信息,因此无需在 EventsList 中定义类型。

监听器

监听器可以定义为行内回调,也可以定义为专门的监听器类。行内回调适用于简单逻辑,而监听器类更适合能受益于依赖注入和可测试性的复杂操作。

行内回调

对于简单的监听器,将一个函数直接传给 emitter.on

start/events.ts
import emitter from '@adonisjs/core/services/emitter'

emitter.on('user:registered', function (user) {
  console.log(`New user: ${user.email}`)
})

同样的方式也适用于基于类的事件。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'
import { events } from '#generated/events'

emitter.on(events.UserRegistered, function (event) {
  console.log(`New user: ${event.user.email}`)
})

监听器类

使用 make:listener 命令创建一个监听器类。

node ace make:listener SendVerificationEmail

这会生成一个带有 handle 方法的类,该方法在事件触发时执行。

app/listeners/send_verification_email.ts
export default class SendVerificationEmail {
  async handle() {
    // Send email
  }
}

更新 handle 方法以接收事件载荷。对于基于类的事件,将参数类型标注为事件类。

app/listeners/send_verification_email.ts
import { events } from '#generated/events'

export default class SendVerificationEmail {
  async #sendEmail(to: string) {
  }

  async handle(event: events.UserRegistered) {
    await this.#sendEmail(event.user.email)
  }
}

通过从 #generated/listeners barrel 文件 导入监听器来注册它。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'
import { events } from '#generated/events'
import { listeners } from '#generated/listeners'

emitter.on(events.UserRegistered, [listeners.SendVerificationEmail])

监听器中的依赖注入

监听器类通过 IoC 容器实例化,因此你可以使用 @inject 装饰器通过构造函数注入依赖。

另见 依赖注入指南

app/listeners/send_verification_email.ts
import { inject } from '@adonisjs/core'
import { events } from '#generated/events'
import TokensService from '#services/tokens_service'

@inject()
export default class SendVerificationEmail {
  constructor(protected tokensService: TokensService) {}

  async handle(event: events.UserRegistered) {
    const token = this.tokensService.generate(event.user.email)
  }
}

监听方法

发射器提供了几种注册监听器的方法,每种都适合不同的用例。

使用 on 的持久监听器

on 方法注册一个监听器,它会在整个应用生命周期内每次事件被发出时触发。

emitter.on('user:registered', function (user) {
  // Runs every time the event fires
})

使用 once 的一次性监听器

once 方法注册一个只触发一次的监听器,之后会自动取消订阅。

emitter.once('user:registered', function (user) {
  // Runs only the first time the event fires
})

使用 listen 的多个监听器

listen 方法在一次调用中为单个事件注册多个监听器。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'
import { events } from '#generated/events'
import { listeners } from '#generated/listeners'

emitter.listen(events.UserRegistered, [
  listeners.SendVerificationEmail,
  listeners.RegisterWithPaymentProvider,
  listeners.ProvisionAccount,
])

当事件触发时,所有监听器会并行执行。

使用 onAny 的通配符监听器

onAny 方法注册一个监听器,它会为应用中发出的每个事件触发。

emitter.onAny((name, event) => {
  console.log(`Event fired: ${name}`)
  console.log(event)
})

这对于日志记录、调试,或实现适用于所有事件的横切关注点很有用。

取消订阅事件

当不再需要监听器时,你可以移除它们。

使用取消订阅函数

ononce 方法返回一个取消订阅函数。调用它以移除监听器。

import emitter from '@adonisjs/core/services/emitter'

const unsubscribe = emitter.on('user:registered', () => {
})

unsubscribe()

使用 off 方法

off 方法移除特定的监听器。你需要一个指向已注册的确切函数或类的引用。

import emitter from '@adonisjs/core/services/emitter'

function sendEmail() {
}

emitter.on('user:registered', sendEmail)
emitter.off('user:registered', sendEmail)

这同样适用于监听器类。

import emitter from '@adonisjs/core/services/emitter'
import { events } from '#generated/events'
import { listeners } from '#generated/listeners'

emitter.on(events.UserRegistered, listeners.SendVerificationEmail)
emitter.off(events.UserRegistered, listeners.SendVerificationEmail)

清除监听器

使用 clearListeners 移除特定事件的所有监听器。

emitter.clearListeners('user:registered')
emitter.clearListeners(events.UserRegistered)

使用 clearAllListeners 移除所有事件的所有监听器。

emitter.clearAllListeners()

错误处理

当一个监听器抛出错误时,由于监听器并行运行,它不会影响其他监听器。然而,未处理的错误会触发 Node.js 的 unhandledRejection 事件,这可能使你的应用崩溃或导致意外行为。

定义一个全局错误处理器,以捕获并处理来自所有监听器的错误。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'

emitter.onError((event, error, eventData) => {
  console.error(`Error in listener for ${event}:`, error)
  // Report to error tracking service
})

错误处理器接收三个参数:事件名称(或类)、被抛出的错误,以及传递给监听器的事件数据。

在测试期间伪造事件

在测试发出事件的代码时,你常常想验证事件已被发出,而无需实际运行监听器。例如,在测试用户注册时,你可能想确认 user:registered 事件已触发,而不发送真实的邮件。

emitter.fake 方法会阻止监听器运行,并返回一个 EventBuffer,用于捕获已发出的事件以供断言。

import emitter from '@adonisjs/core/services/emitter'
import { events } from '#generated/events'

test.group('User signup', () => {
  test('create a user account', async ({ client }) => {
    using fakeEmitter = emitter.fake()

    await client
      .post('signup')
      .form({
        email: 'foo@bar.com',
        password: 'secret',
      })

    fakeEmitter.assertEmitted(events.UserRegistered)
  })
})

using 关键字会在变量超出作用域时(即测试函数结束时)自动恢复发射器。如果你需要对恢复时机有更多控制,也可以手动调用 emitter.restore()

伪造特定事件

fake 传入事件名称或类,以仅伪造特定事件。其他事件将继续正常触发它们的监听器。

emitter.fake('user:registered')
emitter.fake([events.UserRegistered, events.OrderUpdated])

断言

fake 返回的 EventBuffer 提供了几种断言方法。

const fakeEmitter = emitter.fake()

// Assert an event was emitted
fakeEmitter.assertEmitted('user:registered')
fakeEmitter.assertEmitted(events.UserRegistered)

// Assert an event was not emitted
fakeEmitter.assertNotEmitted(events.OrderUpdated)

// Assert an event was emitted a specific number of times
fakeEmitter.assertEmittedCount(events.OrderUpdated, 1)

// Assert no events were emitted at all
fakeEmitter.assertNoneEmitted()

条件断言

assertEmitted 传入一个回调,以断言事件是带着特定数据被发出的。

fakeEmitter.assertEmitted(events.OrderUpdated, ({ data }) => {
  /**
   * Only consider the event as emitted if
   * the orderId matches
   */
  return data.order.id === orderId
})

回调会接收事件数据,如果事件符合你的条件,则应返回 true