什么是 ocev
ocev 是一个事件库,设计目的是为了简化事件处理的复杂性,同时支持 promise/stream 的方式处理事件.
支持代理 web 元素的所有事件,并用 ocev 的 api 进行处理.
所有 api 都最大化的支持 typescript,提供最完整的类型提示
安装
bash
npm install ocev
基本使用
下面是 ocev 最基本的使用方法
typescript
import { SyncEvent } from "ocev"
// 定义事件的类型
type EventHandlerMap = {
event1: (arg1: string, arg2: number) => void
event2: (arg1: number, arg2: string) => void
}
// 实例化
const syncEvent = SyncEvent.new<EventHandlerMap>()
queueMicrotask(() => {
// 触发事件
syncEvent.emit("event1", "1", 2)
syncEvent.emit("event2", 3, "4")
})
// 监听事件
const cancel = syncEvent
.on("event1", (arg1, arg2) => {})
.once("event2", (arg1, arg2) => {})
.on("event1", (arg1, arg2) => {}, {
debounce: { // 防抖
waitMs: 200,
maxWaitMs: 500,
},
})
// 取消注册
// cancel()
// 等待事件触发 Promise
await syncEvent.waitUtil("event1", { timeout: 1000 })
// 创建事件流 Stream
const eventStream = syncEvent.createEventStream(["event1", "event2"])
ocev 的特点
通过上面的示例可以看到 ocev 本质上是一个 (pub/sub)库 , 但是 ocev 还可以代理 web element 所有的事件 , 使用 ocev 可以用promise/stream 处理所有的事件
ocev 有两个类, SyncEvent,EventProxy, 下面的例子都是基于 EventProxy 的,EventProxy 本质上是 SyncEvent 的封装
1. 简化 web 的事件处理方式
我一直都觉得 web 的事件处理方式过于复杂,如果你在使用 react,你很有可能要写这种代码
tsx
useEffect(() => {
const callback = () => {}
target.addEventListener("event", callback)
return () => {
target.removeEventListener("event", callback)
}
}, [target])
对于多个事件,你要写这种代码
tsx
useEffect(() => {
const callback1 = () => {}
target.addEventListener("event1", callback1)
const callback2 = () => {}
target.addEventListener("event2", callback2)
// ....
return () => {
target.removeEventListener("event1", callback1)
target.removeEventListener("event2", callback2)
// ....
}
}, [target])
你注册了多少个,就要清理多少个,这写起来很繁琐
如果你是用 ocev
, 你的代码会是这样的,无限调用,一次性清理
tsx
import { EventProxy } from "ocev"
useEffect(() => {
return EventProxy.new(target)
.on("event1", (...args) => {}) // 支持完整的类型提示
.once("event2", (...args) => {})
.on("event3", (...args) => {})
}, [target])
ocev 的方法 on/once
返回的是一个函数,该函数可以继续调用方法 once,on
, 更详细的介绍请看文档
2. Promise support
考虑一个场景, 你要建立一个 websocket
连接, 并等待连接打开, 并设置最大等待连接时长,然后处理消息和异常, 为了保证正确的释放资源, 你可能会写如下的代码
typescript
async function setupWebSocket(
url: string,
successCallback: (ws: WebSocket) => void,
errorCallback: (err: Error) => void,
timeout: number
) {
const ws = new WebSocket(url)
const timeID = setTimeout(() => {
errorCallback(new Error("timeout"))
ws.removeEventListener("open", onOpen)
ws.removeEventListener("error", onError)
}, timeout)
function onOpen() {
successCallback(ws)
clearTimeout(timeID)
}
function onError() {
errorCallback(new Error("can't connect to server"))
clearTimeout(timeID)
}
ws.addEventListener("open", onOpen)
ws.addEventListener("error", onError)
}
ocev 支持 Promise 处理事件,如果用ocev来处理,代码是会是这样的
typescript
import { EventProxy } from "ocev"
async function setupWebSocket(url: string, timeout: number) {
const ws = new WebSocket(url)
// 等 'open' 事件触发或者超时抛出异常
await EventProxy.new(ws).waitUtil("open", { timeout })
// 或者等 error 事件触发 mapToError 将事件的结果映射成 error
// await EventProxy.new(ws).waitUtilRace([
// { event: "open", timeout },
// { event: "error",
// mapToError: () => new Error("websocket connect error"),
// },
// ])
return ws
}
Promise 让事件处理变得简单优雅,使用 Promise 处理代码可以让逻辑更加的清晰
更进一步,看看怎么用 ocev 来实现消息处理 (Stream)
typescript
import { EventProxy } from "ocev"
async function setupWebSocket(url: string, timeout: number) {
const ws = EventProxy.new(new WebSocket(url))
await ws.waitUtilRace([
{ event: "open", timeout },
{
event: "error",
mapToError: () => new Error("websocket connect error"),
},
])
// 转换成事件流
const eventStream = ws.createEventStream(["close", "message", "error"])
// 另外一种写法(ReadableStream)
// const readableStream = ws.createEventReadableStream(["close", "message", "error"])
// 所有事件会被推送到一个队列里面,轮流触发
for await (const { event, value } of eventStream) {
switch (event) {
case "error": {
throw Error("websocket connect error")
}
case "close": {
throw Error("websocket connection closed")
}
case "message": {
// 支持类型提示
const message = value[0].data
// handle message
break
}
default:
throw new Error("unreachable")
}
}
}
通过异步迭代器,你可以将事件转换成 stream ,在处理消息流的时候,还可以使用 策略 来丢弃一些消息
通过 Promise/Stream , 代码变得清晰可读,当你将所有的代码都转变成 async/await 之后,你可以这样处理重连的逻辑
typescript
let reconnectCount = 0
for (;;) {
try {
await setupWebSocket("", 1000)
} catch (error) {
reconnectCount += 1
}
}
如果你是要建立 WebRTC 连接,你可以这么写
typescript
import { EventProxy } from "ocev"
async function connect(timeout: number) {
const connection = new RTCPeerConnection()
await EventProxy.new(connection).waitUtil("connectionstatechange", {
timeout,
// 只有当 where 返回 true 的时候才会 resolve
where: (ev) => connection.connectionState === "connected",
})
return connection
}
3. 观察一个 web 对象的所有事件
你知道 video 在播放的时候触发了哪些事件吗?使用 ocev 可以直接观察一个 web 对象的所有事件
typescript
import { EventProxy } from "ocev"
EventProxy.new(videoDom).proxyAllEvent().any((eventName, ...args) => {
console.log(eventName)
})
放在 react
里面可以这么写
tsx
import { EventProxy } from "ocev"
import { useEffect, useRef } from "react"
function Video() {
const videoDomRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
return EventProxy.new(videoDomRef.current!, { proxyAllEvent: true }).any((eventName, ...args) => {
console.log(eventName)
})
}, [])
const url = "" // 你的 video 链接
return <video muted autoPlay src={url} ref={videoDomRef} />
}
打开控制台, 你会看到 video
触发的所有事件和它的顺序
不只是 video 元素,几乎所有 web element 都可以被 EventProxy
代理
一个示例codesandbox
更多
上面只是 ocev 的一部分 api 用法,如果你想了解更多的内容,请看sync-event 文档
如果你有好的建议和想法,欢迎 pr 和 issue , github 地址