JS案例:前端Iframe及Worker通信解決思路
目錄
前言
在前端開發中,經常會使用iframe和worker來實現一些特殊的需求,比如將第三方的頁面嵌入到自己的頁面中,或者在同一頁面中顯示多個不同的內容,后臺運行JS代碼等。然而,由于iframe和Worker具有獨立的文檔結構和執行環境,所以在多個頁面及線程之間進行數據交互和通信變得困難。此時文件之間的通信就非常重要,為了讓子頁面與父級或其他頁面共享數據和狀態或使頁面間達到聯動的目的,我用JS實現了一個插件包,這里做個分享
Iframe通信
首先我們需要熟悉iframe的通信方式
window對象提供了postMessage函數,使用postMessage給子頁面,父頁面或者自己發送消息,通過在window對象上監聽message事件獲取收到的消息
window.postMessage("父頁面發送的消息"); // 發給了當前頁面
window.addEventListener(
"message",
console.log.bind(this, "父頁面收到信息") // 父頁面收到信息 MessageEvent
);
在postMessage(message, targetOrigin, transfer)函數中可以傳遞3個參數,分別是
- message:需要發送的消息
- targetOrigin:目標源,如:"http://127.0.0.1:5500/","*" 表示全部通配符
- transfer:取消深拷貝的數據,通過message發送對象是深拷貝的數據,會在目標頁面和當前頁面產生兩個對象,如果直接發送消息會十分損耗性能,使用transfer可以達到保存數據的功能
下面是個簡單的父子通信的例子
// 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類似,通過使用worker實例化對象進行傳遞消息,但是需要注意的是,iframe可以通過parent訪問父頁面,worker只能通過self對象將消息傳遞給父頁面中的實例worker,因此功能實現需要對其做兼容。下面是一段使用worker的代碼
// 父頁面
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實現頁面之間的通信,下面是思維導圖

上述設計中有Server,PeerToPeer,Client三個工具類,它們之間通過觀察者進行通信,然后通過MessageCenter傳遞異步任務
其中Server部署在父頁面中,實現監聽及發送消息的作用;Client部署在子頁面中,作用與Server類似,擔任消息發送與接收的職務;PeerToPeer的作用有兩個:一是擔任廣播群發消息的任務,二是收集廣播的消息。此外,Server與Client之間也可以直接通過message通信,子頁面之間通信通過PeerToPeer進行綁定
實現過程
了解了上面的基本概念與思路后,我們來將這個功能實現一下
MessageCenter類
作為消息收發的核心,許多地方用到了消息中心,具體實現方式可以參照之前的這篇文章
相對應Promise,其優點是可以觸發多段異步操作,既規避了回調函數的耦合,又解決了異步操作,是通信過程不可或缺的一個部分,我們的核心函數可以繼承該類并將其具象化,基于其中部分功能實現具體操作,那么整個程序設計可以使用多個鏈式調用的方式執行函數,如:
server
.mount()
.on("msg", console.log)
.on("msg1", console.log)
.on("msg2", console.log)
.load()
.catch(console.log);
IPC類
整個工具的核心是IPC(進程通信),其集合了前面說到的send發送消息,handleMessage接收消息,此外基于這兩點,我在類中增加了一些可能用的到的函數,來提升類靈活性與高可用性,如:mount函數用于手動掛載當前頁面監聽消息;unmount與前者相反,取消消息監聽卸載頁面;reset重置頁面信息,reset后需要重新實例化類;load用來監聽當前頁面是否加載完成;watchHandler和invokeHandler分別是“監聽函數類型消息并執行對應函數”與“發送函數類型消息觸發函數”
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類
Server類繼承自上面的IPC,此外Server還實現了target存取器,以及sendMethods存取器,由于Client與Server的部分功能不相同,所以在二者中分別實現,target的作用是開放一個入口給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
}
}
Client類
Client可以理解是Server的青春版,里面只有對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
是對原有功能的升級,實際上使用上述代碼即可達到各類通信的要求,但是有些操作需要系統性的調度與分發,此時一個簡單的分發器就比較重要了
PeerToPeer的主要實現有兩大模塊:一是多個子頁面互發消息,二是父頁面與多個子頁面互發消息達到多對多的消息傳遞或函數調用
PeerToPeer類的核心代碼是batchOperation和servers屬性,此時的server可以當成是一個工具類,可以使用該函數對子頁面進行批量操作
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)
}
}
功能演示
基礎功能
父子通信
父子雙向通信,通過Server類與Client類通過關聯直接發送消息
// 父頁面 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" } });
兄弟通信
同一個父頁面下的兩個子頁面稱為兄弟頁面,我們可以使用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的線程worker通信,下面是個例子,與iframe不同的是Client中傳入的目標和當前源都是self,在主線程中peer傳入的列表也是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進行信息標識,避免消息發送錯亂
// 父頁面
const server = new Server({
target: "#son",
id: 12,// 通過id標識發送的消息
});
// 子頁面
const client = new Client({
target: window.parent,
id: 12,// 子頁面不加id就收不到消息
});
卸載頁面
使用unMount取消當前頁面的監聽,取消掛載
client.unMount();
重置頁面
使用reset函數對頁面初始化,重置所有信息
client.reset()
批量執行
批量執行函數invoke是在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" });
總結
以上就是文章全部內容了,本文介紹了iframe和worker通過postMessage與onmessage進行通信,并基于此特性實現了進程通信的功能:IPC,在IPC類的基礎上我們做了拓展,衍生出Server和Client分別對應著服務端和客戶端的操作,此外我們還實現了端對端的批量操作P2P功能并對以上功能進行了演示。
8932dd4b-bbb0-42a8-a2c6-08ae334b2d7b
感謝你看到最后,希望文章對你有幫助,如果覺得文章還不錯的話,還請三連支持一下,感謝!
源碼:js-ipc: JavaScript通信工具包,支持Iframe和Worker通信
或:myCode: 基于js的一些小案例或者項目的js-ipc子模塊
NPM:js-ipc - npm

浙公網安備 33010602011771號