1. 9.3 跨文档消息传递
      1. 9.3.1 简介
      2. 9.3.2 安全性
      3. 9.3.3 发布消息
    2. 9.4 通道消息传递
      1. 9.4.1 简介
        1. 9.4.1.1 示例
        2. 9.4.1.2 端口作为 Web 上对象能力模型的基础
        3. 9.4.1.3 端口作为抽象出服务实现的基础
      2. 9.4.2 消息通道
      3. 9.4.3 消息端口
      4. 9.4.4 端口和垃圾回收
    3. 9.5 向其他浏览上下文广播

9.3 跨文档消息传递

Window/postMessage

在所有当前引擎中支持。

Firefox3+Safari4+Chrome2+
Opera9.5+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS?Chrome Android?WebView Android≤37+Samsung Internet?Opera Android10.1+

出于安全和隐私原因,Web 浏览器会阻止不同域中的文档相互影响;也就是说,不允许跨站点脚本编写。

虽然这是一个重要的安全功能,但它会阻止不同域的页面相互通信,即使这些页面不是敌对的。本节介绍了一种消息传递系统,允许文档彼此通信,无论其源域是什么,并且设计方式不会启用跨站点脚本编写攻击。

postMessage() API 可用作 跟踪向量

9.3.1 简介

例如,如果文档 A 包含一个 iframe 元素,其中包含文档 B,并且文档 A 中的脚本在文档 B 的 Window 对象上调用 postMessage(),那么在该对象上将触发一个消息事件,标记为来自文档 A 的 Window。文档 A 中的脚本可能如下所示

var o = document.getElementsByTagName('iframe')[0];
o.contentWindow.postMessage('Hello world', 'https://b.example.org/');

要注册传入事件的事件处理程序,脚本将使用 addEventListener()(或类似机制)。例如,文档 B 中的脚本可能如下所示

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);
    }
  }
}

此脚本首先检查域是否为预期域,然后查看消息,它会将其显示给用户,或者通过向最初发送消息的文档发送消息来响应。

9.3.2 安全性

使用此 API 需要格外小心,以保护用户免受恶意实体出于自身目的而滥用网站。

作者应检查 origin 属性以确保仅接受来自他们期望接收消息的域的消息。否则,作者消息处理代码中的错误可能会被恶意网站利用。

此外,即使在检查了 origin 属性后,作者还应检查所讨论的数据是否为预期的格式。否则,如果事件源已使用跨站点脚本编写漏洞进行了攻击,则使用 postMessage() 方法发送的未经检查的信息的进一步处理可能会导致攻击传播到接收方。

作者不应在包含任何机密信息的邮件中使用通配符关键字 (*) 在 targetOrigin 参数中,因为否则无法保证邮件仅传递到其预期收件人。


鼓励接受来自任何来源的邮件的作者考虑拒绝服务攻击的风险。攻击者可以发送大量邮件;如果接收页面对每条此类邮件执行昂贵的计算或导致网络流量发送,则攻击者的邮件可能会被乘以拒绝服务攻击。鼓励作者采用限速(每分钟仅接受一定数量的邮件)以使此类攻击变得不切实际。

9.3.3 发布消息

window.postMessage(message [, options ])

将消息发布到给定的窗口。消息可以是结构化对象,例如嵌套对象和数组,可以包含 JavaScript 值(字符串、数字、Date 对象等),并且可以包含某些数据对象,例如 File BlobFileListArrayBuffer 对象。

列在 optionstransfer 成员中的对象将被转移,而不仅仅是克隆,这意味着它们在发送方不再可用。

可以使用 optionstargetOrigin 成员指定目标来源。如果未提供,则默认值为“/”。此默认值将消息限制为仅限同源目标。

如果目标窗口的来源与给定的目标来源不匹配,则消息将被丢弃,以避免信息泄露。要将消息发送到目标,无论来源如何,请将目标来源设置为“*”。

如果 transfer 数组包含重复对象,或者 message 无法克隆,则会抛出 "DataCloneError" DOMException

window.postMessage(message, targetOrigin [, transfer ])

这是 postMessage() 的备用版本,其中目标来源指定为参数。调用 window.postMessage(message, target, transfer) 等效于 window.postMessage(message, {targetOrigin, transfer})

将消息发布到刚导航到新 Document浏览上下文Window 可能会导致消息未收到其预期接收方:目标 浏览上下文 中的脚本必须有时间为邮件设置侦听器。因此,例如,在将消息发送到新创建的子 iframeWindow 的情况下,建议作者让子 Document 向其父级发布消息,宣布其已准备好接收消息,并让父级在开始发布消息之前等待此消息。

9.4 通道消息传递

Channel_Messaging_API

在所有当前引擎中支持。

Firefox41+Safari5+Chrome2+
Opera10.6+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android11+

Channel_Messaging_API/Using_channel_messaging

