浏览器插件-父子页面跨域通信封装
浏览器插件-父子页面跨域通信封装
问题描述 和遇到的问题 以及解决方案
问题描述: 主页面和子页面通信的时候可能存在跨域 会导致消息发不过去 通过 postmessage 可以发送数据 弊端是不是同步的 导致交互起来比较麻烦
想法: 每次发送数据的时候 生成一个 id (通过这个 id 关联发送和返回的数据) 其他页面处理完成后 通过 senderWin 把数据返回来,这边接受到消息后 通过这个 id 把发送数据的这个 promise 处理成 resolve 这时候有个问题:希望的是 发送消息后 能知道 接收方是否已经收到, 然后还能接受到接收方的处理完的结果 解决方案:接收方接受到消息后立刻会返回一个 copy 的命令 发送方知道这个 copy 的命令后 会继续生成一个 promise 把这个 promise 的解决状态给封装一层 然后外面就可以继续 await 这个 promise 而拿到的就是处理完的结果
实际代码
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') } }