UNPKG

broadcast-message

Version:

基于postMessage+BroadcastChannel+localStorage+互信域名的前端页面数据通信解决方案

664 lines (549 loc) 23 kB
/*! * @name BroadcastMessage.js * @description 基于postMessage+BroadcastChannel+localStorage+互信域名的前端页面数据通信解决方案 * @version 0.0.1 * @author xxxily * @date 2022/11/07 09:26 * @github https://github.com/xxxily */ import { parseURL, stringifyToUrl } from './utils/url' class BroadcastMessage { constructor(opts = {}) { /** * 指定消息发送的目标域,规则跟postMessage的targetOrigin一样 * 但不同的是支持定义数组形式的targetOrigin,从而实现批量跨域数据发送 * 当然如果是"*"的话就是给任意运行了本插件的页面发送数据 */ this.targetOrigin = opts.targetOrigin || location.origin /** * 指定数据中转传输使用的传输类型,可选值:BroadcastChannel、localStorage * 不指定的话,优先使用BroadcastChannel,在不兼容BroadcastChannel的浏览器下使用localStorage */ this.transportType = opts.transportType || 'BroadcastChannel' /** * 标识当前脚本是否处于可信域的页面上运行 * 如果是,当前页面作为可信域的中介页嵌入到具体运行环境中 */ this.inTrustedDomainPages = opts.inTrustedDomainPages || false // this.trustedDomainPages = 'https://h5player.anzz.top/demo/postMessage.html' this.trustedDomainPages = opts.trustedDomainPages || '' /** * 允许既是消息的发送页,也可以是消息的接收页,从而做到同源同页收发消息 * postMessage、BroadcastChannel和storage事件都是必须一个页面发送,另一个页面接收 */ this.allowLocalBroadcast = opts.allowLocalBroadcast || false this.channelId = String(opts.channelId || '*') /** * 实例id,当onMessage的handler被重复调用时,很可能是同名的channelId的多个实例导致的 * 所以加上实例id方便问题排查 */ this.instanceId = this.channelId + '_' + (window.performance ? performance.now() : Date.now()) this.debug = opts.debug || false this.emitOriginalMessage = opts.emitOriginalMessage || false this.messageWindow = window /* BroadcastMessage初始化就绪耗时 */ this.readyTime = 0 this.init(opts) } init() { /* 给trustedDomainPages的URL补充相关参数 */ if (this.trustedDomainPages) { const urlInfo = parseURL(this.trustedDomainPages) urlInfo.params.channelId = this.channelId urlInfo.params.instanceId = this.instanceId this.trustedDomainPages = stringifyToUrl(urlInfo) } this.__registerMessageWindow__() /* 如果标识了脚本处于可信域中运行,且已经被嵌入到iframe中,则注册相关监听器 */ if (this.inTrustedDomainPages && window !== top.window) { this.__registerPostMessageListener__() this.__registerStorageMessageListener__() this.__registerBroadcastChannelListener__() this.__sendMessageToParentWindow__('initReady') } } getTrustedDomain() { if (this.trustedDomainPages) { return parseURL(this.trustedDomainPages).origin } else { return location.origin } } /** * 给messageWindow的父页面发送消息,用来传递某些状态,例如告诉父页面:messageWindow初始化完成了,可以开始进行数据通信 * @param {String} msg * @returns */ __sendMessageToParentWindow__(msg) { if (window.parent === window || !msg) { return false } const channelId = window.__broadcastMessageChannelId__ || this.channelId const instanceId = window.__broadcastMessageInstanceId__ || this.instanceId window.parent.postMessage( { data: msg, channelId, instanceId, type: 'Internal-BroadcastMessage', }, '*' ) } __registerMessageWindow__() { if (this.messageWindow !== window) { return false } const iframe = document.createElement('iframe') iframe.style.display = 'none' iframe.style.visibility = 'hidden' iframe.src = this.trustedDomainPages iframe.className = 'broadcast-message-iframe' document.body.appendChild(iframe) this.messageWindow = iframe.contentWindow /** * 当存在trustedDomainPages时,trustedDomainPages应该引入该脚本,且执行初始化逻辑,否则将是无效的trustedDomainPages * 如果创建的是空白iframe,则需要将 * __registerPostMessageListener__、__registerStorageMessageListener__、__registerBroadcastChannelListener__ * 的代码逻辑注入到iframe里 */ if (!this.inTrustedDomainPages && !this.trustedDomainPages && this.messageWindow.document && this.messageWindow.document.write) { const document = this.messageWindow.document if (!document.body) { const body = document.createElement('body') body.innerHTML = '<h1>Broadcast-Message-page</h1>' document.documentElement.appendChild(body) } document.open() document.write(` <script> function ${this.__registerPostMessageListener__}; function ${this.__registerStorageMessageListener__}; function ${this.__registerBroadcastChannelListener__}; function ${this.__sendMessageToParentWindow__}; function init () { if (window.__hasInit__) { return false; } window.__broadcastMessageChannelId__ = "${this.channelId}"; window.__broadcastMessageInstanceId__ = "${this.instanceId}"; __registerPostMessageListener__(); __registerStorageMessageListener__(); __registerBroadcastChannelListener__(); __sendMessageToParentWindow__('initReady'); window.__hasInit__ = true ; } document.addEventListener("DOMContentLoaded", init); window.addEventListener("load", init) setTimeout(init, 100) </script> `) document.close() } } __registerPostMessageListener__() { const self = this if (self.__hasRegisterPostMessageListener__) { return false } /* 一定要处于iframe才会注册PostMessageListener */ if (window.top === window) { return false } /* 创建个随机的windowID,用于确定消息来源是否同属于一个windwo发出的 */ window.__windowId__ = String((Date.now() - Math.random() * 100000).toFixed(2)) if (!window.__broadcastMessageChannelId__) { /* 对于使用trustedDomainPages的情况,通过url参数来传递ChannelId和instanceId */ try { const urlInfo = parseURL(location.href) if (urlInfo.params.channelId && urlInfo.params.instanceId) { window.__broadcastMessageChannelId__ = urlInfo.params.channelId window.__broadcastMessageInstanceId__ = urlInfo.params.instanceId } else { throw new Error('URL缺失相关参数') } } catch (e) { console.error(`[registerPostMessageListener][${location.origin}] 获取broadcastMessageChannelId失败`, e) } } /* 消息中转传输的iframe */ function messageIframe() { let messageIframe = document.querySelector('#message-transport-iframe') if (messageIframe) { return messageIframe } messageIframe = document.createElement('iframe') messageIframe.id = 'message-transport-iframe' messageIframe.style.display = 'none' messageIframe.style.visibility = 'hidden' document.body.appendChild(messageIframe) return messageIframe } /** * 通过当前页的子iframe来执行消息传送逻辑,这样当前页面的window对象才会接收到到storage事件或BroadcastChannel消息 * 如果直接执行消息传送逻辑,则还需要创建个子iframe来接受storage事件或BroadcastChannel的消息,会导致导致需要更多层级的数据传递 */ function transportMessage(event) { /* 给将要中转传输的数据补充相关信息字段 */ const message = event.data message.windowId = window.__windowId__ // message.debug && console.log(`[transportMessage][iframe][${location.origin}]`, messageEvent) // 消息被消费了就会导致后面的逻辑没法执行,所以不能打印event message.debug && console.log(`[transportMessage][iframe][${location.origin}]`) const iframeWindow = messageIframe().contentWindow const broadcastChannelUsable = iframeWindow.BroadcastChannel && iframeWindow.BroadcastChannel.prototype.postMessage /* 优先使用BroadcastChannel进行消息传递 */ if (broadcastChannelUsable && message.transportType !== 'localStorage') { const bcInstance = iframeWindow.__BroadcastChannelInstance__ || new iframeWindow.BroadcastChannel('__BroadcastChannelMessage__') iframeWindow.__BroadcastChannelInstance__ = iframeWindow.__BroadcastChannelInstance__ || bcInstance bcInstance.postMessage(message) } else { iframeWindow.localStorage.setItem('__BroadcastMessage__', JSON.stringify(message)) } } messageIframe() /* 处理从window.parent传过来的内部消息 */ function internalBroadcastMessageHandler(event) { const message = event.data if (!message) { return false } if (message.data === 'readyTest') { window.__broadcastMessageReadyInfo__ = message const sendMessageToParentWindow = self.__sendMessageToParentWindow__ || window.__sendMessageToParentWindow__ if (sendMessageToParentWindow instanceof Function) { /* 发送握手成功消息 */ sendMessageToParentWindow('ready') } } } /* 注册postMessage的侦听事件,并将接收到的数据交给消息中转传输逻辑传送出去或进行内部处理 */ window.addEventListener( 'message', (event) => { const message = event.data if (!message || !message.type) { return false } if (message.type === 'BroadcastMessage') { transportMessage(event) } else if (message.type === 'Internal-BroadcastMessage') { internalBroadcastMessageHandler(event) } }, true ) this.__hasRegisterPostMessageListener__ = true } __registerBroadcastChannelListener__() { if (!window.BroadcastChannel || !BroadcastChannel.prototype.postMessage) { console.error(`[BroadcastChannel][${location.origin}]`, '不支持BroadcastChannel') return false } if (this.__BroadcastChannelInstance__) { return true } const BroadcastChannelInstance = new BroadcastChannel('__BroadcastChannelMessage__') BroadcastChannelInstance.addEventListener('message', (event) => { const message = event.data /* 校验数据字段 */ if (!message || !message.windowId || !message.data) { return false } const channelId = window.__broadcastMessageChannelId__ if (!message.channelId || (channelId && message.channelId !== channelId)) { message.debug && console.info('[transportMessage] channelId不存在或不匹配,禁止数据向上传递', channelId, message) return false } if (!message.allowLocalBroadcast && window.__windowId__ && window.__windowId__ === message.windowId) { message.debug && console.info('[BroadcastChannel-event] 消息源接收端和消息源的来源端一致,禁止数据向上传递') return false } /* 将接受到的事件数据通过postMessage传递回给上层的window */ const targetOriginList = Array.isArray(message.targetOrigin) ? message.targetOrigin : [message.targetOrigin] targetOriginList.forEach((targetOrigin) => { /* 检查当前的BroadcastMessage被哪个父页面嵌套,当父页面的地址和targetOrigin不匹配时,不向上传递数据 */ const readyInfo = window.__broadcastMessageReadyInfo__ || null if (targetOrigin !== '*' && (!readyInfo || !readyInfo.referrer.startsWith(targetOrigin))) { if (message.instanceId !== readyInfo.instanceId) { message.debug && console.warn( `[BroadcastChannel-event] 消息的targetOrigin和当前父页面的地址不匹配,取消数据向上传递,[targetOrigin]${targetOrigin} [parent page]${readyInfo.referrer}` ) } return false } window.parent.postMessage(message, targetOrigin) }) }) this.__BroadcastChannelInstance__ = BroadcastChannelInstance } __registerStorageMessageListener__() { if (this.__hasRegisterStorageListener__) { return false } window.addEventListener('storage', (event) => { let message = event.newValue /** * 空的newValue代表数据被删除,此时不应该传递给上层window * 如果不是由__BroadcastMessage__引起的事件也不应该传递给上层window */ if (!message || event.key !== '__BroadcastMessage__') { return false } try { message = JSON.parse(message) } catch (e) { // BroadcastMessage的数据必须是可以JSON.parse解析的,否则说明数据格式不对 // console.error('[storage-event][parse-error]', message, e) return false } /* 再次校验数据字段 */ if (!message || !message.windowId || !message.data) { return false } const channelId = window.__broadcastMessageChannelId__ if (!message.channelId || (channelId && message.channelId !== channelId)) { message.debug && console.error('[transportMessage] channelId不存在或不匹配,禁止数据向上传递', channelId, message) return false } message.debug && console.log(`[storage-event][iframe][${location.origin}]`, event) if (!message.allowLocalBroadcast && window.__windowId__ && window.__windowId__ === message.windowId) { message.debug && console.info('[storage-event] 消息源接收端和消息源的来源端一致,禁止数据向上传递') return false } /* 将接受到的事件数据通过postMessage传递回给上层的window */ const targetOriginList = Array.isArray(message.targetOrigin) ? message.targetOrigin : [message.targetOrigin] targetOriginList.forEach((targetOrigin) => { /* 检查当前的BroadcastMessage被哪个父页面嵌套,当父页面的地址和targetOrigin不匹配时,不向上传递数据 */ const readyInfo = window.__broadcastMessageReadyInfo__ || { referrer: '' } if (targetOrigin !== '*' && !readyInfo.referrer.startsWith(targetOrigin)) { if (message.instanceId !== readyInfo.instanceId) { message.debug && console.warn( `[storage-event] 消息的targetOrigin和当前父页面的地址不匹配,取消数据向上传递,[targetOrigin]${targetOrigin} [parent page]${readyInfo.referrer}` ) } return false } window.parent.postMessage(message, targetOrigin) }) }) this.__hasRegisterStorageListener__ = true } postMessage(message, messageType) { /* 初始化未就绪前,把需要post出去的数据先预存起来,等Ready之后再发送出去 */ if (!this._isReady_ && messageType !== 'Internal-BroadcastMessage') { if (!this._message_cache_) { this.ready(() => { if (Array.isArray(this._message_cache_)) { this._message_cache_.forEach((message) => { this.postMessage(message) }) delete this._message_cache_ } }) } this._message_cache_ = this._message_cache_ || [] this._message_cache_.push(message) return true } const data = { data: message, type: messageType || 'BroadcastMessage', origin: location.origin || top.location.origin, targetOrigin: this.targetOrigin, referrer: location.href || top.location.href, timeStamp: window.performance ? performance.now() : Date.now(), transportType: this.transportType, allowLocalBroadcast: this.allowLocalBroadcast, channelId: this.channelId, instanceId: this.instanceId, debug: this.debug, } if (!this.messageWindow || !this.messageWindow.postMessage) { this.debug && console.error('[messageWindow error] 无法发送message', data, this.messageWindow) return false } const trustedDomain = this.getTrustedDomain() this.messageWindow.postMessage(data, trustedDomain) } onMessage(handler) { this.__messageListener__ = this.__messageListener__ || [] if (handler instanceof Function && !this.__messageListener__.includes(handler)) { this.__messageListener__.push(handler) } if (this.__hasMessageListener__) { return false } this.__hasMessageListener__ = true window.addEventListener( 'message', (event) => { /** * 此处是最终的消息出口 * 属于数据安全的最后一道防线,也是最脆弱的防线 * 需要对原始数据进行调整或和过滤后才能给handler使用 * 数据传送上来之前应该尽可能避免无关的数据被传到这里 **/ const message = event.data const isBroadcastMessage = message && message.type === 'BroadcastMessage' && message.data && message.channelId && message.referrer /* 排除其它postMessage逻辑发送过来的无关数据 */ if (!isBroadcastMessage) { return false } /** * 不同频道的数据在传送过来前就应该被取消掉,这里是为了加一道保险 * 实际上非同源的不同频道的数据来到这里,说明数据已经不安全了,需完善脚本逻辑 */ if (this.channelId !== '*' && message.channelId !== this.channelId) { if (message.origin !== location.origin) { message.debug && console.error('[messageListener] 存在数据安全隐患,请完善脚本逻辑', this.channelId, event) } return false } const fakeEvent = {} try { for (const key in event) { let value = event[key] if (key === 'data' && !this.emitOriginalMessage) { value = message.data } else { if (key === 'type') { value = 'BroadcastMessage' } } Object.defineProperty(fakeEvent, key, { enumerable: key === 'data', writable: false, configurable: true, value: value, }) } } catch (e) {} this.__messageListener__.forEach((handler) => { handler instanceof Function && handler(fakeEvent) }) }, true ) } offMessage(handler) { this.__messageListener__ = this.__messageListener__ || [] const tempStorageListener = [] this.__messageListener__.forEach((item) => { if (item !== handler) { tempStorageListener.push(item) } }) this.__messageListener__ = tempStorageListener } postMessageToInternal(message) { this.postMessage(message, 'Internal-BroadcastMessage') } /** * 侦听来自messageWindow的内部通信信息,主要用于脚本内部逻辑的状态传递和数据同步等,一般来说业务层无需监听内部消息 */ onInternalMessage(handler) { this.__internalMessageListener__ = this.__internalMessageListener__ || [] if (handler instanceof Function && !this.__internalMessageListener__.includes(handler)) { this.__internalMessageListener__.push(handler) } if (this.__hasInternalMessageListener__) { return false } this.__hasInternalMessageListener__ = true window.addEventListener( 'message', (event) => { const message = event.data const isInternalMessage = message && message.type === 'Internal-BroadcastMessage' && message.channelId === this.channelId && message.instanceId === this.instanceId /* 排除其它postMessage逻辑发送过来的无关数据 */ if (!isInternalMessage) { return false } this.__internalMessageListener__.forEach((handler) => { handler instanceof Function && handler(event) }) }, true ) } offInternalMessage(handler) { this.__internalMessageListener__ = this.__internalMessageListener__ || [] const tempStorageListener = [] this.__internalMessageListener__.forEach((item) => { if (item !== handler) { tempStorageListener.push(item) } }) this.__internalMessageListener__ = tempStorageListener } addEventListener(type, listener) { if (type !== 'message') { return false } this.onMessage(listener) } removeEventListener(type, listener) { if (type !== 'message') { return false } this.offMessage(listener) } ready(handler) { if (this._isReady_) { if (handler instanceof Function) { handler(true) } return true } if (!this.__readyHandler__) { this._readyStartTime_ = Date.now() this.__readyHandler__ = [] const readyHandler = (event) => { const message = event.data if (message.data === 'initReady') { /** * 发送握手消息,测试是否真的ready * 这也是给messageWindow初始化成功后发的第一条信息,可以帮助messageWindow完善初始化信息 */ this.postMessageToInternal('readyTest') } else if (message.data === 'ready') { this._isReady_ = true this.readyTime = Date.now() - this._readyStartTime_ delete this._readyStartTime_ if (this.debug) { console.log(`[BroadcastMessage][ready] 耗时:${this.readyTime}`) } this.__readyHandler__.forEach((handler) => { if (handler instanceof Function) { handler(true) } }) /* 解绑readyHandler */ delete this.__readyHandler__ this.offInternalMessage(readyHandler) } } this.onInternalMessage(readyHandler) } if (handler instanceof Function) { this.__readyHandler__.push(handler) } else { return new Promise((resolve, reject) => { this.__readyHandler__.push(resolve) }) } } close() { if (this.__BroadcastChannelInstance__ && this.__BroadcastChannelInstance__.close) { this.__BroadcastChannelInstance__.close() } if (this.messageWindow) { document.body.removeChild(this.messageWindow) } this.__messageListener__ = [] this.__readyHandler__ = [] } } export default BroadcastMessage