JS のケース: フロントエンド Iframe およびワーカー通信ソリューション

目次

序文

Iframe通信

Worker通信

実装のアイデア

実装プロセス

メッセージセンタークラス

IPCクラス

サーバークラス

クライアントクラス

ピアツーピア

デモ

基本機能

父子通信

兄弟通信

父子兄弟通信

スレッド通信

その他の機能

関数呼び出し

インデックスID

アンインストールページ

ページをリセットする

バッチ実行

バッチ操作

要約する


序文

フロントエンド開発では、iframe とワーカーは、サードパーティのページを自分のページに埋め込む、同じページに複数の異なるコンテンツを表示する、バックグラウンドで JS コードを実行するなど、いくつかの特別な要件を達成するためによく使用されます。ただし、iframe と Worker は独立したドキュメント構造と実行環境を持っているため、複数のページやスレッド間でのデータのやり取りや通信が困難になります。このとき、ファイル間の通信が非常に重要になりますので、子ページが親ページや他のページとデータやステータスを共有したり、ページ間の連携という目的を達成するために、JSでプラグインパッケージを実装しました。

Iframe通信

まず、iframe の通信方法を理解する必要があります。

window オブジェクトは、postMessage 関数を提供します。postMessage を使用して、子ページ、親ページ、または自分自身にメッセージを送信し、window オブジェクトのメッセージ イベントをリッスンして受信メッセージを取得します。

window.postMessage("父页面发送的消息"); // 发给了当前页面

window.addEventListener(
  "message",
  console.log.bind(this, "父页面收到信息") // 父页面收到信息 MessageEvent
);

postMessage(message, targetOrigin, transfer) 関数では、次の 3 つのパラメータを渡すことができます。

  • メッセージ: 送信されるメッセージ
  • targetOrigin: ターゲット ソース (例: "http://127.0.0.1:5500/"、"*" はすべてのワイルドカードを意味します)
  • 転送: ディープ コピー データをキャンセルします。メッセージを通じてオブジェクトを送信します。これはディープ コピー データです。ターゲット ページと現在のページに 2 つのオブジェクトが生成されます。メッセージを直接送信すると、大量のパフォーマンスが消費されます。データを保存する機能を実現するには転送を使用します。

以下は簡単な親子コミュニケーションの例です。

     // parent
      sonIframe.onload = () => {
        sonIframe.contentWindow.postMessage(data, "*");
      };
      window.addEventListener("message", (e) => {
        console.log("父页面收到信息", e.data);
      });
    // son
      window.addEventListener("message", (e) => {
        console.log("子页面收到信息", e.data);
        window.parent.postMessage(e.data, "*");
      });

Worker通信

Worker は js のマルチスレッドです。通信方法は iframe と似ています。ワーカーのインスタンス化オブジェクトを使用してメッセージを送信します。ただし、iframe はparent経由で親ページにアクセスでき、ワーカーはself オブジェクト経由でのみ親ページ内のインスタンスワーカーにメッセージを渡すことができるため、関数の実装には互換性が必要です。以下はワーカーを使用したコードです。

// 父页面
const worker = new Worker("./worker.js", { type: "module" });
worker.onmessage = (e) => {
  console.log("parent收到了消息:", e.data);
};
worker.postMessage({ type: "msg", msg: "你也好" });

// 子页面
self.onmessage = (e) => {
  console.log("worker收到了消息:", e.data);
};
self.postMessage({ type: "msg", msg: "你好" });

 

実装のアイデア

上記の例では、API を使用してページ間の通信を実現できます。マインド マップは次のとおりです。

上記の設計には、サーバー、PeerToPeer、およびクライアントの 3 つのツール クラスがあり、これらはオブザーバーを通じて通信し、MessageCenterを通じて非同期タスクを渡します。

このうち、サーバーは親ページに配置されてメッセージの監視と送信を行い、クライアントは子ページに配置され、サーバーと同様にメッセージの送受信の役割を担い、PeerToPeer には 2 つの機能があります。1 つはブロードキャストしてメッセージを送信する機能、もう 1 つはブロードキャスト メッセージを収集する機能です。さらに、サーバーとクライアントはメッセージを通じて直接通信することもでき、サブページ間の通信は PeerToPeer を通じてバインドされます。

実装プロセス

上記の基本的な概念と考え方を理解したら、この機能を実装してみましょう

メッセージセンタークラス

メッセージセンターはメッセージ送受信の中核として多くの場所で利用されており、具体的な実装方法は前回の記事を参照してください

