事件发射器
本指南介绍 AdonisJS 应用中的事件发射器。你将学到如何:
- 定义并发出类型安全的事件
- 使用回调或类注册监听器
- 在测试期间处理错误并伪造事件
概述
事件发射器在 AdonisJS 应用中实现事件驱动架构。当你发出一个事件时,所有已注册的监听器会异步执行,而不会阻塞触发该事件的代码。这种模式对于将副作用与主应用逻辑解耦很有用。
一个常见的例子是用户注册:在创建用户账户后,你可能需要发送验证邮件、通过支付提供商配置资源,并为分析记录该注册。与其在控制器中按顺序执行所有这些任务,你可以发出单个 user:registered 事件,让各个独立的监听器分别处理各自的关注点。
AdonisJS 提供了两种定义事件的方式。基于字符串的事件使用 TypeScript 模块增强来实现类型安全,而基于类的事件将事件标识符和数据封装在单个类中。
如果你想查看 AdonisJS 及其官方包所发出的事件列表,请参阅 事件参考指南 。
定义事件和事件数据
一个事件由两部分组成:一个标识符和相关联的数据。标识符通常是一个字符串,如 user:registered,而数据则是你想传递给监听器的任何载荷(例如 User 模型的一个实例)。
基于类的事件将标识符和数据封装在单个类中。类本身充当标识符,而类的实例持有事件数据。这种方式无需额外配置即可提供内置的类型安全。
基于字符串的事件
基于字符串的事件使用字符串标识符,如 user:registered 或 order:shipped。为了使这些事件类型安全,你要使用 TypeScript 模块增强来定义事件名称及其载荷类型。
-
定义事件类型
创建一个
types/events.ts文件,并增强EventsList接口以声明你的事件及其载荷类型。types/events.tsimport User from '#models/user' declare module '@adonisjs/core/types' { interface EventsList { 'user:registered': User } }EventsList接口将事件名称映射到它们的载荷类型。在本示例中,user:registered事件携带一个User模型实例作为其载荷。当你发出事件或注册监听器时,TypeScript 会强制执行此契约。 -
监听事件
创建一个预加载文件来注册你的事件监听器。运行以下命令生成该文件。
node ace make:preload events这会创建
start/events.ts,它在应用启动时会被自动加载。使用emitter.on方法注册监听器。start/events.tsimport emitter from '@adonisjs/core/services/emitter' emitter.on('user:registered', function (user) { console.log(user.email) })监听器回调会接收事件载荷作为其参数。由于你在
EventsList中定义了载荷类型,TypeScript 知道user是User模型的一个实例。 -
发出事件
使用
emitter.emit从应用中的任何地方发出事件。第一个参数是事件名称,第二个参数是载荷。app/controllers/users_controller.tsimport 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 会报错。
基于类的事件
基于类的事件无需模块增强即可提供类型安全。事件类既充当标识符,又充当事件数据的容器。
-
创建事件类
使用
make:event命令生成一个事件类。node ace make:event UserRegistered这会创建一个继承
BaseEvent的事件类。通过构造函数接收事件数据,并将其作为实例属性暴露。app/events/user_registered.tsimport { BaseEvent } from '@adonisjs/core/events' import User from '#models/user' export default class UserRegistered extends BaseEvent { constructor(public user: User) { super() } }事件类没有行为。它纯粹是一个数据容器,其中构造函数参数定义了事件携带哪些数据。
-
监听事件
从
#generated/eventsbarrel 文件 导入事件类,并将其作为emitter.on的第一个参数。start/events.tsimport emitter from '@adonisjs/core/services/emitter' import { events } from '#generated/events' emitter.on(events.UserRegistered, function (event) { console.log(event.user.email) })监听器会接收事件类的一个实例。通过你在构造函数中定义的实例属性来访问事件数据。
-
派发事件
基于类的事件使用静态
dispatch方法派发,而不是emitter.emit。app/controllers/users_controller.tsimport 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。
import emitter from '@adonisjs/core/services/emitter'
emitter.on('user:registered', function (user) {
console.log(`New user: ${user.email}`)
})
同样的方式也适用于基于类的事件。
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 方法的类,该方法在事件触发时执行。
export default class SendVerificationEmail {
async handle() {
// Send email
}
}
更新 handle 方法以接收事件载荷。对于基于类的事件,将参数类型标注为事件类。
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 文件
导入监听器来注册它。
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 装饰器通过构造函数注入依赖。
另见 依赖注入指南 。
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 方法在一次调用中为单个事件注册多个监听器。
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)
})
这对于日志记录、调试,或实现适用于所有事件的横切关注点很有用。
取消订阅事件
当不再需要监听器时,你可以移除它们。
使用取消订阅函数
on 和 once 方法返回一个取消订阅函数。调用它以移除监听器。
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 事件,这可能使你的应用崩溃或导致意外行为。
定义一个全局错误处理器,以捕获并处理来自所有监听器的错误。
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: '[email protected]',
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。