基本介绍
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, EventProxy 是 SyncEvent 的一种封装,关于这两者的更多信息,请看文档
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
}
import { EventProxy } from "ocev"
async function setupWebSocket(url: string, timeout: number) {
  const ws = new WebSocket(url)
  // race 等待 open 事件或者 error 中任意一个先触发
  const { event } = await EventProxy.new(ws).waitUtilRace(["open", "error"])
  if (event === "error") {
    throw new Error("websocket connect error")
  }
  return ws
}
import { EventProxy } from "ocev"
async function setupWebSocket(url: string, timeout: number) {
  const ws = new WebSocket(url)
  // race 等待 open , error 中一个触发然后resolve,timeout 则 reject 异常
  const { event } = await EventProxy.new(ws).waitUtilRace([
    { event: "open", timeout },
    "error",
  ])
  if (event === "error") {
    throw new Error("websocket connect error")
  }
  return ws
}
import { EventProxy } from "ocev"
async function setupWebSocket(url: string, timeout: number) {
  const ws = new WebSocket(url)
  // race 等待 open 事件触发 resolve ,或者 error ,timeout 中任意一个先触发异常
  await EventProxy.new(ws).waitUtilRace([
    { event: "open", timeout },
    { event: "error",
      mapToError: () => new Error("websocket connect error"),
    },
  ])
  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)
})
import { EventProxy } from "ocev"
EventProxy.new(videoDom).proxyAllEvent().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