浏览器插件-父子页面跨域通信封装

浏览器插件-父子页面跨域通信封装

  1. 问题描述 和遇到的问题 以及解决方案

    问题描述: 主页面和子页面通信的时候可能存在跨域 会导致消息发不过去 通过 postmessage 可以发送数据 弊端是不是同步的 导致交互起来比较麻烦

    想法: 每次发送数据的时候 生成一个 id (通过这个 id 关联发送和返回的数据) 其他页面处理完成后 通过 senderWin 把数据返回来,这边接受到消息后 通过这个 id 把发送数据的这个 promise 处理成 resolve 这时候有个问题:希望的是 发送消息后 能知道 接收方是否已经收到, 然后还能接受到接收方的处理完的结果 解决方案:接收方接受到消息后立刻会返回一个 copy 的命令 发送方知道这个 copy 的命令后 会继续生成一个 promise 把这个 promise 的解决状态给封装一层 然后外面就可以继续 await 这个 promise 而拿到的就是处理完的结果

  2. 实际代码

    interface CallBack<T = any> {
        id: string // 唯一标识
        message: T // 数据
        t: number // 定时器
        isCallback: 0 | 1 | 2 // 0 是默认 1是callback返回 2是执行完成
        resolve: (response: any) => void
    }
    interface RealMessage<T> {
        type: 'byChildren' | 'byParent'
        message: T
    }
    interface RequestsFuncArgs<T = any> {
        message: RealMessage<T>
        id: string
        resolve?: (response: T) => void
        type: 'send'
    }
    interface ResponseResult<T = any> {
        message: T
        id: string
        type: 'response' | 'copy'
    }
    
    class WebChannel {
        messageReBack: CallBack[] // 存储命令来回发送的状态数据的村粗
        constructor({
            callByChildren,
            callByParent,
        }: {
            callByChildren: (message: any, id: string, sendWin: MessageEventSource | null) => Promise<any> // 由自己的子页面发过来的消息 需要处理的方法
            callByParent: (message: any, id: string, sendWin: MessageEventSource | null) => Promise<any>
            // 由自己的父页面发过来的消息 需要处理的方法
        }) {
            this.messageReBack = []
            window.addEventListener('message', async (ev: WindowEventMap['message']) => {
                const { data, source: sendWin } = ev
    
                if (data.type === 'send') {
                    // 如果是send的数据
                    const requests = data as RequestsFuncArgs
                    const { id, message = { type: '没有收到 message' } } = requests
                    // 先发送数据给发送者 代表自己已经收到消息
                    this.sendCopyToTarget(id, sendWin)
                    let result = null
                    if (message.type === 'byChildren') {
                        //交给传过来的 方法去处理 返回的一个 promise
                        result = await callByChildren(requests.message, requests.id, sendWin)
                    } else if (message.type === 'byParent') {
                        //交给传过来的 方法去处理 返回的一个 promise
                        result = await callByParent(requests.message, requests.id, sendWin)
                    }
                    // 将结果直接发给发送方 带上id和message 以及加上是response 结果
                    this.senderPostmessageChannel(sendWin, { message: result, id, type: 'response' })
                } else if (data.type === 'response') {
                    // 表示收到的接受方处理完的结构
                    const response = data as ResponseResult
                    const { id, message = { type: '没有收到 message' } } = response
                    // 直接丢给方法去处理
                    this.gotMessageByTarget(id, message)
                } else if (data.type === 'copy') {
                    // 表示 自己是发送方 发送的数据已经到了接受方 这是接受方回了个copy
                    const response = data as ResponseResult
                    const { id } = response
                    //  这时候 让发送者改变这个消息的 isCallback => 1 代表其收到消息 并且结束掉 自己的定时器(100ms后会让这次消息 返回false)
                    this.gotMessageByTarget(id)
                }
            })
        }
        gotMessageByTarget(id: string, message?: any) {
            // 先找到是哪条消息已经收到了copy
            const rowIndex = this.messageReBack.findIndex((item) => item.id === id)
            const row = this.messageReBack[rowIndex]
    
            if (row && row.isCallback === 0) {
                // 改变 isCallback 表示自己已经知道了 下次过来的数据就是 这条消息的处理完的结果
                row.isCallback = 1
                // 删掉定时器 如果不删 会在100ms后 直接resolve false出去表示接受方没有收到消息
                row.t && clearTimeout(row.t)
                // 记录 这个发送的时候的promise
                const callResolve = row.resolve
                // 直接返回出去 同时 把message改成一个promise 这个promise的resolve 会在下次消息过来的时候执行 而完成 这样就会把消息返回给执行函数的那边
                callResolve({
                    message: new Promise((resolve) => {
                        row.resolve = resolve
                    }),
                    id,
                    type: 'copy',
                })
            } else if (row && row.isCallback === 1) {
                // 这边表示 已经copy了 然后 又执行到这 说明是接受方处理完了数据 这时候直接把message 返回出去 resolve 同时把这条消息的数组里面的清空 表示这次的postmessage 已经完成
                row.isCallback = 2
                row.resolve(message)
                this.messageReBack.splice(rowIndex, 1)
            }
        }
        sendCopyToTarget(id: string, win: MessageEventSource | null) {
            // 发送copy命令回去
            win?.postMessage(
                {
                    message: null,
                    id,
                    type: 'copy',
                },
                { targetOrigin: '*' }
            )
        }
        senderPostmessageChannel<T = any>(senderWin: MessageEventSource | null, opt: ResponseResult<T>) {
            // 发送处理完的结果出去
            senderWin?.postMessage({ ...opt, type: 'response' }, { targetOrigin: '*' })
        }
        postMessageChannel<T = any, R = any>(
            window: Window,
            message: T,
            type: 'byChildren' | 'byParent',
            id?: string
        ): Promise<false | ResponseResult<R>> {
            // 生成随机id 如果传了id 就用这个id
            const readId = id || this.getRandomId(type)
            // 构建 发送的实际数据 加上id 和type 微send
            const opt: RequestsFuncArgs<T> = {
                message: {
                    type,
                    message,
                },
                id: readId,
                type: 'send',
            }
            window?.postMessage(opt, '*')
            return new Promise((resolve) => {
                // 立刻开启定时器 如果上面 没有收到copy 就会返回false出去 如果收到copy 会把这个定时器取消掉
                const t = setTimeout(() => {
                    this.cancelResolve(readId)
                }, 100)
                // 加入到数组里面 储存 resolve 以及message 初始化这个消息
                this.messageReBack.push({
                    id: readId,
                    t,
                    message,
                    isCallback: 0,
                    resolve,
                })
            })
        }
        cancelResolve(id: string) {
            // 取消掉这个发送消息 并且resolve false出去 代表这次消息失败
            const rowIndex = this.messageReBack.findIndex((item) => item.id === id)
            const row = this.messageReBack[rowIndex]
    
            if (row && row.isCallback === 0) {
                row.resolve(false)
                this.messageReBack.splice(rowIndex, 1)
            }
        }
        getRandomId(type = 'system') {
            return `random:${Math.random()
                .toString(36)
                .slice(2)};randomNum:${Math.random()};time:${new Date().getTime()};type:${type}`
        }
        getAllIframes() {
            // 获取所有的iframe
            return Array.from(window.document.querySelectorAll('iframe') as NodeListOf<HTMLIFrameElement>)
        }
        async postMessageToChildren<T = any, R = any>(
            message: T,
            target?: Window | null
        ): Promise<false | ResponseResult<Promise<R>>> {
            // 发消息给子页面 如果传了window 代表指定转给谁 不然就是发给所有的子页面
            if (target) {
                return await this.postMessageChannel(target, message, 'byParent')
            }
            const iframes = this.getAllIframes()
            for (const i of iframes) {
                i.contentWindow && this.postMessageChannel(i.contentWindow, message, 'byParent')
            }
            return false
        }
        async postMessageToParent<T = any, R = any>(message: T): Promise<false | ResponseResult<Promise<R>>> {
            // 发消息给父页面 这表需要判断 top === self 表示已经在最上层了
            if (top === self) {
                // 表示 已经是最上一层了 不能继续往上发了
                return false
            }
            return await this.postMessageChannel(window.parent, message, 'byChildren')
        }
    }
    

浏览器插件-父子页面跨域通信封装的相似文章