基于 EventEmitter 封装更上层的 api
本文最后更新于:2021年12月5日 凌晨
场景
已经有许许多多类似的封装了,例如最流行的可能是 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
方法一定会包含回调,即便是事实上不需要关心回调的也一样
事实上,浏览器原生支持的 MessageChannel,其实也就是增加了传递回调函数的功能。
支持仅触发事件的 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 协议 ,转载请注明出处!