Promise と比較すると、その利点は、複数の非同期操作をトリガーできることです。これにより、コールバック関数の結合が回避されるだけでなく、非同期操作も解決されます。これは、通信プロセスの不可欠な部分です。コア関数は、このクラスを継承し、視覚化できます。特定の操作を実装する関数の一部に基づいて、プログラム設計全体で複数のチェーン呼び出しを使用して、次のような関数を実行できます。

  server
    .mount()
    .on("msg", console.log)
    .on("msg1", console.log)
    .on("msg2", console.log)
    .load()
    .catch(console.log);

IPCクラス

ツール全体の中核となるのは、前述のメッセージ送信用の send とメッセージ受信用の handleMessage を統合する IPC (プロセス コミュニケーション) です。さらに、これら 2 点に基づいて、クラスの柔軟性と高可用性を向上させるために、いくつかの可能な関数をクラスに追加しました。たとえば、mount 関数は、メッセージを監視するために現在のページを手動でマウントするために使用されます。vokeHandler は、それぞれ「関数タイプのメッセージをリッスンして対応する関数を実行する」および「関数タイプのメッセージを送信して関数をトリガーする」です。

import type { MessageCenter as TypeMessageCenter } from "utils-lib-js"
import { PeerToPeer } from "./p2p"
const { defer, MessageCenter, getType } = UtilsLib
export namespace IPCSpace {
    export type IObject<T = any> = {
        [key: string | number | symbol]: T
    }
    export type IHandler<T = any> = {
        (...args: any[]): Promise<T> | void
    }

    export type IOptions<Target = ITarget> = {
        target: Target // 目标页面,一般指Iframe或者window.parent
        origin: string // 发送消息给哪个域名
        source: Window | Worker // 当前window对象或Worker
        handlers: IObject<IHandler> // 钩子函数,等对方触发
        id: string // 标识,用来区分调用者
        handlersFixStr: string // 动态修改函数type关键字
        transfer: any // 需要传递的较大的数据,避免message的深复制导致两边的性能损耗较大
    }
    export type ISendParams = {
        type: string // 消息类型
        data?: unknown // 传递数据
        id?: number | string // 消息标识
    }
    export type ITarget = Window | HTMLIFrameElement | Worker
}
export class IPC extends (MessageCenter as typeof TypeMessageCenter) {
    constructor(protected opts: Partial<IPCSpace.IOptions> = {}) {
        super()
        const {
            origin = "*",
            source = window,
            target = null,
            transfer,
            handlers = {},
            id = "",
            handlersFixStr = "@invoke:ipc:handlers:"
        } = opts
        this.opts = {
            origin,
            source,
            target,
            transfer,
            handlers,
            id,
            handlersFixStr
        }
    }
    get sendMethods() {
        console.error('重写此函数');
        return (() => { }) as Function
    }
    /**
     * 目标对象,父页面中的iframe,子页面中的window.parent,子类重写该对象,进行校验
     */
    get target() {
        const { target } = this.opts
        if (!!!target) throw new Error("target 不能为空")
        return target
    }
    set id(id) {
        this.opts.id = id
    }
    get id() {
        return this.opts.id
    }
    get source() {
        return this.opts.source
    }
    /**
     * 当前页面加载完成
     * @returns promise
     */
    load() {
        const { promise, resolve } = defer()
        this.target.addEventListener("load", resolve)
        return promise
    }
    /**
     * 挂载当前页面,监听对方消息
     * @returns IPC
     */
    mount(handler?: (e: MessageEvent) => void) {
        const { source } = this
        this.unMount(handler)
        source?.addEventListener('message', handler ?? this.handleMessage);
        return this
    }
    /**
     * 卸载当前页面,取消监听对方消息
     * @returns IPC
     */
    unMount(handler?: (e: MessageEvent) => void) {
        const { source } = this
        source?.removeEventListener('message', handler ?? this.handleMessage);
        return this
    }
    /**
     * 重置当前IPC
     */
    reset() {
        this.unMount()
        this.clear()
        this.opts.handlers = {}
    }
    /**
     * 触发target的钩子函数
     * @param params 
     */
    invokeHandler(params: IPCSpace.ISendParams) {
        const { handlersFixStr } = this.opts
        const { type, ...oths } = params
        this.send({ type: `${handlersFixStr}${type}`, ...oths })
    }
    /**
     * 钩子函数处理
     * @param params 
     * @returns 函数运行结果
     */
    watchHandler(params) {
        const { handlerType, data = [] } = params
        const { handlers } = this.opts
        const fn = handlers[handlerType]
        return fn?.(...data)
    }
    /**
     * 当前页面接收消息
     * @param e  message 事件对象
     * @returns void
     */
    handleMessage = (e: MessageEvent) => {
        const { id, type, data } = e.data
        const { handlersFixStr } = this.opts
        if (!!!this.checkID(id)) return
        const handlerType = this.isHandler(type, handlersFixStr)
        if (handlerType) {
            return this.watchHandler({ handlerType, data })
        }
        this.emit(type, data)
    }
    /**
     * 发送消息
     * @param params 
     * @returns IPC
     */
    send(params: IPCSpace.ISendParams) {
        const { origin, transfer } = this.opts
        const { type, data = {}, id = this.id } = params
        const { target, sendMethods } = this
        let fnParams = [{ type, data, id }, origin, transfer]
        if (type) {
            isWorker(target) && (fnParams = [{ type, data, id }, transfer]); sendMethods?.(...fnParams);
        }

        return this
    }
    /**
     * 校验id
     * @param id 
     * @returns {boolean}
     */
    private checkID(id: string) {
        return id === this.id
    }
    isWindow = isWindow
    formatToIframe = formatToIframe
    isHandler = isHandler
    isWorker = isWorker
}
/**
 * 格式化Iframe,取selector还是element对象
 * @param target 
 * @returns 
 */
