avatar

理解 postMessage 通信

postMessage 是一种安全的跨源通信方法,用于在两个不同的浏览上下文(如不同的窗口、iframe、标签页)之间发送消息。

使用 postMessage 相互通信的每个源都需要有一个 message 监听器,调用对方的 postMessage 方法来向对方传递信息,对方的message 监听器根据 eventorigin 来过滤它想要的信息。

发送消息

在使用 postMessage 时,首先需要确定要发送消息的目标窗口。可以通过 window.open 打开的窗口对象,或者通过 document.getElementById 获取的 iframe 元素的 contentWindow 属性来获取目标窗口。

// 获取目标窗口对象
const target = document.getElementById('iframe').contentWindow;

// 发送消息
target.postMessage('i am parent', 'https://example.com');

接收消息

接收消息的一方需要监听 message 事件,通过 event.data 来获取发送过来的数据。

window.addEventListener('message', function(event) {
  if (event.origin === 'https://example.com') {
    console.log('message:', event.data);
  }
}, false);

双向通信的典型例子

// parent page

// 发送数据给 child
const iframe = document.getElementById('iframe');
iframe.contentWindow.postMessage('this is parent', 'http://www.sourceB.com');

// 接收来自 child 的数据,其实不单单是子 iframe 的数据
// 而是可以来自任何源的数据,所以要对源鉴别一下
// 来自 parent 自己的消息也是可以的,不信可以试一试
window.addEventListener('message', function (event) {
  if (event.origin === 'http://www.sourceB.com') {
    console.log('data from child:', event.data);
  }
});
// child page
window.addEventListener('message', function (event) {
  if (event.origin === 'http://www.sourceA.com') {
    console.log('data from parent:', event.data);

    // 处理数据
    const result = event.data + ' I am from child.';

    // 返回数据给 parent
    event.source.postMessage(result, event.origin);
  }
});

我们假设一个 h5 开发的场景,parent 是一个原生 app 中的容器页面,child 是一个内嵌的页面,内嵌页面需要从原生 app 获取一些必要信息来进行展示,它们之间的通信流程是类似以下的:内嵌页面通过 postMessage 向容器页面发送一个 event,容器页面通过 message 监听器获取这个 event,然后它调用原生 app 的方法处理这个 event 获得结果,容器页面再通过 postMessage 向内嵌页面发送结果,内嵌页面通过 message 监听器获取这个结果。好了,内嵌页面终于获得了它想要的信息,想想这有什么问题呢,能不能优化下?

child 和 parent 的通信方式的处理模型是不是和后端响应前端 API 请求一样的,但对于 API 请求,前端只需要一句 const response = await fetch(url) 就好了,并不需要如此多的异步代码。这般的异步代码,逻辑并不类聚,带来更大的维护成本,我们来优化吧,把它变得如同同步代码一般。

Preview Demo on StackBlitz

优化

把异步代码变成用同步的方式书写,无他,得利用 promise 封装一下,使其用起来像同步代码一样清晰。

class Bridge {
  constructor() {
    this.messageHandlers = new Map();
    this.messageId = 0;

    // listen for messages from parent
    window.addEventListener("message", (event) => {
      const { type, messageId, data } = event.data;

      const handler = this.messageHandlers.get(messageId);
      if (handler) {
        handler(data);
        this.messageHandlers.delete(messageId);
      }
    });
  }

  // send message and wait for response
  async callHandler(actionName, data = {}) {
    return new Promise((resolve) => {
      const messageId = this.messageId++;

      this.messageHandlers.set(messageId, resolve);
      window.parent.postMessage(
        {
          type: actionName,
          messageId,
          data,
        },
        "*"
      );
    });
  }
}

Bridge 的帮助下,我们上述的例子就可以如下实现,是不是清晰了许多,不需要弯弯绕绕。

const bridge = new WebViewBridge();
const userInfo = await bridge.callHandler('getUserInfo');
Preview Demo on StackBlitz