JS案例:前端Iframe及Worker通信解决思路

简介: 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功能并对以上功能进行了演示。

感谢你看到最后,希望文章对你有帮助,如果觉得文章还不错的话,还请三连支持一下,感谢!

相关文章
|
8天前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
|
8天前
|
移动开发 前端开发 JavaScript
前端实训,刚入门,我用原生技术(H5、C3、JS、JQ)手写【网易游戏】页面特效
于辰在大学期间带领团队参考网易游戏官网的部分游戏页面,开发了一系列前端实训作品。项目包括首页、2021校园招聘页面和明日之后游戏页面,涉及多种特效实现,如动态图片切换和人物聚合效果。作品源码已上传至CSDN,视频效果可在CSDN预览。
13 0
前端实训,刚入门,我用原生技术(H5、C3、JS、JQ)手写【网易游戏】页面特效
|
13天前
|
JavaScript 前端开发 开发者
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第27天】在前端开发领域,Vue.js和Angular是两个备受瞩目的框架。本文对比了两者的优劣,Vue.js以轻量级和易上手著称,适合快速开发小型到中型项目;Angular则由Google支持,功能全面,适合大型企业级应用。选择时需考虑项目需求、团队熟悉度和长期维护等因素。
19 1
|
24天前
|
前端开发 JavaScript 安全
JavaScript前端开发技术
JavaScript(简称JS)是一种广泛使用的脚本语言,特别在前端开发领域,它几乎成为了网页开发的标配。从简单的表单验证到复杂的单页应用(SPA),JavaScript都扮演着不可或缺的角色。
19 3
|
14天前
|
JavaScript 前端开发 API
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第26天】前端技术的飞速发展让开发者在构建用户界面时有了更多选择。本文对比了Vue.js和Angular两大框架,介绍了它们的特点和优劣,并给出了在实际项目中如何选择的建议。Vue.js轻量级、易上手,适合小型项目;Angular结构化、功能强大,适合大型项目。
14 1
|
17天前
|
前端开发 JavaScript UED
"前端小技巧大揭秘:JS如何将后台时间戳秒变亲切小时前、分钟前,让用户秒懂,提升互动体验!"
【10月更文挑战第23天】在Web开发中,将后台返回的时间戳转换为“小时前”、“分钟前”、“刚刚”等友好的时间描述是常见需求。本文介绍如何用JavaScript实现这一功能,通过计算当前时间和时间戳的差值,返回相应的描述,提升用户体验。
22 0
|
28天前
|
JSON 前端开发 JavaScript
Vue微前端新探:iframe优雅升级,扬长避短,重获新生
Vue微前端新探:iframe优雅升级,扬长避短,重获新生
93 0
|
28天前
|
JavaScript 前端开发 应用服务中间件
vue前端开发中,通过vue.config.js配置和nginx配置,实现多个入口文件的实现方法
vue前端开发中,通过vue.config.js配置和nginx配置,实现多个入口文件的实现方法
129 0
|
6月前
|
前端开发 JavaScript 网络协议
前端最常见的JS面试题大全
【4月更文挑战第3天】前端最常见的JS面试题大全
99 5
|
6月前
|
JavaScript 前端开发
web前端JS高阶面试题(1),高级开发工程师面试
web前端JS高阶面试题(1),高级开发工程师面试