export const formatToIframe = (target: IPCSpace.ITarget | string) => {
    return getType(target) === "string" ? document.querySelector(`${target}`) : target
}
/**
 * 当前环境是不是父窗口
 * @param source 需要判断的对象
 * @returns 
 */
export const isWindow = (source: any) => {
    return source && source === source.window
}
/**
 * 当前环境是不是子线程或线程对象
 * @param worker 需要判断的对象
 * @returns 
 */
export const isWorker = (worker: any) => {
    return worker instanceof Worker || typeof DedicatedWorkerGlobalScope !== "undefined"
}
/**
 * 当前的type是否能被截取,用来截取函数调用消息
 * @param type 消息类型
 * @param __fixStr 截取的字符
 * @returns 
 */
export const isHandler = (type, __fixStr = '') => {
    return type.split(__fixStr)?.[1]
}

サーバークラス

Server クラスは上記の IPC を継承しています。さらに、サーバーはターゲット アクセサーと sendMethods アクセサーも実装しています。クライアントとサーバーの一部の機能が異なるため、これらは 2 つに別々に実装されます。ターゲットの役割は、サブページでバッチ操作を実行できるように P2P へのエントリを開くことです。sendMethods は postMessage と互換性があります。

import { IPC, IPCSpace } from "./ipc"

export class Server extends IPC {
    constructor(opts: Partial<IPCSpace.IOptions<HTMLIFrameElement | Worker>>) {
        super(opts)
    }
    get sendMethods() {
        return this.target instanceof Worker ? this.target?.postMessage.bind(this.target) : this.target?.contentWindow?.postMessage// Server发送消息的方式取子页面的contentWindow,如果是worker则直接使用postMessage
    }
    /**
     * 允许重新设置目标对象
     */
    set target(_target) {
        this.opts.target = _target
    }

    /**
     * 校验目标对象,若没传则说明当前server与client是一对多关系
     */
    get target() {
        const { target } = this.opts
        if (!!!target) return null
        const _target = this.formatToIframe(target)
        if (!!!(_target instanceof HTMLIFrameElement || _target instanceof Worker)) throw new Error("target必须是IFrame、Worker或标签选择器")
        return _target
    }
}

クライアントクラス

クライアントは、ターゲットと sendMethods の実装の個別の処理のみを備えたサーバーの若いバージョンとして理解できます。

import { IPC, IPCSpace } from "./ipc"
export class Client extends IPC {
    constructor(opts: Partial<IPCSpace.IOptions<Window>>) {
        super(opts)
    }
    get sendMethods() {
        return this.target.postMessage // Client发送消息的方式取父页面,一般是parent
    }
    /**
     * 校验父页面
     */
    get target() {
        const { target } = this.opts
        if (!!!(this.isWindow(target) || target === self)) throw new Error("target必须是Window或Worker的self对象")
        return target as Window
    }
}

ピアツーピア

これはオリジナルの機能のアップグレードであり、実際には上記のコードを使用してさまざまな通信要件を満たすことができますが、一部の操作では体系的なスケジューリングと配信が必要であり、現時点では単純なディストリビュータの方が重要です。

PeerToPeer の主な実装には 2 つの主要なモジュールがあります。1 つは複数のサブページ間でメッセージを交換するモジュールで、もう 1 つは親ページと複数のサブページの間でメッセージを送信して多対多のメッセージングまたは関数呼び出しを実現するモジュールです。

