Nefariously Pushing Master

    rpc-shooter
    TypeScript icon, indicating that this package has built-in type declarations

    0.0.14 • Public • Published

    rpc-shooter

    A tool library for handling window && iframe && worker communication based on the JSON RPC specification

    一个基于 JSON-RPC 规范用于处理 window && iframe && worker 通讯的工具库

    为什么要写这个工具?

    使用 iframeWeb Worker 经常需要写如下代码:

    iframe 中的服务调用

    // parent.js
    const childWindow = document.querySelector('iframe').contentWindow;
    window.addEventListener('message', function (event) {
        const data = event.data;
        if (data.event === 'do_something') {
            // ... handle iframe data
            childWindow.postMessage({
                event: 're:do_something',
                data: 'some data',
            });
        }
    });
    
    // iframe.js
    window.top.postMessage(
        {
            event: 'do_something',
            data: 'ifame data',
        },
        '*'
    );
    window.addEventListener('message', function (event) {
        const data = event.data;
        if (data.event === 're:do_something') {
            // ... handle parent data
        }
    });

    worker 服务调用

    // parent.js
    const worker = new Worker('worker.js');
    worker.addEventListener('message', function (event) {
        const data = event.data;
        if (data.event === 'do_something') {
            // ... handle worker data
            worker.postMessage({
                event: 're:do_something',
                data: 'some data',
            });
        }
    });
    
    // worker.js
    self.postMessage({
        event: 'do_something',
        data: 'worker data',
    });
    self.addEventListener('message', function (event) {
        const data = event.data;
        if (data.event === 're:do_something') {
            // ... handle parent data
        }
    });

    上述的方式可以处理简单的事件通信,但针对复杂场景下跨页面(进程)通信需要一个简单的有效的处理方式,如果可以封装成异步函数调用方式,则会优雅很多,如下:

    // parent.js
    const parentRPC = new RPC({...});
    parentRPC.registerMethod('parent.do_something', (data) => {
        return Promise.resolve({...});
    });
    parentRPC.invoke('child.do_something', { data: 'xxx' })
        .then(res => {
            console.error(res);
        })
        .catch(error => {
            console.error(error);
        });
    
    // child.js
    const childRPC = new RPC({...});
    childRPC.registerMethod('child.do_something', (data) => {
        return Promise.resolve({...});
    });
    childRPC.invoke('parent.do_something', { data: 'xxx' })
        .then(res => {
            console.error(res);
        })
        .catch(error => {
            console.error(error);
        });

    使用 JSON-RPC 2.0 规范可以很清晰简单的描述两个服务间的调用,rpc-shooter 中使用 JSON-RPC 作为数据交互格式。

    安装

    yarn add rpc-shooter
    # or
    npm i rpc-shooter -S

    使用

    使用 RPCMessageEvent 模块可以涵盖下列模块的消息通信:

    如果有更复杂的事件交互场景,实现自己的 event 模块即可。

    iframe

    // main.ts
    import { RPCMessageEvent, RPC } from 'rpc-shooter';
    
    (async function () {
        const iframe = document.querySelector('iframe')!;
        const rpc = new RPC({
            event: new RPCMessageEvent({
                currentEndpoint: window,
                targetEndpoint: iframe.contentWindow!,
                config: { targetOrigin: '*' },
            }),
            // 初始化时注册处理函数
            methods: {
                'Main.max': (a: number, b: number) => Math.max(a, b),
            },
        });
        // 动态注册处理函数
        rpc.registerMethod('Main.min', (a: number, b: number) => {
            return Promise.resolve(Math.min(a, b));
        });
    
        // 检查链接,配置超时时间
        await rpc.connect(2000);
    
        // 调用 iframe 服务中的注册方法
        const randomValue = await rpc.invoke('Child.random', null, { isNotify: false, timeout: 2000 });
        console.log(`Main invoke Child.random result: ${randomValue}`);
    })();
    // child.ts
    import { RPCMessageEvent, RPC } from 'rpc-shooter';
    (async function () {
        const rpc = new RPC({
            event: new RPCMessageEvent({
                currentEndpoint: window,
                targetEndpoint: window.top,
            }),
        });
    
        rpc.registerMethod('Child.random', () => Math.random());
    
        await rpc.connect(2000);
    
        const max = await rpc.invoke('Main.max', [1, 2]);
        const min = await rpc.invoke('Main.min', [1, 2]);
        console.log({ max, min });
    })();

    window

    // main.ts
    import { RPCMessageEvent, RPC } from 'rpc-shooter';
    (async function () {
        const openNewWindow = (path: string) => {
            return window.open(path, '_blank');
        };
        const newWindow = openNewWindow('new-window.html');
        const rpc = new RPC({
            event: new RPCMessageEvent({
                currentEndpoint: window,
                targetEndpoint: newWindow,
                config: { targetOrigin: '*' },
            }),
        });
        rpc.registerMethod('Main.max', (a: number, b: number) => {
            return Promise.resolve(Math.max(a, b));
        });
        await rpc.connect(2000);
        await rpc.invoke('Child.random', null);
    })();
    // child.ts
    import { RPCMessageEvent, RPC } from 'rpc-shooter';
    (async function () {
        const rpc = new RPC({
            event: new RPCMessageEvent({
                currentEndpoint: window,
                targetEndpoint: window.opener,
            }),
        });
        rpc.registerMethod('Child.random', () => Math.random());
    
        await rpc.connect(2000);
    
        await rpc.invoke('Main.max', [1, 2]);
    })();

    Web Worker

    // main.ts
    import { RPCMessageEvent, RPC } from 'rpc-shooter';
    
    (async function () {
        const worker = new Worker('./slef.worker.ts');
        const rpc = new RPC({
            event: new RPCMessageEvent({
                currentEndpoint: worker,
                targetEndpoint: worker,
            }),
        });
        rpc.registerMethod('Main.max', (a: number, b: number) => {
            return Promise.resolve(Math.max(a, b));
        });
        await rpc.connect(2000);
        await rpc.invoke('Child.random', null);
    })();
    // child.ts
    import { RPCMessageEvent, RPC } from 'rpc-shooter';
    
    (async function () {
        const ctx: Worker = self as any;
        const rpc = new RPC({
            event: new RPCMessageEvent({
                currentEndpoint: ctx,
                targetEndpoint: ctx,
            }),
        });
        rpc.registerMethod('Child.random', () => Math.random());
    
        await rpc.connect(2000);
    
        await rpc.invoke('Main.max', [1, 2]);
    })();

    Shared Worker

    // main.ts
    import { RPCMessageEvent, RPC } from 'rpc-shooter';
    
    (async function () {
        const worker: SharedWorker = new SharedWorker('./shared.worker.ts');
        worker.port.start();
        const rpc = new RPC({
            event: new RPCMessageEvent({
                currentEndpoint: worker.port,
                targetEndpoint: worker.port,
            }),
        });
        rpc.registerMethod('Main.max', (a: number, b: number) => {
            return Promise.resolve(Math.max(a, b));
        });
        await rpc.connect(2000);
        await rpc.invoke('Child.random', null);
    })();
    // child.ts
    import { RPCMessageEvent, RPC } from 'rpc-shooter';
    
    interface SharedWorkerGlobalScope {
        onconnect: (event: MessageEvent) => void;
    }
    
    const ctx: SharedWorkerGlobalScope = self as any;
    
    ctx.onconnect = async (event: MessageEvent) => {
        const port = event.ports[0];
        port.start();
        const rpc = new RPC({
            event: new RPCMessageEvent({
                currentEndpoint: port,
                targetEndpoint: port,
            }),
        });
        rpc.registerMethod('Child.random', () => Math.random());
    
        await rpc.connect(2000);
    
        await rpc.invoke('Main.max', [1, 2]);
    };

    配置项

    rpc-shooter 由核心两个模块组成:

    • RPCMessageEvent 用于两个通信上下文(window | iframe | worker)的事件交互
    • RPC 对 RPCMessageEvent 事件交互进行封装,提供方法注册与调用

    RPC

    const RPCInitOptions = {
        timeout: 200,
        event: new RPCMessageEvent({
            currentEndpoint: window,
            targetEndpoint: iframe.contentWindow!,
            config: { targetOrigin: '*' },
        }),
        // 初始化时注册处理函数
        methods: {
            'Main.max': (a: number, b: number) => Math.max(a, b),
            'Main.abs': (a: number) => Math.abs(a),
        },
    };
    const rpc = new RPC(RPCInitOptions);
    // 动态注册处理函数
    rpc.registerMethod('Main.min', (a: number, b: number) => {
        return Promise.resolve(Math.min(a, b));
    });
    // 移除注册函数
    rpc.removeMethod('Main.abs');
    // 检查链接,配置超时时间
    await rpc.connect(2000);
    // 调用 iframe 服务中的注册方法
    const value = await rpc.invoke('Child.random', null, { isNotify: false, timeout: 2000 });

    RPC Options

    interface RPCEvent {
        emit(event: string, ...args: any[]): void;
        on(event: string, fn: RPCHandler): void;
        off(event: string, fn?: RPCHandler): void;
        onerror: null | ((error: RPCError) => void);
        destroy?: () => void;
    }
    
    interface RPCHandler {
        (...args: any[]): any;
    }
    
    interface RPCInitOptions {
        event: RPCEvent;
        methods?: Record<string, RPCHandler>;
        timeout?: number;
    }
    参数 类型 说明
    event 必填 RPCEvent 用于服务间通信的事件模块,可参考 RPCMessageEvent 实现,满足 RPCEvent 接口即可
    methods 可选 Record<string, RPCHandler> 用于注册当前服务可调用的方法
    timeout 可选 number 方法调用的全局超时时间,为 0 则不设置超时时间

    RPC Methods

    connect

    connect(timeout?: number): Promise<void>;

    timeout 超时设置,会覆盖全局设置

    registerMethod

    registerMethod(method: string, handler: RPCHandler);

    注册调用方法

    removeMethod

    removeMethod(method: string);

    移除调用方法

    invoke

    invoke(
        method: string,
        ...args: any[] | [...any[], RPCInvokeOptions]
    ): Promise<any>;
    rpc1.registerMethod('add', (a: number, b: number, c: number) => {
        return new Promise((resolve) => {
            setTimeout(() => resolve(a + b + c), 400);
        });
    });
    // 可以通过 invokeOptions 配置调用超时
    const res1 = await rpc2.invoke('add', 1, 2, 3);
    const res2 = await rpc2.invoke('add', 4, 5, 6, { isNotify: true });
    const res3 = await rpc2.invoke('add', 7, 8, 9, { timeout: 1000 });
    const res4 = await rpc2.invoke('add', 10, 11, 12, { timeout: 4 }).catch((error) => error);

    调用远程服务

    • method string 方法名
    • args any 参数
    • invokeOptions.timeout number timeout 超时设置,会覆盖全局设置
    • invokeOptions.isNotify boolean 是否是个一个通知消息

    可以函数调用参数末尾配置 invokeOptions 选项,如果 invoke 配置了 isNotify,则作为一个通知消息,方法调用后会立即返回,不理会目标服务是否相应,目标也不会响应回复此消息。内部使用 JSON-PRC 的 id 进行标识。

    没有包含“id”成员的请求对象为通知, 作为通知的请求对象表明客户端对相应的响应对象并不感兴趣,本身也没有响应对象需要返回给客户端。服务端必须不回复一个通知,包含那些批量请求中的。 由于通知没有返回的响应对象,所以通知不确定是否被定义。同样,客户端不会意识到任何错误(例如参数缺省,内部错误)。

    RPCMessageEvent

    RPCMessageEvent 实现一套与 socket.io 类似的事件接口,用于处理 window iframe worker 场景下消息通信:

    interface RPCHandler {
        (...args: any[]): any;
    }
    
    interface RPCEvent {
        emit(event: string, ...args: any[]): void;
        on(event: string, fn: RPCHandler): void;
        off(event: string, fn?: RPCHandler): void;
        onerror: null | ((error: RPCError) => void);
        destroy?: () => void;
    }

    使用:

    // main.ts
    import { RPCMessageEvent } from 'rpc-shooter';
    const mainEvent = new RPCMessageEvent({
        currentEndpoint: window,
        targetEndpoint: iframe.contentWindow,
        config: {
            targetOrigin: '*',
        },
        receiveAdapter(event) {
            return event.data;
        },
        receiveAdapter(data) {
            return data;
        },
    });
    
    mainEvent.on('Main.test', (data) => {});
    mainEvent.emit('Child.test', (data) => {});
    // mainEvent.off('Main.someMethod');
    // mainEvent.destroy();
    // child.ts
    import { RPCMessageEvent } from 'rpc-shooter';
    const childEvent = new RPCMessageEvent({
        currentEndpoint: window,
        targetEndpoint: window.top,
        config: {
            targetOrigin: '*',
        },
        receiveAdapter(event) {
            return event.data;
        },
        receiveAdapter(data) {
            return data;
        },
    });
    
    childEvent.emit('Main.test', (data) => {});

    RPCMessageEvent Options

    RPCMessageEvent 初始化选项定义如下:

    interface RPCMessageDataFormat {
        event: string;
        args: any[];
    }
    
    interface RPCPostMessageConfig {
        targetOrigin?: string;
        transfer?: Transferable[];
    }
    
    interface RPCMessageEventOptions {
        currentEndpoint: RPCMessageReceiveEndpoint;
        targetEndpoint: RPCMessageSendEndpoint;
        config?:
            | ((data: any, context: RPCMessageSendEndpoint) => RPCPostMessageConfig)
            | RPCPostMessageConfig;
        sendAdapter?: (
            data: RPCMessageDataFormat | any,
            context: RPCMessageSendEndpoint
        ) => {
            data: RPCMessageDataFormat;
            transfer?: Transferable[];
        };
        receiveAdapter?: (event: MessageEvent) => RPCMessageDataFormat;
    }
    参数 类型 说明
    currentEndpoint 必填 WindowWorkerMessagePort 等满足 RPCMessageReceiveEndpoint 接口对象 当前通信对象的上下文,可以是 WindowWorker 或者 MessagePort 对象
    targetEndpoint 必填 WindowWorkerMessagePort 等满足 RPCMessageSendEndpoint 接口对象 目标通信对象的上下文,可以是 WindowWorker 或者 MessagePort 对象
    config 可选 RPCPostMessageConfig or Function 用于给 targetEndpoint.postMessage 方法配置参数
    sendAdapter 可选 Function 消息发动前数据处理函数
    receiveAdapter 可选 Function 消息接受前数据处理函数

    config

    interface WindowPostMessageOptions {
        transfer?: Transferable[];
        targetOrigin?: string;
    }
    
    interface RPCPostMessageConfig extends WindowPostMessageOptions {
    }
    
    type config?:
            | ((data: any, context: RPCMessageSendEndpoint) => RPCPostMessageConfig)
            | RPCPostMessageConfig;

    用于给 targetEndpoint 的 postMessage 方法配置参数,可以直接配置一个对象,也可以通过函数动态返回一个配置。

    • window.postMessage 配置 targetOrigin -> { targetOrigin: '*' }
    • worker.postMessage 配置 transfer -> { transfer: [...] }
    • figma.ui.postMessage 配置 origin -> { origin: '*' }
    new RPCMessageEvent({
        currentEndpoint: worker,
        targetEndpoint: worker,
        config: {
            targetOrigin: '*',
        },
    });
    // window.postMessage(data, targetOrigin, [transfer]);

    sendAdapter

    type sendAdapter = (
        data: RPCMessageDataFormat | any,
        context: RPCMessageSendEndpoint
    ) => {
        data: RPCMessageDataFormat;
        transfer?: Transferable[];
    };

    发送数据前的适配函数,在一些特殊环境下可以对发送的数据做一些处理:

    • 可以为发送的数据附加 transfer 优化数据传输
    • 一些应用插件场景对交互数据格式有一定要求则可以使用此适配器进行包装
    new RPCMessageEvent({
        currentEndpoint: worker,
        targetEndpoint: worker,
        sendAdapter(data) {
            const transfer = [];
            // 将 ImageBitmap 添加至 transfer 优化数据传输
            JSON.stringify(data, (_, value) => {
                if (value?.constructor.name === 'ImageBitmap') {
                    transfer.push(value);
                }
                return value;
            });
            return { data, transfer };
        },
    });

    receiveAdapter

    type receiveAdapter = (event: MessageEvent) => RPCMessageDataFormat;

    数据接收前的处理函数,一般情况不需要配置,在一些特殊场景下:

    • 检查数据 origin 来源过滤掉不安全消息请求
    • 过滤掉本地开发场景下一些 dev server 抛出消息事件
    • 一些应用插件场景对交互数据格式有一定要求则可以使用此适配器进行包装
    new RPCMessageEvent({
        currentEndpoint: window,
        targetEndpoint: window.parent,
        receiveAdapter(event) {
            if (event.origin === 'xxx') {
                return event.data;
            }
            return null;
        },
    });

    如 figma 插件中 iframe 与主应用通信需要使用 pluginMessage 字段包裹。

    // figma plugin ifame
    new RPCMessageEvent({
        currentEndpoint: window,
        targetEndpoint: window.parent,
        receiveAdapter(event) {
            return event.data.pluginMessage;
        },
        sendAdapter(data) {
            return { pluginMessage: data };
        },
    });

    RPCMessageEvent Methods

    interface RPCHandler {
        (...args: any[]): any;
    }
    
    interface RPCEvent {
        emit(event: string, ...args: any[]): void;
        on(event: string, fn: RPCHandler): void;
        off(event: string, fn?: RPCHandler): void;
        onerror: null | ((error: RPCError) => void);
        destroy?: () => void;
    }

    参考 socket.io API 不做赘述

    方法 说明
    on 设置本地服务事件监听
    emit 触发远程服务事件
    off 移除本地服务事件监听
    onerror 发生错误时触发 onerror 回调
    destroy 释放 RPCMessageEvent 资源与内部事件监听

    RPCMessageEvent Event

    onerror

    type onerror = null | ((error: RPCError) => void);

    事件模块内部发生错误时的回调函数。

    const event = new RPCMessageEvent({...});
    event.onerror = (error) => {
        console.log(error);
    };

    开发

    # 依赖
    yarn
    # 开发
    yarn dev
    # 构建
    yarn build

    TODO

    • [ ] 添加测试用例
    • [ ] onmessage 需要检查消息来源
    • [ ] proxy 化
    • [ ] 协程支持

    Install

    npm i rpc-shooter

    DownloadsWeekly Downloads

    78

    Version

    0.0.14

    License

    APACHE-2.0

    Unpacked Size

    65.9 kB

    Total Files

    8

    Last publish

    Collaborators

    • kinglisky