在所有当前引擎中支持。

Firefox41+Safari5+Chrome2+
Opera10.6+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android11+

9.4.1 简介

为了使独立的代码段(例如,在不同的 浏览上下文 中运行)能够直接通信,作者可以使用 通道消息传递

此机制中的通信通道作为双向管道实现,每端都有一个端口。在一个端口发送的消息将在另一个端口传递,反之亦然。消息作为 DOM 事件传递,不会中断或阻塞正在运行的 任务

要创建连接(两个“纠缠”端口),调用 MessageChannel() 构造函数

var channel = new MessageChannel();

其中一个端口作为本地端口保留,另一个端口发送到远程代码,例如使用 postMessage()

otherWindow.postMessage('hello', 'https://example.com', [channel.port2]);

要发送消息,使用端口上的 postMessage() 方法

channel.port1.postMessage('hello');

要接收消息,请监听 message 事件

channel.port1.onmessage = handleMessage;
function handleMessage(event) {
  // message is in event.data
  // ...
}

通过端口发送的数据可以是结构化数据;例如,这里在 MessagePort 上传递一个字符串数组

port1.postMessage(['hello', 'world']);
9.4.1.1 示例

在此示例中,两个 JavaScript 库使用 MessagePort 相互连接。这允许库稍后在不同的框架中或在 Worker 对象中托管,而无需对 API 进行任何更改。

<script src="contacts.js"></script> <!-- exposes a contacts object -->
<script src="compose-mail.js"></script> <!-- exposes a composer object -->
<script>
 var channel = new MessageChannel();
 composer.addContactsProvider(channel.port1);
 contacts.registerConsumer(channel.port2);
</script>

以下是“addContactsProvider()”函数的实现方式

function addContactsProvider(port) {
  port.onmessage = function (event) {
    switch (event.data.messageType) {
      case 'search-result': handleSearchResult(event.data.results); break;
      case 'search-done': handleSearchDone(); break;
      case 'search-error': handleSearchError(event.data.message); break;
      // ...
    }
  };
};

或者,它可以按如下方式实现

function addContactsProvider(port) {
  port.addEventListener('message', function (event) {
    if (event.data.messageType == 'search-result')
      handleSearchResult(event.data.results);
  });
  port.addEventListener('message', function (event) {
    if (event.data.messageType == 'search-done')
      handleSearchDone();
  });
  port.addEventListener('message', function (event) {
    if (event.data.messageType == 'search-error')
      handleSearchError(event.data.message);
  });
  // ...
  port.start();
};

主要区别在于,当使用 addEventListener() 时,还必须调用 start() 方法。当使用 onmessage 时,对 start() 的调用是隐式的。

无论显式调用还是隐式调用(通过设置 onmessage),start() 方法都会启动消息流:最初暂停在消息端口上发布的消息,以便在脚本有机会设置其处理程序之前不会被丢弃。

9.4.1.2 端口作为 Web 上对象能力模型的基础

端口可以被视为一种向系统中的其他参与者公开有限能力(在对象能力模型意义上)的方式。这可以是一个弱能力系统,其中端口仅仅用作特定来源内的一种方便模型,或者是一个强能力系统,其中端口由一个来源 提供者 提供,作为另一个来源 消费者 影响或获取 提供者 中的信息的唯一机制。

例如,考虑这样一种情况:一个社交网站在一个iframe中嵌入用户的电子邮件联系人提供商(来自第二个来源的地址簿网站),而在第二个iframe中嵌入一个游戏(来自第三个来源)。外部社交网站和第二个iframe中的游戏无法访问第一个iframe内部的任何内容;它们只能共同

联系人提供商可以使用这些方法,尤其是第三种方法,来提供一个API,其他来源可以使用该API来操作用户的地址簿。例如,它可以响应一条消息“add-contact Guillaume Tell <[email protected]>”,通过将给定的人员和电子邮件地址添加到用户的地址簿来响应。

为了避免网络上的任何站点都能操作用户的联系人,联系人提供商可能只允许某些可信站点(例如社交网站)这样做。

现在假设游戏想要将一个联系人添加到用户的地址簿中,并且社交网站愿意代表它这样做,本质上是“共享”联系人提供商对社交网站的信任。它可以有几种方法来做到这一点;最简单的方法是,它可以代理游戏站点和联系人站点之间的消息。但是,这种解决方案有一些困难:它要求社交网站要么完全信任游戏站点不会滥用特权,要么要求社交网站验证每个请求以确保它不是它不想允许的请求(例如添加多个联系人、读取联系人或删除联系人);如果有多个游戏同时尝试与联系人提供商交互,它还需要一些额外的复杂性。

