messager.js

/**
 * 跨域页面消息通信
 * @module $ui/utils/messager
 * @author 陈华春
 */

import {save, LOCAL} from './storage'

export const MESSAGE_TYPE = '__MY_WEB_MESSAGER__'

/**
 * 跨域页面消息通信类
 * @class
 */
export default class Messager {
  /**
   *
   * @param {object} options 实例化参数选项
   * @param {Window} [options.target] 通信目标,使用桥通讯时可以不传
   * @param {string} [options.bridge] 目标桥页面url,如需要跨浏览器窗口传输数据,必须要设置
   * @param {string} [options.origin] 自身的桥,目标可以通过桥联系到自己
   * @param {function} [options.ready] 桥页面加载完成时回调函数
   */
  constructor(options) {
    const o = this.options = {
      target: window,
      bridge: null,
      origin: null,
      ready: null,
      ...options
    }
    /**
     * 通信目标对象
     * @property {Window} target
     * @type {Window | *}
     */
    this.target = o.target
    this.handlers = {}
    this.proxyBridgeHandler = this.bridgeHandler.bind(this)
    window.addEventListener('storage', this.proxyBridgeHandler)
    if (o.bridge) {
      this.buildBridge().then((el) => {
        this.target = el.contentWindow
        this.el = el
        o.ready && o.ready(this)
      })
    } else {
      o.ready && o.ready(this)
    }
  }
  
  /**
   * 侦听消息
   * @param {string} type 消息类型
   * @param {Function} handler 处理函数
   */
  on(type, handler) {
    const listener = function (evt) {
      // message: {type:'XDH_WEB_MESSAGER', data:{type:'事件类型', data:{真正发送的数据},bridge:'' }}
      const message = evt.data || {}
      if (!message) return
      if (message.type === MESSAGE_TYPE && message.data.type === type) {
        handler(message.data.data, message.data.bridge)
      }
    }
    
    if (!this.handlers[type]) {
      this.handlers[type] = []
    }
    this.handlers[type].push(listener)
    window.addEventListener('message', listener)
  }
  
  /**
   * 取消侦听
   * @param {string} [type] 消息类型
   * @param {Function} [handler] 处理函数
   */
  off(type, handler) {
    // 制定类型和事件句柄
    if (type && handler) {
      const handlers = this.handlers[type] || []
      handlers.forEach((h, index) => {
        if (handler === h) {
          handlers.splice(index, 1)
          window.removeEventListener('message', h)
        }
      })
      // 制定事件类型
    } else if (type) {
      const handlers = this.handlers[type] || []
      handlers.forEach(h => {
        window.removeEventListener('message', h)
      })
      delete this.handlers[type]
      // 全不制定,全部取消侦听
    } else {
      Object.keys(this.handlers).forEach(key => {
        this.off(key)
      })
    }
  }
  
  /**
   * 发送消息
   * @param {string} [type] 消息类型
   * @param {Object} [data] 发送数据
   */
  fire(type, data) {
    if (!this.target) return
    
    const message = {
      type: MESSAGE_TYPE,
      data: {
        type,
        data,
        bridge: this.options.origin
      }
    }
    // 桥转发时,数据包多一层
    if (this.options.bridge) {
      const bridge = {
        type: MESSAGE_TYPE,
        data: message
      }
      this.target.postMessage(bridge, '*')
    } else {
      this.target.postMessage(message, '*')
    }
  }
  
  /**
   * 接收消息,只接收一次
   * @param {string} [type] 消息类型
   * @param {Function} [handler] 处理函数
   */
  once(type, handler) {
    this.on(type, () => {
      handler.apply(this, arguments)
      this.off(type, handler)
    })
  }
  
  /**
   * 搭桥,创建iframe,加载桥页面
   * @return {Promise<any>}
   */
  buildBridge() {
    return new Promise((resolve, reject) => {
      const el = document.createElement('iframe');
      el.style.display = 'none'
      el.setAttribute('src', this.options.bridge + '?t=' + new Date().getTime())
      el.onload = () => {
        resolve(el)
      }
      el.onerror = (e) => {
        reject(e)
      }
      document.body.appendChild(el)
    })
    
  }
  
  /**
   * 桥页面传输写入localStorage
   * @param {object} message
   * @param {string} origin 消息来源
   */
  pass(message) {
    save(MESSAGE_TYPE, {
      message: message,
      __t__: (new Date()).getTime()
    }, LOCAL)
  }
  
  /**
   * localStorage 事件处理函数
   * @param {object} evt
   */
  bridgeHandler(evt) {
    if (evt.key !== MESSAGE_TYPE) return
    const value = JSON.parse(evt.newValue)
    if (value && value.message) {
      const message = value.message
      const handlers = this.handlers[message.type] || []
      handlers.forEach(handler => {
        handler({
          data: {
            type: MESSAGE_TYPE,
            data: message
          }
        })
      })
    }
  }
  
  /**
   * 销毁
   */
  destroy() {
    this.off()
    if (this.proxyBridgeHandler) {
      window.removeEventListener('storage', this.proxyBridgeHandler)
    }
    if (this.el) {
      this.el.parentNode.removeChild(this.el)
    }
  }
}