事件发射器(Event Emitter)
AdonisJS 拥有一个构建于 emittery 之上的内置事件发射器。Emittery 会异步派发事件,并修复了 Node.js 默认事件发射器的许多常见问题。
AdonisJS 进一步以额外的特性增强了 emittery。
- 通过定义事件列表及其关联的数据类型,提供静态类型安全。
- 支持基于类的事件和监听器。将监听器移至专门的文件中,可以让你的代码库保持整洁且易于测试。
- 能够在测试期间伪造(fake)事件。
基本用法
事件监听器定义在 start/events.ts 文件中。你可以使用 make:preload ace 命令创建该文件。
node ace make:preload events
你必须使用 emitter.on 来监听一个事件。该方法接受事件名称作为第一个参数,监听器作为第二个参数。
import emitter from '@adonisjs/core/services/emitter'
emitter.on('user:registered', function (user) {
console.log(user)
})
一旦定义好事件监听器,你就可以使用 emitter.emit 方法发射 user:registered 事件。它接受事件名称作为第一个参数,事件数据作为第二个参数。
import emitter from '@adonisjs/core/services/emitter'
export default class UsersController {
async store() {
const user = await User.create(data)
emitter.emit('user:registered', user)
}
}
你可以使用 emitter.once 来只监听一次事件。
emitter.once('user:registered', function (user) {
console.log(user)
})
使事件类型安全
AdonisJS 强制要求你为应用中想要发射的每个事件定义静态类型。类型注册在 types/events.ts 文件中。
在以下示例中,我们将 User 模型注册为 user:registered 事件的数据类型。
如果你觉得为每个事件定义类型很繁琐,可以改用基于类的事件。
import User from '#models/User'
declare module '@adonisjs/core/types' {
interface EventsList {
'user:registered': User
}
}
基于类的监听器
与 HTTP 控制器类似,监听器类提供了一个抽象层,用于将内联的事件监听器移入专门的文件中。监听器类存储在 app/listeners 目录中,你可以使用 make:listener 命令创建一个新的监听器。
node ace make:listener sendVerificationEmail
监听器文件会以 class 声明和 handle 方法为骨架生成。在这个类中,你可以定义额外的方法来监听多个事件(如有需要)。
import User from '#models/user'
export default class SendVerificationEmail {
handle(user: User) {
// Send email
}
}
作为最后一步,你必须在 start/events.ts 文件中将监听器类绑定到一个事件。你可以使用 #listeners 别名导入监听器。这些别名使用 Node.js 的子路径导入特性定义。
import emitter from '@adonisjs/core/services/emitter'
import SendVerificationEmail from '#listeners/send_verification_email'
emitter.on('user:registered', [SendVerificationEmail, 'handle'])
延迟加载监听器
建议延迟加载监听器以加快应用的启动阶段。延迟加载非常简单,只需将 import 语句放到一个函数后面,并使用动态导入即可。
import emitter from '@adonisjs/core/services/emitter'
import SendVerificationEmail from '#listeners/send_verification_email'
const SendVerificationEmail = () => import('#listeners/send_verification_email')
emitter.on('user:registered', [SendVerificationEmail, 'handle'])
依赖注入
你不能在监听器类中注入 HttpContext。因为事件是异步处理的,监听器可能会在 HTTP 请求完成后才运行。
监听器类使用 IoC 容器实例化;因此,你可以在类的构造函数或处理事件的方法中,通过类型提示(type-hint)声明依赖。
在以下示例中,我们将 TokensService 作为构造函数参数进行类型提示。调用此监听器时,IoC 容器将注入一个 TokensService 类的实例。
import { inject } from '@adonisjs/core'
import TokensService from '#services/tokens_service'
@inject()
export default class SendVerificationEmail {
constructor(protected tokensService: TokensService) {}
handle(user: User) {
const token = this.tokensService.generate(user.email)
}
}
在以下示例中,我们将 TokensService 注入到 handle 方法内。不过请记住,第一个参数始终是事件的载荷(payload)。
import { inject } from '@adonisjs/core'
import TokensService from '#services/tokens_service'
import UserRegistered from '#events/user_registered'
export default class SendVerificationEmail {
@inject()
handle(event: UserRegistered, tokensService: TokensService) {
const token = tokensService.generate(event.user.email)
}
}
基于类的事件
一个事件是事件标识符(通常是基于字符串的事件名称)与关联数据的组合。例如:
user:registered是事件标识符(也就是事件名称)。- User 模型的一个实例是事件数据。
基于类的事件将事件标识符和事件数据封装在同一个类中。类的构造函数充当标识符,而类的实例持有事件数据。
你可以使用 make:event 命令创建一个事件类。
node ace make:event UserRegistered
事件类没有行为。相反,它是事件的数据容器。你可以通过类的构造函数接受事件数据,并将其作为实例属性提供。
import { BaseEvent } from '@adonisjs/core/events'
import User from '#models/user'
export default class UserRegistered extends BaseEvent {
constructor(public user: User) {
super()
}
}
监听基于类的事件
你可以使用 emitter.on 方法为基于类的事件附加监听器。第一个参数是事件类的引用,其后是监听器。
import emitter from '@adonisjs/core/services/emitter'
import UserRegistered from '#events/user_registered'
emitter.on(UserRegistered, function (event) {
console.log(event.user)
})
在以下示例中,我们同时使用基于类的事件和监听器。
import emitter from '@adonisjs/core/services/emitter'
import UserRegistered from '#events/user_registered'
const SendVerificationEmail = () => import('#listeners/send_verification_email')
emitter.on(UserRegistered, [SendVerificationEmail])
发射基于类的事件
你可以使用 static dispatch 方法发射基于类的事件。dispatch 方法接受事件类构造函数所接受的参数。
import User from '#models/user'
import UserRegistered from '#events/user_registered'
export default class UsersController {
async store() {
const user = await User.create(data)
/**
* Dispatch/emit the event
*/
UserRegistered.dispatch(user)
}
}
简化事件监听体验
如果你决定使用基于类的事件和监听器,你可以使用 emitter.listen 方法来简化绑定监听器的过程。
emitter.listen 方法接受事件类作为第一个参数,以及一个基于类的监听器数组作为第二个参数。此外,注册的监听器必须有一个 handle 方法来处理事件。
import emitter from '@adonisjs/core/services/emitter'
import UserRegistered from '#events/user_registered'
emitter.listen(UserRegistered, [
() => import('#listeners/send_verification_email'),
() => import('#listeners/register_with_payment_provider'),
() => import('#listeners/provision_account')
])
处理错误
默认情况下,监听器抛出的异常将导致 unhandledRejection。因此,建议使用 emitter.onError 方法自行捕获并处理错误。
emitter.onError 方法接受一个回调,该回调接收事件名称、错误和事件数据。
import emitter from '@adonisjs/core/services/emitter'
emitter.onError((event, error, eventData) => {
})
监听所有事件
你可以使用 emitter.onAny 方法监听所有事件。该方法接受监听器回调作为唯一参数。
import emitter from '@adonisjs/core/services/emitter'
emitter.onAny((name, event) => {
console.log(name)
console.log(event)
})
取消订阅事件
emitter.on 方法返回一个取消订阅函数,你可以调用它来移除事件监听器的订阅。
import emitter from '@adonisjs/core/services/emitter'
const unsubscribe = emitter.on('user:registered', () => {})
// Remove the listener
unsubscribe()
你也可以使用 emitter.off 方法移除事件监听器的订阅。该方法接受事件名称/类作为第一个参数,以及对监听器的引用作为第二个参数。
import emitter from '@adonisjs/core/services/emitter'
function sendEmail () {}
// Listen for event
emitter.on('user:registered', sendEmail)
// Remove listener
emitter.off('user:registered', sendEmail)
emitter.offAny
emitter.offAny 移除一个监听所有事件的通配符监听器。
emitter.offAny(callback)
emitter.clearListeners
emitter.clearListeners 方法移除给定事件的所有监听器。
//String-based event
emitter.clearListeners('user:registered')
//Class-based event
emitter.clearListeners(UserRegistered)
emitter.clearAllListeners
emitter.clearAllListeners 方法移除所有事件的所有监听器。
emitter.clearAllListeners()
可用事件列表
请查看事件参考指南以查看可用事件的列表。
在测试期间伪造事件
事件监听器常用于在某个动作之后执行副作用。例如:向新注册的用户发送邮件,或在订单状态更新后发送通知。
在编写测试时,你可能希望避免向测试期间创建的用户发送邮件。
你可以通过伪造事件发射器来禁用事件监听器。在以下示例中,我们在发起注册用户的 HTTP 请求之前调用 emitter.fake。请求之后,我们使用 events.assertEmitted 方法来确保 SignupController 发射了特定的事件。
import emitter from '@adonisjs/core/services/emitter'
import UserRegistered from '#events/user_registered'
test.group('User signup', () => {
test('create a user account', async ({ client, cleanup }) => {
const events = emitter.fake()
cleanup(() => {
emitter.restore()
})
await client
.post('signup')
.form({
email: 'foo@bar.com',
password: 'secret',
})
})
// Assert the event was emitted
events.assertEmitted(UserRegistered)
})
event.fake方法返回一个 EventBuffer 类的实例,你可以用它进行断言和查找已发射的事件。emitter.restore方法恢复伪造。恢复伪造后,事件将正常发射。
伪造特定事件
如果你调用 emitter.fake 方法时不带任何参数,它会伪造所有事件。如果你想伪造特定的事件,请将事件名称或类作为第一个参数传递。
// Fakes only the user:registered event
emitter.fake('user:registered')
// Fake multiple events
emitter.fake([UserRegistered, OrderUpdated])
多次调用 emitter.fake 方法将移除旧的伪造。
事件断言
你可以使用 assertEmitted、assertNotEmitted、assertNoneEmitted 和 assertEmittedCount 方法为伪造的事件编写断言。
assertEmitted 和 assertNotEmitted 方法接受事件名称或类构造函数作为第一个参数,以及一个可选的查找函数,该函数必须返回一个布尔值以标记事件是否已发射。
const events = emitter.fake()
events.assertEmitted('user:registered')
events.assertNotEmitted(OrderUpdated)
events.assertEmitted(OrderUpdated, ({ data }) => {
/**
* Only consider the event as emitted, if
* the orderId matches
*/
return data.order.id === orderId
})
// Assert count of a specific event
events.assertEmittedCount(OrderUpdated, 1)
// Assert no events were emitted
events.assertNoneEmitted()