但是,使用消息通道和MessagePort对象,所有这些问题都可以解决。当游戏告诉社交网站它想要添加一个联系人时,社交网站可以要求联系人提供商不要添加联系人,而是请求添加单个联系人的能力。然后,联系人提供商创建一对MessagePort对象,并将其中一个对象发送回社交网站,社交网站将其转发给游戏。然后,游戏和联系人提供商之间建立了直接连接,并且联系人提供商知道只响应单个“添加联系人”请求,其他任何请求都不会响应。换句话说,游戏被授予添加单个联系人的能力。

9.4.1.3 端口作为抽象服务实现的基础

继续上一节中的例子,特别考虑联系人提供商。虽然初始实现可能只是在服务的iframe中使用XMLHttpRequest 对象,但服务的演变可能希望使用一个带有单个WebSocket 连接的共享工作线程

如果初始设计使用MessagePort 对象授予功能,或者只是为了允许多个同时独立的会话,那么服务实现可以从每个iframe 中的XMLHttpRequest 模型切换到共享的WebSocket 模型,而不会改变API本身:服务提供者端的端口都可以转发到共享工作线程,而不会影响API用户的丝毫影响。

9.4.2 消息通道

MessageChannel

在所有当前引擎中支持。

Firefox41+Safari5+Chrome2+
Opera10.6+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android11+
channel = new MessageChannel()

返回一个新的MessageChannel 对象,该对象包含两个新的MessagePort 对象。

channel.port1

返回第一个MessagePort 对象。

channel.port2

返回第二个MessagePort 对象。

9.4.3 消息端口

MessagePort

在所有当前引擎中支持。

Firefox41+Safari5+Chrome2+
Opera10.6+Edge79+
Edge(旧版)12+Internet Explorer10+
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android11+

每个通道都有两个消息端口。通过一个端口发送的数据将被另一个端口接收,反之亦然。

port.postMessage(message [, transfer])
port.postMessage(message [, { transfer }])

通过通道发布消息。在transfer中列出的对象将被转移,而不是仅仅被克隆,这意味着它们在发送端不再可用。

如果transfer包含重复的对象或port,或者如果message无法克隆,则抛出"DataCloneError" DOMException

port.start()

开始调度接收到的端口消息。

port.close()

断开端口连接,使其不再处于活动状态。

9.4.4 端口和垃圾回收

强烈建议作者显式关闭MessagePort 对象以将其解开,以便重新收集其资源。创建许多MessagePort 对象并将其丢弃而不关闭它们会导致较高的瞬时内存使用量,因为垃圾回收不一定及时执行,尤其是对于MessagePort,其中垃圾回收可能涉及跨进程协调。

9.5 向其他浏览上下文广播

BroadcastChannel

在所有当前引擎中支持。

Firefox38+Safari15.4+Chrome54+
Opera?Edge79+
Edge (Legacy)?Internet ExplorerNo
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?

Broadcast_Channel_API

在所有当前引擎中支持。

Firefox38+Safari15.4+Chrome54+
Opera?Edge79+
Edge (Legacy)?Internet ExplorerNo
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?

同一用户在同一个用户代理中但在不同的不相关的浏览上下文中打开的同一来源上的页面有时需要相互发送通知,例如“嘿,用户在这里登录了,请检查你的凭据”。

对于复杂的案例,例如管理共享状态的锁定,管理服务器和多个本地客户端之间资源的同步,共享与远程主机的WebSocket 连接,等等,共享工作线程是最合适的解决方案。

但是,对于简单的案例,共享工作线程的开销太大,作者可以使用本节中描述的简单的基于通道的广播机制。

broadcastChannel = new BroadcastChannel(name)

返回一个新的BroadcastChannel 对象,通过该对象可以发送和接收给定通道名称的消息。

broadcastChannel.name

返回通道名称(如传递给构造函数)。

broadcastChannel.postMessage(message)

将给定消息发送到为该通道设置的其他BroadcastChannel 对象。消息可以是结构化对象,例如嵌套对象和数组。

broadcastChannel.close()

关闭BroadcastChannel 对象,将其打开以进行垃圾回收。

强烈建议作者在不再需要时显式关闭BroadcastChannel 对象,以便进行垃圾回收。创建许多BroadcastChannel 对象并将其丢弃,同时为其保留事件侦听器而不关闭它们,会导致明显的内存泄漏,因为这些对象将继续存在,直到它们拥有事件侦听器(或直到它们的页面或工作线程关闭)为止。

假设一个页面想要知道用户何时注销,即使用户是从同一站点的另一个标签页注销的

var authChannel = new BroadcastChannel('auth');
authChannel.onmessage = function (event) {
  if (event.data == 'logout')
    showLogout();
}

function logoutRequested() {
  // called when the user asks us to log them out
  doLogout();
  showLogout();
  authChannel.postMessage('logout');
}

function doLogout() {
  // actually log the user out (e.g. clearing cookies)
  // ...
}

function showLogout() {
  // update the UI to indicate we're logged out
  // ...
}