基于 EventEmitter 封装更上层的 api

本文最后更新于:2021年11月24日 下午

场景

已经有许许多多类似的封装了,例如最流行的可能是 ChromeLab 的 comlink

之前虽然通过 IEventEmitter 声明了子应用与系统之间的通信,但实际使用时可能不够简单,所以基于它封装更上层的 api MessageChannel,希望它能够有以下功能。

基础实现

  • 在一边通过 on 方法注册后应该可以直接返回值并自动发回至调用者
  • 默认对参数及返回值做一些处理以提高性能,主要是通过 JSON.stringify 实现(待完成测试)
import { IEventEmitter } from "./IEventEmitter";
// 这只是一个简单的 id 生成器
import { CallbackIdGenerator } from "./CallbackIdGenerator";

export class MessageChannel {
  constructor(private readonly emitter: IEventEmitter) {}

  invoke(channel: string, data?: any): Promise<any> {
    return new Promise((resolve) => {
      const callbackId = CallbackIdGenerator.generate();
      this.emitter.on(callbackId, (msg) => {
        resolve(msg === undefined ? undefined : JSON.parse(msg));
        this.emitter.offByChannel(callbackId);
      });
      this.emitter.emit(channel, JSON.stringify({ data, callbackId }));
    });
  }

  on(channel: string, handle: (data: any) => any) {
    this.emitter.on(channel, async (msg) => {
      const value = JSON.parse(msg) as { callbackId: string; data: any };
      const res = await handle(value.data);
      if (value.callbackId) {
        this.emitter.emit(value.callbackId, JSON.stringify(res));
      }
    });
  }

  offByChannel(channel: string) {
    this.emitter.offByChannel(channel);
  }
}

使用

messageChannel.on("hello", (name: string) => `hello ${name}`);
expect(await messageChannel.invoke("hello", "liuli")).toBe("hello liuli");

但它仍存在一些问题

  • invoke 方法一定会包含回调,即便是事实上不需要关心回调的也一样

支持仅触发事件的 emit 函数

主要思路是添加 type 字段标识调用的类型,即由调用者决定是否需要关心返回值。

import { IEventEmitter } from "./IEventEmitter";
import { CallbackIdGenerator } from "./CallbackIdGenerator";

type MessageChannelData = { data?: any } & (
  | { type: "emit" }
  | { type: "invoke"; callbackId: string }
);

export class MessageChannel {
  constructor(private readonly emitter: IEventEmitter) {}

  invoke(channel: string, data?: any): Promise<any> {
    return new Promise((resolve) => {
      const callbackId = CallbackIdGenerator.generate();
      this.emitter.on(callbackId, (msg) => {
        resolve(msg === undefined ? undefined : JSON.parse(msg));
        this.emitter.offByChannel(callbackId);
      });
      this.emitter.emit(
        channel,
        JSON.stringify({
          type: "invoke",
          data,
          callbackId,
        } as MessageChannelData)
      );
    });
  }

  emit(channel: string, data?: any): void {
    this.emitter.emit(channel, {
      type: "emit",
      data,
    } as MessageChannelData);
  }

  on(channel: string, handle: (data: any) => any) {
    this.emitter.on(channel, async (msg) => {
      const value = JSON.parse(msg) as MessageChannelData;
      const res = await handle(value.data);
      if (value.type === "invoke") {
        this.emitter.emit(value.callbackId, JSON.stringify(res));
      }
    });
  }

  offByChannel(channel: string) {
    this.emitter.offByChannel(channel);
  }
}

基于 Proxy + interface 实现更好的 dx

首先定义一些概念

  • 命名空间:一个命名空间总是对应一个接口,包含一组动作
  • 动作:一个动作总是对应一个函数,并且支持参数与返回值
import { ConditionalKeys } from "type-fest";
import { MessageChannel } from "./MessageChannel";

type Func = (...args: any[]) => any;
type FunctionKeys<T> = ConditionalKeys<T, Func>;
type PromiseValue<T> = T extends Promise<any> ? T : Promise<T>;
type PromiseFunc<T extends Func> = (
  ...args: Parameters<T>
) => PromiseValue<ReturnType<T>>;

export interface BaseDefine<T extends string> {
  namespace: T;
}

export type ClientApi<T extends BaseDefine<any>> = {
  [P in FunctionKeys<T>]: T[P] extends Func ? PromiseFunc<T[P]> : never;
};

/**
 * 包装生成客户端
 * @param messageChannel
 * @param namespace
 */
export function wrap<T extends BaseDefine<any>>(
  messageChannel: MessageChannel,
  namespace: T["namespace"]
): ClientApi<T> {
  return new Proxy(
    {},
    {
      get(target: {}, p: string): any {
        return (data: any) => {
          return messageChannel.invoke(`${namespace}.${p}`, data);
        };
      },
    }
  ) as ClientApi<T>;
}

/**
 * 注册服务端
 * @param messageChannel
 * @param server
 * @param endpoints
 */
export function expose<T extends BaseDefine<any>>(
  messageChannel: MessageChannel,
  server: T,
  endpoints: FunctionKeys<T>[]
) {
  endpoints.forEach((endpoint) => {
    messageChannel.on(
      `${server.namespace}.${endpoint}`,
      (server[endpoint] as unknown as Func).bind(server)
    );
  });
}

使用

interface IDemoApi extends BaseDefine<"demo"> {
  hello(name: string): string;
}

class DemoApi implements IDemoApi {
  namespace = "demo" as const;

  hello(name: string) {
    return `hello ${name}`;
  }
}

expose<IDemoApi>(messageChannel, new DemoApi(), ["hello"]);
const systemClient = wrap<IDemoApi>(messageChannel, "demo");
const res = await systemClient.hello("liuli");
expect(res).toBe("hello liuli");

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!