MessageChannel 与 postMessage

MessageChannel

MessageChannel 主要是通过其实例的 port1 和 port2 两个只读属性进行通信。在其中一个 port 上调用 postMessage() 方法,在另一个 port 的 onmessage 回调上就能获取到数据。

const { port1, port2 } = new MessageChannel()
port1.onmessage = e => {
  console.log('port1 received:', e.data)
}
port2.postMessage(
  'data from port2',
  // []
)

这里的 postMessage() 方法有一个可选参数 transfer,这个后面再说。

case1

const m1 = document.querySelector("#m1");
const m2 = document.querySelector("#m2");
const btn1 = document.querySelector("#btn1");
const btn2 = document.querySelector("#btn2");

const channel = new MessageChannel();
const { port1, port2 } = channel;

btn1.addEventListener("click", sendMessage1, false);
btn2.addEventListener("click", sendMessage2, false);

port1.onmessage = function(e) {
  m1.textContent = e.data;
}
port2.onmessage = function (e) {
  m2.textContent = e.data;
}

function sendMessage1 () {
  port1.postMessage("I am port1");
}
function sendMessage2 () {
  port2.postMessage("I am port2");
}

case2

// demo.js
const channel = new MessageChannel();
const { port1, port2 } = channel;
const oTitle = document.querySelector('h1');

port1.onmessage = (e) => {
  // 接受消息时,修改dom
  oTitle.textContent = e.data;
  port1.postMessage('dom rendered');
}

export default port2;
// index.js
import port2 from './demo.js';

;(() => {
  // 发送消息
  port2.postMessage('This is new title');
  port2.onmessage = (e) => {
    console.log(e.data);
  };
})();

postMessage

postMessage() 方法至少存在于以下几个对象上:

  • MessageChannel 实例的 port1/port2 属性上
  • window 对象,对 iframe 来说就是 contentWindow 属性
  • Web Worker 实例上
  • Service Worker 的 client 对象上

还有其他一些对象也提供了 postMessage() 方法,这里就不详细列举了。

下面以 iframe 为例简单介绍一下。

以 iframe 为例

方便起见,把主动发起通信的一方,或者说第一次发消息的一方称作发送方,另一方称为接收方。

在当前的场景下,因为是跨页面的,所以发送方发送消息时只能用 window 的 postMessage() 方法,接收方需要通过监听 window 上的 message 事件获取消息。

假设现在有两个页面,主页面地址为 http://localhost:8080/index.html,iframe 页面地址为 http://localhost:8080/iframe.html

下面示例演示的是,主页面向 iframe 发送消息,iframe 收到消息后回复主页面:

// index.html
const { contentWindow } = document.querySelector('iframe')
contentWindow.addEventListener('load', e => {
  contentWindow.postMessage(
    'data from index.html',
    '*',
    // []
  )
})
window.addEventListener("message", e => {
  console.log(e.data)
})

// iframe.html
window.addEventListener("message", e => {
  window.parent.postMessage(
    'response from iframe.html',
    '*'
  )
})

可以看到,这里的 postMessage() 参数比 MessageChannel 上的多了一个。现在该说一下 postMessage() 的参数了。

postMessage() 接受三个参数:

  • 第二个参数 targetOrigin 规定了目标窗口的 origin,* 表示不限制。如果做了限制,那么目标页面必须与 targetOrigin 同源,否则数据不能正常发送。
    如果把上面主页面发送时候的 * 改成 http://localhost:8081/iframe.html 就会报错了,因为端口不同。
  • 第三个参数 transfer 是搭配 MessageChannel 使用的,简单来说就是用来发送 port 的

既然 window.postMessage() 能够向接收方发送 port,再加上前面我们知道了 MessageChannel 的 port 也可以用来收发消息,这样一来,接收方回复消息的时候就多一种选择了。见示例:

// index.html
const { port1, port2 } = new MessageChannel()
const { contentWindow } = document.querySelector('iframe')
contentWindow.addEventListener('load', e => {
  contentWindow.postMessage(
    'data from index.html',
    '*',
    [port1] // 把 port1 发送给 iframe
  )
  // port1.postMessage('another data from index.html')
})
// 然后在 port2 上监听消息
port2.onmessage = e => {
  console.log('port2 received:', e.data)
}

// iframe.html
window.addEventListener("message", e => {
  const [port1] = e.ports
  // 通过 index.html 提供的 port1 回复消息
  port1.postMessage('response from iframe.html')
})

transfer 的一个注意事项是,把一个 port 发送出去之后,本页面就不再拥有这个 port 的操作权了。比如上面示例代码里注释掉的 port1.postMessage('another data from index.html') 这一行,因为 port1 已经发送出去了,所以这一行执行了是没有效果的。

另外,MessageChannel 上的 portMessage() 没有 targetOrigin 参数,只有 transfer。

> cd ..