PeerToPeerクラスの核となるコードはbatchOperationとservers属性ですが、このときサーバーをツールクラスとみなすことができ、この機能を利用してサブページに対するバッチ操作を行うことができます。

import { formatToIframe, IPCSpace } from "./ipc"
import { Server } from './server'
export type IClients = Iframe | string[]
export type Iframe = HTMLIFrameElement[]
export class PeerToPeer {
    /*关联的iframe列表,可以传element或选择器,
    如'#iframe','.iframe'等等*/
    clients: Iframe
    /**我们把每个client当成是一个观察者,新建一个server进行批量操作 */
    server: Server
    isWorker: boolean // 是否用于线程中
    constructor(clients: IClients, protected opts: Partial<IPCSpace.IOptions> = {}) {
        this.clients = this.formatClients(clients)
        this.isWorker = this.clients.every(it => it instanceof Worker)
        this.create(this.opts)
    }
    /**
     * 将iframe选择器转换成element对象
     * @param _clients 
     * @returns 
     */
    formatClients(_clients: IClients) {
        return _clients.map((it) => formatToIframe(it)) as Iframe
    }
    /**
     * 批量操作,核心操作
     * @param fn Server的函数
     * @param arr clients列表,默认全选
     * @param hook 数组操作函数
     * @returns 
     */
    protected batchOperation(fn, arr = this.clients, hook = "forEach") {
        const __clients = arr[hook]((it) => {
            this.server.target = it
            return fn(this.server)
        })
        this.server.target = null // 操作过后清空操作对象,函数内部产生闭包,可以正常运行
        return __clients
    }
    /**
     * 创建server,将批量操作功能放进了batchOperation中,与上一版相比节省资源,只需创建一个server即可
     */
    private create(opts) {
        this.server = new Server(opts)
    }
    /**
     * 子页面加载完毕
     * @returns 
     */
    load() {
        return Promise.all(this.batchOperation(it => it.load(), undefined, "map"))
    }
    /**
     * 挂载页面
     * @returns 
     */
    connect() {
        this.disconnect()
        if (this.isWorker) {
            // Worker的target是self而不是window,所以需要单独处理
            this.batchOperation(it => it.target.addEventListener('message', this.message))
        } else {
            this.server.mount()
            this.server.mount(this.message)
        }
        return this
    }
    /**
     * 卸载页面
     * @returns 
     */
    disconnect() {
        if (this.isWorker) {
            this.batchOperation(it => it.target.removeEventListener('message', this.message))
        } else {
            this.server.unMount()
            this.server.unMount(this.message)
        }
        return this
    }
    /**
     * 重置页面
     * @returns P2P
     */
    reset() {
        this.disconnect()
        this.server.reset()
        return this
    }
    /**
     * 消息接收钩子
     * @param e 
     */
    protected message = (e) => {
        const { data, source, target } = e
        // 线程与窗口取值不同
        let __target = source?.frameElement
        if (this.isWorker) {
            __target = target
            this.server.handleMessage(e)
        }
        this.broadcast(data, this.filterSelf(__target))
    }
    /**
     * 过滤当前页面,不发给自己
     * @param self 当前页面,即发送消息的子页面
     * @returns 
     */
    protected filterSelf(self) {
        return this.clients.filter(it => it !== self)
    }
    /**
     * 广播
     * @param param0 
     * @param clients // 发送给哪些列表
     */
    broadcast({ type, id, data }, clients?: Iframe) {
        this.batchOperation(it => it.send({ type, data, id }), clients)
    }
    /**
     * 批量执行函数
     * @param param0 
     * @param clients 
     */
    invoke({ type, id, data }, clients?: Iframe) {
        this.batchOperation(it => it.invokeHandler({ type, data, id }), clients)
    }
}

デモ

基本機能

父子通信

親と子間の双方向通信。サーバー クラスとクライアント クラス間の関連付けを通じてメッセージを直接送信します。

// 父页面 index.html
const server = new Server({
  target: "#son",
});
// 监听 "msg" 事件
server.on("msg", console.log.bind(null, "parent收到消息"));
// 挂载并加载服务
await server.mount().load();
// 发送 "msg" 消息
server.send({ type: "msg", data: { name: "parent" } });

// 子页面 son.html
const client = new Client({
  target: window.parent,
});
// 建立连接
client.mount();
// 监听 "msg" 事件
client.on("msg", console.log.bind(null, "son收到消息"));
// 发送 "msg" 消息
client.send({ type: "msg", data: { name: "son" } });

兄弟通信

同じ親ページの下にある 2 つの子ページは兄弟ページと呼ばれ、PeerToPeer クラスを使用して新しい接続を確立できます。

