前言

在前端开发中,有时候我们需要在两个页面间进行数据交换。如果两个页面属于同一个源,我们有很多办法可以实现这一目标。但是,当两个页面属于不同的源时,办法就很有限了。本文将介绍其中的一种方法,即使用postMessage在不同源的两个页面间进行数据交换,当然该方法也适用于同源的页面间。

很多接口(interface)都提供了postMessage方法,比如WindowMessagPortWorkerClientServiceWorkerBroadcastChannel等。首先要做的事情是分清楚不同postMessage的作用和应用场景,本文将结合在不同源的两个页面间进行数据交换的场景,介绍Window: postMessage()MessagePort: postMessage()方法。

Window: postMessage()

window.postMessage()方法可以安全地在不同源的window对象间进行通信。当然这有两个前提,

  1. 消息发送方需要拿到接收方的window对象,
  2. 消息接收方需要监听message事件

有两种方式可以满足前提1,分别是调用window.open()和使用<iframe>标签,因此也限制了window.postMessage()方法只能在这两种场景下使用。

window.open()返回被打开页面的window对象,被打开的页面则可以通过window.opener访问调用了window.open()方法的页面window对象。

主frame可以通过iframe.contentWindow访问iframe内部的window对象,iframe内部则可以通过window.parent访问主frame的window对象。

消息接收方可以通过收到消息中的source属性获得发送方的window对象。

API

发送方API

1
2
3
4
5
postMessage(message)
postMessage(message, targetOrigin)
postMessage(message, targetOrigin, transfer)

postMessage(message, options)
  • message参数很好理解,就是发送的消息,可以传递能够被结构化克隆[3]的对象,比如js中的大多数类型。
  • targetOrigin参数指定接收方的origin,只有和实际的接收方origin(包括协议、域名和端口)相同时,接收方才能收到该消息。默认取当前页面的origin,可以设置为*,表示所有源都可以收到该消息。
  • transfer参数表示将一组可转移对象(Transferable objects)[4]的控制权交给接收方,用于共享内存或者交换MessagePort对象。
  • options参数是一个对象,包含targetOrigintransfer属性。

接收方收到的参数

接收方可以通过监听message事件来接收消息。

1
2
3
4
5
6
7
8
9
window.addEventListener(
"message",
(event) => {
if (event.origin !== "http://example.org:8080") return;

// …
},
false,
);

event包含下列属性:

  • data:接收到的消息,对应发送方的message参数。
  • origin:消息发送方的origin,这里的目的其实是和发送方做相互校验。
  • source:消息发送方的window对象,可以用该对象建立双向通信通道。

🌰

  • 发送消息

    1
    2
    var o = document.getElementsByTagName('iframe')[0];
    o.contentWindow.postMessage('Hello world', 'https://b.example.org/');
  • 接收消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    window.addEventListener('message', receiver, false);
    function receiver(e) {
    if (e.origin == 'https://example.com') {
    if (e.data == 'Hello world') {
    e.source.postMessage('Hello', e.origin);
    } else {
    alert(e.data);
    }
    }
    }

MessagePort:postMessage()

Channel Messaging API目的是让两个不同上下文的脚本可以直接建立通道,进行双向通信。MessagePort则是使用该API建立通道后的两侧端点。

API

建立通道(MessageChannel)

1
const channel = new MessageChannel();

channel有两个属性,port1port2,分别是通道的两侧端点即MessagePort对象。具体的收发消息需要使用这两个端点来完成。

收发消息(MessagePort)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 发送消息
postMessage(message)
postMessage(message, transfer)
postMessage(message, options)

// 接收消息
addEventListener("message", (event) => {});

onmessage = (event) => {};

addEventListener("messageerror", (event) => {});

onmessageerror = (event) => {};

// 开始
start()

// 关闭
close()

MessagePort是一个可转移对象。

  • postMessage的参数含义和window.postMessage一样,在此不再赘述,不一样的地方是不需要指定targetOrigin参数了。

  • 接收消息的方式也和接收window.postMessage消息的方式一样,只是调用方由window对象变成了MessagePort对象。有一点不一样的是,如果使用addEventListener("message", (event) => {});方式来监听消息的话,需要手动执行start()方法。

    • event包含下列属性:
      • data:接收到的消息,对应发送方的message参数。
      • origin:消息发送方的origin,实践中不同源场景下两侧端点收到的消息origin都为空。
      • source:消息发送方,可以是WindowProxy, MessagePort或者ServiceWorker,实践中不同源场景下两侧端点收到的消息source都为空。
      • lastEventId:消息的序号,实践中不同源场景下两侧端点收到的消息lastEventId都为空。
      • ports:与消息一起发送的所有MessagePort对象8,实践中不同源场景下两侧端点收到的消息ports都为空数组。
  • start(): 开始接收消息,在调用该方法之前收到的消息并不会丢弃,只是不执行message的回调函数。等start()执行后,之前收到的消息会依次调用已注册的回调函数。

  • close(): 关闭接收消息。

到目前为止,创建的channel和对应的port1port2都还是在同一个上下文中。要实现在不同上下文的脚本中直接建立通道,需要借助可以转移对象控制权的方法,比如前文所述的Window: postMessage()

🌰

  • 发送消息

    1
    2
    3
    var channel = new MessageChannel();
    otherWindow.postMessage('hello', 'https://example.com', [channel.port2]);
    channel.port1.postMessage('hello');
  • 接收消息

    1
    2
    3
    4
    5
    channel.port2.onmessage = handleMessage;
    function handleMessage(event) {
    // message is in event.data
    // ...
    }

refences

  1. https://www.google.com/search?q=postmessage+mdn&newwindow=1&sca_esv=d79442c60ff0c8fa&sxsrf=ADLYWIK6h__HzcQYgHwwAuYeqfWZyZftLw%3A1735269517122&source=hp&ei=jRxuZ8OrBajk2roP646MsQY&iflsig=AL9hbdgAAAAAZ24qnZBmMSYyd9SKfIRjjIjC4h2jXTxI&ved=0ahUKEwiDpp_Z_saKAxUoslYBHWsHI2YQ4dUDCBg&uact=5&oq=postmessage+mdn&gs_lp=Egdnd3Mtd2l6Ig9wb3N0bWVzc2FnZSBtZG4yChAjGIAEGCcYigUyBhAAGBYYHjIGEAAYFhgeMgsQABiABBiGAxiKBTILEAAYgAQYhgMYigUyCxAAGIAEGIYDGIoFSIAhUABY9R9wBXgAkAEAmAGCAaAB7AmqAQQxLjEwuAEDyAEA-AEBmAIQoAKNCsICBBAjGCfCAgsQABiABBiRAhiKBcICCxAuGIAEGLEDGIMBwgIIEAAYgAQYsQPCAgsQABiABBixAxiDAcICDhAAGIAEGLEDGIMBGIoFwgIKEAAYgAQYQxiKBcICDRAAGIAEGLEDGEMYigXCAgsQABiABBixAxjJA8ICDhAuGIAEGNEDGJIDGMcBwgIFEC4YgATCAgUQABiABMICCxAuGIAEGLEDGNQCwgIIEAAYgAQYywHCAgkQABgWGMcDGB6YAwCSBwQ1LjExoAelUw&sclient=gws-wiz
  2. https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
  3. https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
  4. https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects
  5. https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API
  6. https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API/Using_channel_messaging
  7. https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
  8. https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/message_event
  9. https://html.spec.whatwg.org/multipage/web-messaging.html