钩子
如果你熟悉vue或react,你应该对钩子概念有或多或少的认识,它是函数式编程的产物。react钩子主要依赖组件同步调用机制将节点和当前执行的钩子进行隐式绑定,从而能实现组件级别的状态管理,副作用清理,组件实例绑定等,每个页面都可以视作生命周期复杂的微应用。
和前端框架不同的是,ov0的插件主要对高频事件进行管理和调度,并且按照业务最小单元原则将事件注册组合到一个模块中,构成了组件。一般而言,插件组件并不像前端页面那样频繁切换导致出现复杂的生命周期,因此ov0插件的每个组件的生命周期一般只有插件系统启动与卸载,或客户端用户主动启用或卸载组件,但是钩子对组件的功能扩展还是有必要的。同时,事件一般需要在组件内进行注册,因此又需要一套扩展接口来对高频的事件进行管理和扩展,显而易见,ov0的钩子主要分为了组件级钩子和事件级钩子。
组件级钩子
通常情况下ov0的函数式组件的结构如下:
import { plugin } from './plugin'
import { useRes,useEffect } from 'ov0'
import { Onebot11Adapter } from 'ov0-plugin-adapter-onebot11'
export default plugin.defineListener<Onebot11Adapter>({
name: '监听器测试',
adapters: ['onebot11']
}, ctx => {
const { hooks, useEventHooks } = ctx.createHooks({
useRes,
useEffect
})
hooks.useEffect(()=>{
ctx.logger('监听器测试加载成功!')
},[])
ctx.on('message', {
name: '监听消息测试',
description: '发送 #测试 查看效果',
regExp: /#测试/
}, e => {
e.reply('hello world!')
})
})
这里通过ctx.createHooks创建的钩子被称为组件级钩子,返回的对象的hooks属性是无需传递上下文参数的集合,可以向react一样直接调用,例如hooks.useEffect可以组件挂载时执行回函数、根据依赖项重复执行回调函数或返回一个函数来清理副作用。
事件级钩子
在一般业务中,我们更多的会用到事件级钩子:
基于前面的提到的组件我们仅需在组件内注册事件监听回调:
import { plugin } from './plugin'
import { useRes,useEffect } from 'ov0'
import { Onebot11Adapter } from 'ov0-plugin-adapter-onebot11'
export default plugin.defineListener<Onebot11Adapter>({
name: '监听器测试',
adapters: ['onebot11']
}, ctx => {
const { hooks, useEventHooks } = ctx.createHooks({
useRes,
useEffect
})
hooks.useEffect(()=>{
ctx.logger('监听器测试加载成功!')
},[])
ctx.on('message', {
name: '监听消息测试',
description: '发送 #测试 查看效果',
regExp: /#测试/
}, useEventHooks(async (e, {useRes}) => {
const res = useRes()
res.text('hello world').send()
}))
})
这里我门将ctx.on的普通回调函数替换成了useEventHooks回调函数以支持useRes能隐式获取事件上下文。至此可以看出,ov0插件的钩子定义时需要预留上下文形参以支持在组件内注入。
以useRes为例,其定义方式如下:
// useRes.ts
import { defineComponentHook,sharedSegment } from 'ov0';
// 基本格式
export const useRes = defineComponentHook(({ ctx, e }) => {
// 闭包引用上下文返回实际钩子
return () => {
return Object.assign({
message: [] as OB11Segment[],
end() {
const messageElems = this.message
this.message = []
return messageElems
},
send() {
if (!e) return
if ('reply' in e) {
const response = e.reply(this.message)
this.message = []
return response
}
}
}, sharedSegment);
}
})
通过defineComponentHook定义的钩子需要传入携带上下文的回调函数,并返回一个可供调用的实际钩子函数,这样定义的优势在于组件中仅需通过ctx.createHooks传递一次钩子函数即可在整个组件中调用,而不需要在每次调用钩子时都需要传递上下文参数ctx或事件e:
ctx.on('message', {
name: '监听消息测试',
description: '发送 #测试 查看效果',
regExp: /#测试/
}, e => {
const res = useRes({e})
// 或者
const [state,setState] = useState({e},'hello')
const [state,setState] = useState({e},'hello')
})
这里可以看出如果useRes是个普通函数那么每次调用时都需传递参数e
你可能疑惑为什么ov0的钩子无法像react一样无需显示传递上下文,核心原因如下:
- react组件严格同步执行,在全局维护了一个组件指针,当某钩子被调用时钩子内访问的全局指针正确映射到当前组件
- ov0插件的事件级钩子所处的回调函数异步并行执行,旨在提高事件并发时性能,无法通过全局指针隐式绑定当前事件