关于浏览器插件开发过程中注入脚本与页面内容间的通信

关于浏览器插件开发过程中注入脚本与页面内容间的通信

说明

最近在做 safari 浏览器扩展及 360 浏览器扩展的开发,发现与之前的 chrome 扩展不同的是,在低版本的 safari (12) 与低版本的 360 浏览器中,使用 window.postMessage() 进行 注入脚本与页面之间的通信,是不成功的。

为了实现通信功能,我们还需要使用一个比较 hack 的方式,这个方式也是 Chrome 与 360 官方文档中建议使用的方式:

  • 创建一个 dom 元素,将这个 dom 元素的 innerText 作为存储数据的介质
  • 然后给这个 dom 元素创建自定义的事件(document.createEvent(‘Event’))
  • 传递数据时,先将 innerText 更新,然后厨房这个自定义的事件( dom.dispatchEvent(event) )
  • 通过 dom.addEventListener() 监听事件后进行后续的操作

实现

由于插入脚本中的 window 与页面内容的 window 在某些浏览器上有一定的隔离,并不能共享属性或者方法,因此要实现两者的互相通信,需要在两侧都注册一个自定义事件,然后两者之间按照约定好的 事件名进行事件监听。

按照以上逻辑我封装了一个类用来建立插入脚本与页面内容之间的通信平台 (用了 flow 做类型声明,不需要的可以删除)模仿了 window.postMessage 的 api , 实现了在 window.postMessage 可用是优先使用,不可用时使用 dom 节点做中转的方法

// @flow

/*
 * 页面与注入脚本之间的通信平台
 * */

class CommunicationPlatform {
  communicationPointId: string;
  communicationPoint: HTMLDivElement;
  fromEventName: string;
  toEventName: string;
  communicationEvent: CustomEvent;
  isPostMessageUseful: boolean;

  constructor(communicationPointId: string, event: {from: string, to: string}) {
    this.communicationPointId = communicationPointId;
    this.communicationPoint = document.getElementById(communicationPointId);
    this.isPostMessageUseful = this.judgeHasPostMessage();
    // postMessage 是优先使用的
    if (!this.isPostMessageUseful) {
      this.fromEventName = event.from;
      this.toEventName = event.to;
      this.communicationPoint && this.registerExtensionEvent();
    }
  }

  registerExtensionEvent() {
    this.communicationEvent = document.createEvent('Event');
    this.communicationEvent.initEvent(this.toEventName, true, true);
  }

  addEventListener = (listener: (result: {data: Object}) => void) => {
    if (typeof listener !== 'function') return;

    if (this.isPostMessageUseful) {
      return window.addEventListener('message', listener);
    }

    const point = this.communicationPoint;
    if (point && typeof point.addEventListener === 'function') {
      point.addEventListener(this.fromEventName, () => {
        const eventData = this.parseEventData(point.innerText);
        // 模拟与 window.addEventListener('message', ({data, source}) => {}) 一样的数据形式
        listener({data: eventData || {}, source: window});
      });
    }
  };

  postMessage = (data: Object) => {
    if (this.isPostMessageUseful) {
      return window.postMessage(data);
    }

    const point = this.communicationPoint;
    if (point && typeof point.dispatchEvent === 'function') {
      point.innerText = this.transferDataToString(data) || '';
      point.dispatchEvent(this.communicationEvent);
    }
  };

  // 主要是 extension 中用, 不做这个判断也不会导致崩溃
  hasPoint = (): boolean => {
    return !!this.communicationPoint;
  };

  judgeHasPostMessage() {
    let hasPostMessage = true;
    try {
      window.postMessage({type: 'TEST_POST_MESSAGE_USEFUL'});
    } catch (e) {
      hasPostMessage = false;
    }

    return hasPostMessage;
  }

  parseEventData(eventData: ?string): Object | null {
    let result = null;
    if (typeof eventData === 'string') {
      try {
        const temp = eventData; // 这里可以做个加密的处理
        result = JSON.parse(temp);
      } catch (err) {
        console.error(err);
      }
    }
    return result;
  }

  transferDataToString(data: Object): string | null {
    let result = null;
    try {
      const temp = JSON.stringify(data); // 这里可以做个解密的处理
      result = temp;
    } catch (err) {
      console.error(err);
    }
    return result;
  }
}

export default CommunicationPlatform;

页面内容部分使用


// 创建一个通信节点
const extensionCommunicationPointId = 'xxx_extension_communication_point';
const extensionCommunicationPoint = document.createElement('div');
_.set(extensionCommunicationPoint, 'id', extensionCommunicationPointId);
_.set(extensionCommunicationPoint, 'style.display', 'none'); // 隐藏显示
document.body.appendChild(extensionCommunicationPoint);

// 给 window 挂载一个用来与浏览器插件注入脚本通信的对象
window.CommunicationPlatform = new CommunicationPlatform(extensionCommunicationPointId, {
  from: 'communicationPageMessage', // 页面部分的事件名,用来触发事件
  to: 'communicationInjectMessage', // 脚本部分的事件名, 用来监听来自这个事件名的事件
});

// 向 注入脚本传递事件
window.CommunicationPlatform.postMessage(data);

// 接收来自注入脚本的事件
window.CommunicationPlatform.addEventListener((event: {source: Object, data: Object}) => {})

注入脚本部分使用

const InjectCommunicationPlatform = new CommunicationPlatform(
  'xxx_extension_communication_point',
  {from: 'communicationInjectMessage', to: 'communicationPageMessage'},
);

const hasPoint = InjectCommunicationPlatform &&
  typeof InjectCommunicationPlatform.hasPoint === 'function' &&
  InjectCommunicationPlatform.hasPoint();
const hasPostMessage = InjectCommunicationPlatform.judgeHasPostMessage();

if (hasPoint || hasPostMessage) {
    InjectCommunicationPlatform.postMessage(data); // 向页面发出消息
    InjectCommunicationPlatform.addEventListener((event: {source: Object, data: Object}) => {}) // 接收页面的消息
}

你可能感兴趣的:(浏览器插件,chrome,360,extension,safari,浏览器插件通信)