目次
序文
フロントエンド開発では、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