npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

rpc-shooter

v0.0.14

Published

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

Downloads

318

Readme

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 化
  • [ ] 协程支持