跳到主要内容

基本介绍

ocev 是什么?

ocev 是一个事件库,设计目的是为了简化事件处理的复杂性,同时支持 promise/stream 的方式处理事件.

支持代理 web 元素的所有事件,并用 ocev 的 api 进行处理.

所有 api 都最大化的支持 typescript,提供最完整的类型提示.

Install

  npm install ocev

基本示例

下面是 ocev 最基本的使用方法

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()

// 等待事件触发
await syncEvent.waitUtil("event1", { timeout: 1000 })

// 创建事件流
const eventStream = syncEvent.createEventStream(["event1", "event2"])

为什么要使用 ocev

通过上面的示例可以看到 ocev 本质上是一个 (pub/sub)库, 但是 ocev 还可以代理 web element 所有的事件, 使用 ocev 可以用promise/stream 处理所有的事件

提示

ocev 有两个类, SyncEvent,EventProxy, 下面的例子都是基于 EventProxy

ocev 有以下的一些特点

1. 简化 web 的事件处理方式

我一直都觉得 web 的事件处理方式过于复杂,如果你在使用 react,你很有可能要写这种代码

useEffect(() => {
const callback = () => {}
target.addEventListener("event", callback)

return () => {
target.removeEventListener("event", callback)
}
}, [target])

多个事件

useEffect(() => {
const callback1 = () => {}
target.addEventListener("event1", callback1)

const callback2 = () => {}
target.addEventListener("event2", callback2)

// ....

return () => {
target.removeEventListener("event1", callback1)
target.removeEventListener("event2", callback2)
// ....
}
}, [target])

你注册了多少个,就要清理多少个,这写起来很繁琐

如果你是用 ocev, 你的代码会是这样的,无限调用,一次性清理

import { EventProxy } from "ocev"

useEffect(() => {
return EventProxy.new(target)
.on("event1", (...args) => {}) // 支持完整的类型提示
.once("event2", (...args) => {})
.on("event3", (...args) => {})
}, [target])

ocev 的方法 on/once 返回的是一个函数,该函数可以继续调用方法 once,on, 更详细的介绍请看文档

提示

本节所有的示例都是基于 EventProxy, EventProxySyncEvent 的一种封装,关于这两者的更多信息,请看文档

2. Promise support

考虑一个场景, 你要建立一个 websocket 连接, 并等待连接打开, 并设置最大等待连接时长,然后处理消息和异常, 为了保证正确的释放资源, 你可能会写如下的代码

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来处理,代码是会是这样的

import { EventProxy } from "ocev"

async function setupWebSocket(url: string, timeout: number) {
const ws = new WebSocket(url)
// 等待 open 事件触发或者 timeout 抛出异常
await EventProxy.new(ws).waitUtil("open", { timeout })

return ws
}

Promise 让事件处理变得简单优雅,使用 Promise 处理代码可以让逻辑更加的清晰

更进一步,看看怎么用 ocev 来实现消息处理 (Stream)

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 之后,你可以这样处理重连的逻辑

let reconnectCount = 0
for (;;) {
try {
await setupWebSocket("", 1000)
} catch (error) {
reconnectCount += 1
}
}

如果你是要建立 WebRTC 连接,你可以这么写

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 对象的所有事件

import { EventProxy } from "ocev"
// 支持类型提示 !
EventProxy.new(videoDom, { proxyAllEvent: true }).any((eventName, ...args) => {
console.log(eventName)
})

放在 react 里面可以这么写

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 代理

更多

上面只是 ocev 的一部分 api 用法,如果你想了解更多的内容,请看文档sync-event

如果你有好的建议和想法,欢迎 pr 和 issue