// 父页面 index.html
// 建立多点连接
const peer = new PeerToPeer(["#son2", "#son"]);
// 开启连接
peer.connect();
// 等待子页面加载
await peer.load();
// 加载完成,群发消息
peer.broadcast({ type: "load:finish" });

// 子页面1 son1.html
const client = new Client({
  target: window.parent,
});
client.mount();
client.on("msg", console.log.bind(null, "son收到消息"));
// 等待所有页面加载完成
client.on("load:finish", () => {
  client.send({ type: "msg", data: { name: "son" } });
});

// 子页面2 son2.html
const client = new Client({
  target: window.parent,
});
client.mount();
client.on("msg", console.log.bind(null, "son2收到消息"));
client.on("load:finish", () => {
  client.send({ type: "msg", data: { name: "son2" } });
});

父子兄弟通信

父と子、兄弟間のコミュニケーションは高度な使い方であり、上記の兄弟コミュニケーションページをベースにメッセージ受信を追加してメッセージを聞いたり、メッセージを送信したりすることはpeer.broadcastを使用して実装できます。

// 父页面收发消息
peer.server.on("msg", console.log.bind(null, "parent收到消息"));
peer.broadcast({ type: "msg", data: { name: "parent" } });

 

スレッド通信

さらに、js-ipc は js スレッド ワーカー通信もサポートしています。以下は例です。iframe との違いは、クライアントで渡されるターゲットと現在のソースが両方とも self であり、メイン スレッドのピアによって渡されるリストも Worker オブジェクトであることです。

// index.js
const worker1 = new Worker("./worker1.js", { type: "module" });
const worker2 = new Worker("./worker2.js", { type: "module" });
const peer = new PeerToPeer([worker1, worker2]);
peer.connect();
peer.server.on("msg", console.log.bind(null, "parent收到消息"));

// worker1.js
const client = new Client({
  target: self,
  source: self,
});
// 建立连接
client.mount();
// 监听 "msg" 事件
client.on("msg", console.log.bind(null, "worker1收到消息"));

// worker2.js 同worker1

その他の機能

関数呼び出し

関数呼び出しは実際にはメッセージ送信の拡張であり、invokeHandler メソッドを通じて相手の関数を呼び出します。

// 父页面
const server = new Server({
  target: "#son",
  handlers: {
    // 父页面的处理函数
    log: console.log,
  },
});


// 子页面
client.invokeHandler({ type: "log", data: ["log"] });

インデックスID

ページ数が多い場合、メッセージ送信の混乱を避けるためにIDで情報を識別できる

// 父页面
const server = new Server({
  target: "#son",
  id: 12,// 通过id标识发送的消息
});


// 子页面
const client = new Client({
  target: window.parent,
  id: 12,// 子页面不加id就收不到消息
});

アンインストールページ

unMount を使用して現在のページの監視をキャンセルし、マウントをキャンセルします。

client.unMount();

ページをリセットする

リセット機能を使用してページを初期化し、すべての情報をリセットします

client.reset()

バッチ実行

バッチ実行関数の呼び出しは、peer.broadcast に基づいて実装されます。たとえば、son4Info を含むすべてのサブページでこの関数を呼び出します。

peer.invoke({ type: "son4info", data: ["parent"] });

P2P登録機能による親ページ

const peer = new PeerToPeer(["#son4", "#son5"], {
  handlers: {
    parentLog: console.log,
  },
});

バッチ操作

サブページのバッチリセット、アンインストール、マウント、ロードの監視

const peer = new PeerToPeer(["#son4", "#son5"]);
await peer.reset().disconnect().connect().load();
peer.broadcast({ type: "load:finish" });

要約する

以上が記事の全内容です. 本記事では, postMessage, onmessageによるiframeとワーカー間の通信を紹介し, この機能であるIPCをベースとしたプロセス通信の機能を実現しています. IPCをベースとして, サーバーとクライアントの動作にそれぞれ対応するServerとClientを導出する拡張を作成しました. さらに, エンドツーエンドのバッチ操作P2P機能も実装し, 上記機能をデモしました.

最後まで読んでいただきありがとうございます、記事があなたのお役に立てれば幸いです、記事が悪くないと思ったら三回サポートしてください、ありがとうございます!

ソースコード: js-ipc: JavaScript 通信ツールキット、Iframe と Worker 間の通信をサポート

または: myCode: js またはプロジェクトの js-ipc サブモジュールに基づくいくつかの小さなケース

NPM:js-ipc - npm

おすすめ

転載: blog.csdn.net/time_____/article/details/130246659