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

Overview

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 化
  • 协程支持
You might also like...

Challenge [Frontend Mentor] - In this challenge, JavaScript was used to filter jobs based on the selected categories. Technologies used: HTML5, CSS3 and React.

Getting Started with Create React App This project was bootstrapped with Create React App. Available Scripts In the project directory, you can run: np

Apr 13, 2022

A document based messaging queue for Mongo, DocumentDB, and others

DocMQ Messaging Queue for any document-friendly architectures (DocumentDB, Mongo, Postgres + JSONB, etc). Why Choose This DocMQ is a good choice if yo

Dec 7, 2022

A general-purpose message and event queuing library for MongoDB

MongoMQ2 MongoMQ2 is a light-weight Node.js library that turns MongoDB collections into general-purpose message queues or event logs, without addition

Dec 28, 2022

🌈 A very simple window communication library

🌈 A very simple window communication library

window-channel A very simple window communication library. Get Start! Install npm install @haiyaotec/window-channel Usage/Examples Client import

Oct 29, 2022

Type-safe and Promisified API for Web Worker and Iframe

💛 You can help the author become a full-time open-source maintainer by sponsoring him on GitHub. typed-worker Install npm i typed-worker Usage Create

Dec 31, 2022

⚡️The Fullstack React Framework — built on Next.js

⚡️The Fullstack React Framework — built on Next.js

The Fullstack React Framework "Zero-API" Data Layer — Built on Next.js — Inspired by Ruby on Rails Read the Documentation “Zero-API” data layer lets y

Jan 4, 2023

Patronum: Ethereum RPC proxy that verifies RPC responses against given trusted block hashes

Patronum: Ethereum RPC proxy that verifies RPC responses against given trusted block hashes

Patronum Ethereum RPC proxy that verifies RPC responses against given trusted block hashes. Currently, most of the DAPPs and Wallets interact with Eth

Dec 7, 2022

A lightweight (~850 B) library for easy mac/window shortcut notation. kbd-txt convert shortcut text depending on the type of OS (window/linux/mac).

kbd-txt A lightweight (~850 B) library for easy mac/window shortcut notation. kbd-txt convert shortcut text depending on the type of OS (window/linux/

Jan 1, 2023

Run RPC over a MessagePort object from a Worker thread (or WebWorker)

thread-rpc Run RPC over a MessagePort object from a Worker thread (or WebWorker) npm install thread-rpc Usage First in the parent thread const Thread

May 31, 2022

A reference implementation for the specification that can create and configure a dev container from a devcontainer.json.

A reference implementation for the specification that can create and configure a dev container from a devcontainer.json.

Dev Container CLI This repository holds the dev container CLI, which can take a devcontainer.json and create and configure a dev container from it. Co

Jan 6, 2023

Grupprojekt för kurserna 'Javascript med Ramverk' och 'Agil Utveckling'

JavaScript-med-Ramverk-Laboration-3 Grupprojektet för kurserna Javascript med Ramverk och Agil Utveckling. Utvecklingsguide För information om hur utv

May 18, 2022

Hemsida för personer i Sverige som kan och vill erbjuda boende till människor på flykt

Getting Started with Create React App This project was bootstrapped with Create React App. Available Scripts In the project directory, you can run: np

May 3, 2022

Kurs-repo för kursen Webbserver och Databaser

Webbserver och databaser This repository is meant for CME students to access exercises and codealongs that happen throughout the course. I hope you wi

Jan 3, 2023

Defines the communication layer between mobile native(iOS/Android) and webview using JSON Schema and automatically generates SDK code

Defines the communication layer between mobile native(iOS/Android) and webview using JSON Schema and automatically generates SDK code.

Dec 8, 2022

Collection of JSON-RPC APIs provided by Ethereum 1.0 clients

Ethereum JSON-RPC Specification View the spec The Ethereum JSON-RPC is a collection of methods that all clients implement. This interface allows downs

Jan 8, 2023

JSON-RPC 2.0 implementation over WebSockets for Node.js and JavaScript/TypeScript

JSON-RPC 2.0 implementation over WebSockets for Node.js and JavaScript/TypeScript

WebSockets for Node.js and JavaScript/TypeScript with JSON RPC 2.0 support on top. About The rpc-websockets library enables developers to easily imple

Dec 21, 2022

Get JSON RPC on a stream

json-rpc-on-a-stream Get JSON RPC on a stream npm install json-rpc-on-a-stream

May 31, 2022

Like JSON-RPC, but supports streaming.

Like JSON-RPC, but supports streaming.

Earthstar Streaming RPC Similar to JSON-RPC, but also supports streaming (soon). Written to be used in Earthstar (github, docs). Table of Contents Usa

Feb 10, 2022

A simple javascript utility library to include partial html (iframe alternate) without a framework or jQuery.

alt-iframe A simple javascript utility library to include partial html (iframe alternate) without a framework or jQuery. !doctype html html lang="e

Dec 30, 2022
Owner
臼犀
英雄请不要随便死在路上~
臼犀
Premium Queue package for handling distributed jobs and messages in NodeJS.

The fastest, most reliable, Redis-based queue for Node. Carefully written for rock solid stability and atomicity. Sponsors · Features · UIs · Install

null 13.5k Dec 31, 2022
A tiny utility to asynchronously drive a namespace exposed through a Worker.

proxied-worker Social Media Photo by Ricardo Gomez Angel on Unsplash A tiny utility to asynchronously drive a namespace exposed through a Shared/Servi

Andrea Giammarchi 43 Dec 8, 2022
Easily redirect one entire domain to another with a serverless Cloudflare Worker.

Domain Redirecting with Cloudflare Workers Easily redirect one entire domain to another with a serverless Cloudflare Worker. All paths and other data

Erisa A 19 Dec 11, 2022
Redirect requests of current origin to another domain with Service Worker.

Service Worker to Redirect Origin This is a tool for your static website which could intercept all GET requests of the origin domain and redirect them

Menci 9 Aug 28, 2022
⚙️ Offline-capable Astro apps via SWSR (Service Worker Side Rendering)

Astro-service-worker ⚙️ Offline-capable Astro apps via SWSR (Service Worker Side Rendering) astro-service-worker will take your Astro SSR project, and

Pascal Schilp 41 Dec 4, 2022
Cloudflare Worker that will allow you to progressively migrate files from an S3-compatible object store to Cloudflare R2.

A Cloudflare Worker for Progressive S3 to R2 Blog Post: https://kian.org.uk/progressive-s3-to-cloudflare-r2-migration-using-workers/ This is a Cloudfl

Kian 29 Dec 30, 2022
Send emails using Cloudflare Worker, for free.

Email API for Proselog. Not intended for use outside of Proselog, but it should work with any worker, without any configuration. import { sendEmail }

Proselog 65 Nov 7, 2022
Cloudflare Worker to make a R2 Bucket public!

r2-public-worker A Cloudflare Worker to make your R2 bucket public! Minimum Requirements Cloudflare Account wrangler >= 2.0.2 Note: Ensure you are usi

Cole Mackenzie 20 Sep 19, 2022
Using Cloudflare worker to generate host list from firebog to keep updated.

AdGuardCloudflareHostGenerator Use a cloudflare worker to generate a up to date list from FireBog's ticked list found at https://v.firebog.net/hosts/l

Jake Steele 14 Nov 30, 2022
BullMQ - Premium Message Queue for NodeJS based on Redis

The fastest, most reliable, Redis-based distributed queue for Node. Carefully written for rock solid stability and atomicity. Read the documentation F

Taskforce.sh Inc. 3.1k Dec 30, 2022