1. 9.4 跨文档通信
      1. 9.4.1 概述
      2. 9.4.2 安全
        1. 9.4.2.1 作者
        2. 9.4.2.2 用户代理
      3. 9.4.3 发布消息
    2. 9.5 通道
      1. 9.5.1 概述
        1. 9.5.1.1 示例
        2. 9.5.1.2 端口作为 Web 上对象能力模型的基础
        3. 9.5.1.3 端口作为抽象服务实现的基础
      2. 9.5.2 消息通道
      3. 9.5.3 消息端口
      4. 9.5.4 广播给多个端口
      5. 9.5.5 端口与垃圾回收
    3. 9.6 向其他浏览上下文广播

9.4 跨文档通信

Window/postMessage

Support in all current engines.

Firefox8+Safari4+Chrome1+
Opera9.5+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android8+Safari iOS3.2+Chrome Android18+WebView Android≤37+Samsung Internet1.0+Opera Android10.1+

Web 浏览器由于安全和隐私等原因禁止不同域的文档见互相影响,也就是说不允许跨站脚本。

虽然这是一个非常重要的安全特性,但它也阻止了不同域的页面之间进行非恶意的通信。 本章引入了一个消息系统来允许不同源的文档间通信,这一设计不会引起跨站脚本攻击。

postMessage() API 可以用作 跟踪向量

9.4.1 概述

This section is non-normative.

例如,如果文档 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.4.2 安全

9.4.2.1 作者

使用此 API 需要格外小心, 以防止恶意实体为达到自己的目的在网站上滥用。

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

此外,即使在检查 origin 属性后,作者也应该检查相关数据的格式。否则,如果事件的源被跨站脚本攻击,对通过 postMessage() 方法发送的消息的未经检查的处理可能会把攻击传播到接收者。

对于任何包含机密信息的消息中,作者不应在 targetOrigin 参数中使用通配符关键字(*),否则无法保证消息仅传递给它希望的接收方。


鼓励接受任何来源消息的作者考虑 DoS 攻击的风险。攻击者可能会发送大量的消息; 如果接收页面执行了耗时的计算或为每个此类消息产生网络流量, 则攻击者的消息可能会被成倍放大,从而导致 DoS 攻击。 鼓励作者采用速率限制(每分钟只接受一定数量的消息)以使这种攻击不可行。

9.4.2.2 用户代理

本 API 的完整性基于一个 的脚本无法向其他(不同的)域 (使用 dispatchEvent() 或其他方法)发送任意事件。

强烈建议实现方在实现这项功能时要格外小心。 它允许作者将信息从一个域传送到另一个域,通常出于安全原因这是不允许的。 它还要求 UA 小心地允许访问某些属性,但禁止其他的。


也鼓励用户代理考虑对不同 之间的消息进行速率限制,来保护简单的网站不受 DoS 攻击。

9.4.3 发布消息

window . postMessage(message [, options ] )

Window/postMessage

Support in all current engines.

Firefox8+Safari4+Chrome1+
Opera9.5+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android8+Safari iOS3.2+Chrome Android18+WebView Android37+Samsung Internet1.0+Opera Android10.1+

像指定的 window 发送消息。消息可以是结构化的对象, 例如嵌套对象和数组,可以包含 JavaScript 值(字符串、数字、Date、对象等等), 也可以包含数据对象比如 File BlobFileList, 以及 ArrayBuffer 对象。

optionstransfer 成员中的对象会被转移(不是克隆), 意味着它们在发送侧不再可用。

可以用 optionstargetOrigin 成员来指定目标源。 如果没有指定,默认为 "/"。这个默认值限制了消息只能发送到同源目标。

为避免信息泄露,如果目标窗口的源不匹配给定的源消息会被丢弃。如果要无视目标源直接发送消息,需要设置目标源为 "*"。

如果 transfer 数组包含重复的对象,或者 message 不可克隆时, 抛出一个 "DataCloneError" DOMException

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

这是 postMessage() 的另一个版本,以目标源作为参数。调用 window.postMessage(message, target, transfer) 等价于 window.postMessage(message, {targetOrigin, transfer})

给刚导航到新 Documentbrowsing context 上的 Window 发布消息时,消息可能无法到达接收者: 目标 浏览上下文 中的脚本必须有时间设置消息监听器。 因此,如果要发送消息给一个刚创建的子 iframeWindow, 建议作者在子 Document 中发送消息给父级来声明它已经准备好接收消息了, 父级则等待该消息到来后再开发发送消息。

给定 targetWindow, messageoptions窗口发送消息步骤 如下:

  1. targetRealmtargetWindowRealm

  2. incumbentSettings当前设置对象

  3. targetOriginoptions["targetOrigin"]。

  4. 如果 targetOrigin 是一个 U+002F SOLIDUS 字符 (/),则将 targetOrigin 设置为 incumbentSettingsorigin

  5. 否则,如果 targetOrigin 不是一个 U+002A ASTERISK 字符 (*),则:

    1. parsedURL 为在 targetOrigin 上执行 URL 解析 的结果。

    2. 如果 parsedURL 是失败,则抛出 "SyntaxError" DOMException

    3. 设置 targetOriginparsedURLorigin

  6. transferoptions["transfer"]。

  7. serializeWithTransferResultStructuredSerializeWithTransfer(message, transfer)。 重新抛出任何异常。

  8. 已发送消息任务源 上给定 targetWindow 入队一个全局任务 来执行以下步骤:

    1. 如果 targetOrigin 参数不是单个 U+002A ASTERISK 字符 (*) 且 targetWindow关联 Document 不 与 targetOrigin 同源,则返回。

    2. originincumbentSettingsorigin序列化

    3. sourceWindowProxy 对象对应的 incumbentSettings全局对象 (一个 Window 对象)。

    4. deserializeRecordStructuredDeserializeWithTransfer(serializeWithTransferResult, targetRealm)。

      如果这抛出了异常,捕获它并使用 MessageEventtargetWindow产生 一个名为 messageerror 的事件, 并把 origin 属性初始化为 originsource 属性初始化为 source, 然后返回。

    5. messageClonedeserializeRecord.[[Deserialized]]。

    6. newPorts 为一个新的 frozen array,包含 deserializeRecord.[[TransferredValues]] 中所有的 MessagePort 对象(如果有的话), 并保持它们的相对顺序。

    7. 使用 MessageEventtargetWindow 上 产生 一个名为 message 的事件, 其 origin 属性初始化为 originsource 属性初始化为 sourcedata 属性初始化为 messageCloneports 属性初始化为 newPorts

Window 上调用 postMessage(message, options) 方法时,必须执行以下步骤:

  1. targetWindow 为这个 Window 对象。

  2. 给定 targetWindowmessageoptions, 执行 窗口发送消息步骤

Window 上调用 postMessage(message, targetOrigin, transfer) 方法时,必须执行以下步骤:

  1. targetWindow 为这个 Window 对象。

  2. options 为 «[ "targetOrigin" → targetOrigin, "transfer" → transfer ]»。

  3. 给定 targetWindowmessageoptions, 执行 窗口发送消息步骤

9.5 通道

9.5.1 概述

This section is non-normative.

为了使独立的代码片段之间可以相互通信(例如运行在不同 浏览上下文 的代码直接通信), 作者可以使用 Channel 通信

该机制下的通信 Channel 实现为双向管道,两端各一个端口。 从一个端口发送的消息被传递到另一个端口,反之亦然。 消息被作为 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.5.1.1 示例

This section is non-normative.

在这个例子中,两个 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>

Here's what the "addContactsProvider()" function's implementation could look like:

function addContactsProvider(port) {
  port.onmessage = function (event) {
    switch (event.data.messageType) {
      'search-result': handleSearchResult(event.data.results); break;
      'search-done': handleSearchDone(); break;
      '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.5.1.2 端口作为 Web 上对象能力模型的基础

This section is non-normative.

可以把端口当做为系统中其他角色提供有限能力的一种方式(在对象能力模型的意义上), 这可能是一个弱能力的系统,用端口只是为了在源内通信的方便, 也可能是一个强能力的系统,由一个源 provider 提供,作为唯一的机制给另一个源 consumer 来从 provider 获取信息或造成改变。

例如,考虑一个社交网站嵌入了一个 iframe 它是用户的邮件联系人提供者(一个联系人网站,来自第二个源), 以及第二个 iframe(一个游戏网站,来自第三个源) 外面的社交网站和第二个 iframe 中的游戏无法访问第一个 iframe 中的任何东西;它们只能:

联系人提供者使用这些方法(尤其是第三个)来给其他域提供操作用户地址簿的 API。 例如,响应 "add-contact Guillaume Tell <tell@pomme.example.net>" 时,在用户的地址簿中新增指定的人和 e-mail。

为了避免 Web 上任何站点都可以操作用户的地址簿,联系人提供者可能只允许确定的信任站点调用, 比如这个社交网站。

现在假设这个游戏希望添加联系人到用户的地址簿,而且这个社交网站也愿意允许它这样做, 实质上是“共享”联系人提供者对社交网站的信任。实现这一共享有很多方式, 最简单的是在游戏站点和联系人站点之间代理消息。 然而该方案有一些困难:它需要社交网站完全信任游戏网站不会滥用特权, 或者要求社交网站对每个请求进行验证来确保请求是否被允许 (例如添加多个联系人、读取联系人、删除联系人等)。 如果可能有多个游戏同时与联系人提供者通信,还需要额外的复杂性。

然而,使用消息 Channel 和 MessagePort 对象可以解决所有这些问题。 当游戏告诉设计网络它需要添加联系人时,社交网站可以请求联系人提供者添加单个联系人的 能力,而非直接请求添加一个联系人。 然后联系人提供者提供一对 MessagePort 对象,把其中一个返回给社交网站, 社交网站再把它转交给游戏。游戏和联系人提供者就有了直接的连接, 因此联系人提供者知道只对它开放 "添加联系人" 的请求。 换句话说,这个游戏被赋予了添加单个联系人的能力。

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

This section is non-normative.

继续上一部分提到的例子,特别考虑联系人提供者。 初始的实现可能是简单地在服务的 iframe 中使用 XMLHttpRequest 对象, 一个改进版本可能会使用 共享 worker 和一个 WebSocket 连接。

如果初始的设计使用了 MessagePort 对象来赋予能力,或者甚至仅仅是为了支持多个并发的独立会话, 这个服务的实现就可以直接从 "每个 iframe 中一个 XMLHttpRequest" 模型迁移到 "共享 WebSocket" 模型, 完全不需要改变 API:服务提供者一侧的端口可以全部转发到共享 Worker,完全不影响 API 的用户。

9.5.2 消息通道

[Exposed=(Window,Worker)]
interface MessageChannel {
  constructor();

  readonly attribute MessagePort port1;
  readonly attribute MessagePort port2;
};
channel = new MessageChannel()

MessageChannel/MessageChannel

Support in all current engines.

Firefox41+Safari5+Chrome4+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android41+Safari iOS5.1+Chrome Android18+WebView Android4.4+Samsung Internet1.0+Opera Android11+

返回有两个新的 MessagePort 对象的,一个新的 MessageChannel 对象。

channel . port1

MessageChannel/port1

Support in all current engines.

Firefox41+Safari5+Chrome4+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android41+Safari iOS5.1+Chrome Android18+WebView Android4.4+Samsung Internet1.0+Opera Android11+

返回第一个 MessagePort 对象。

channel . port2

MessageChannel/port2

Support in all current engines.

Firefox41+Safari5+Chrome4+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android41+Safari iOS5.1+Chrome Android18+WebView Android4.4+Samsung Internet1.0+Opera Android11+

返回第二个 MessagePort 对象。

MessageChannel 对象有一个关联的 port 1port 2,都是 MessagePort 对象。

new MessageChannel() 构造步骤为:

  1. 设置 thisport 1 为在 this相关 Realm 中的 new MessagePort

  2. 设置 thisport 2 为在 this相关 Realm 中的 new MessagePort

  3. 绑定 thisport 1thisport 2

port1 获取步骤为返回 thisport 1

port2 获取步骤为返回 thisport 2

9.5.3 消息端口

每个通道都有两个消息端口。从一个端口发送的数据会从另一个端口收到,反之亦然。

[Exposed=(Window,Worker,AudioWorklet), Transferable]
interface MessagePort : EventTarget {
  undefined postMessage(any message, optional sequence<object> transfer = []);
  undefined postMessage(any message, optional PostMessageOptions options = {});
  undefined start();
  undefined close();

  // event handlers
  attribute EventHandler onmessage;
  attribute EventHandler onmessageerror;
};

dictionary PostMessageOptions {
  sequence<object> transfer = [];
};
port . postMessage(message [, transfer] )

MessagePort/postMessage

Support in all current engines.

Firefox41+Safari5+Chrome4+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android41+Safari iOS5.1+Chrome Android18+WebView Android37+Samsung Internet1.0+Opera Android11+
port . postMessage(message [, { transfer }] )

通过通道发布一条消息。列在 transfer 中的对象已经被传输(不仅是克隆), 意味着在发送侧无法使用了。

如果 transfer 数组包含重复的对象、源或目标端口时, 或者 message 不可克隆, 抛出一个 "DataCloneError" DOMException

port . start()

MessagePort/start

Support in all current engines.

Firefox41+Safari5+Chrome4+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android41+Safari iOS5.1+Chrome Android18+WebView Android37+Samsung Internet1.0+Opera Android11+

开始派发端口上收到的消息。

port . close()

MessagePort/close

Support in all current engines.

Firefox41+Safari5+Chrome4+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android41+Safari iOS5.1+Chrome Android18+WebView Android37+Samsung Internet1.0+Opera Android11+

断开端口,端口不再处于激活状态。

每个 MessagePort 对象可以与另一个关联(对称关系)。 每个 MessagePort 对象也可以有一个 任务源 称为 端口消息队列,初始为空。 端口消息队列 可以被启用和禁用,初始禁用。 一旦被启用,就不能再被禁用了 (虽然队列中的消息可以移动到其他队列或者全部移除,也可以达到一样的效果), MessagePort 还有一个 已经被转移 标志, 初始必须为 false。

当端口的 端口消息队列 被启用时, 事件循环 不许使用它作为其中一个 任务源。当端口的 相关全局对象Window 时, 在它的 端口消息队列 中的所有 任务 必须与该端口的 相关全局对象关联 Document 相关联。

如果该文档是 完全激活的, 但事件监听器的脚本的 设置对象 指定的 负责文档 完全激活的,消息会在这些文档 完全激活的 时才收到。

每个 事件循环 有一个 任务源 称为 未转移的端口消息队列。 这是一个虚拟的 任务源,它的必须表现地就像它包含满足以下条件的的每个 MessagePort端口消息队列 的所有 任务 一样: 1. 已经被转移 标志位 false; 2. 启用了 端口消息队列; 3. 相关代理事件循环 为该 事件循环。 顺序为它们被添加到对应的 任务源 的顺序。 当 任务未转移的端口消息队列 移除时, 它必须也从它的 端口消息队列 中移除。

MessagePort已被转移 标志为 false 时, 事件循环 必须忽略它的 端口消息队列 (此时使用 未转移的端口消息队列)。

当一个端口、其关联端口、或者它克隆自的对象被传输时, 已被转移 标志位设为 true 当 MessagePort已被转移 标志为 true 时, 其 端口消息队列 是一级 任务源,不受任何 未转移的端口消息队列 影响。

当用户代理 关联 两个 MessagePort 对象时, 必须执行以下步骤:

  1. 如果其中给一个端口已经关联,则把它与之前关联的端口解关联。

    如果那两个之前关联的端口是同一个 MessageChannel 对象的两个端口,则那个 MessageChannel 对象不再表示任何真正的端口:它的两个端口已经不再关联。

  2. 关联这两个端口,让它们组成新通道的两部分。 (没有 MessageChannel 对象表示这个通道)

    经过这个步骤的两个端口 AB 被称为关联的;其中一个关联到了另一个,反之亦然。

    尽管本标准把这个过程描述为瞬间的,但它们更有可能通过消息传递来实现。 就像其他所有算法一样,关键是只要最终结果无法与规范区分(在黑盒的意义上)。


MessagePort 对象是 可传输对象。 给定 valuedataHolder, 它们的 传输步骤 是:

  1. 设置 value已被转移 标志为 true。

  2. 设置 dataHolder.[[PortMessageQueue]] 到 value端口消息队列

  3. 如果 value 与另一个端口 remotePort 关联,则:

    1. 设置 remotePort已被转移 标志为 true。

    2. 设置 dataHolder.[[RemotePort]] 为 remotePort

  4. 否则,设置 dataHolder.[[RemotePort]] 为 null。

给定 dataHoldervalue, 它们的 传输-接收步骤 是:

  1. 设置 value已被转移 标志为 true。

  2. 把所有要在 dataHolder.[[PortMessageQueue]] 上触发 message 事件的 tasks(如果有的话) 移动到 value端口消息队列 上, 保持 value端口消息队列 处于初始的禁用状态, 如果 value相关全局对象Window, 将移动后的 任务value相关全局对象关联 Document 相关联。

  3. 如果 dataHolder.[[RemotePort]] 非 null,则 关联 dataHolder.[[RemotePort]] 和 value。(这将会把 dataHolder.[[RemotePort]] 与它之前关联的被转移的端口解关联)。


给定 targetPortmessageoptions消息端口发消息的步骤 如下:

  1. transferoptions["transfer"]。

  2. 如果在 transfer 中的任何一个对象是这个 MessagePort, 则抛出一个 "DataCloneError" DOMException

  3. doomed 为 false。

  4. 如果 targetPort 非 null 且 transfer 中有任何对象是 targetPort,则设置 doomed 为 true,并(可选地)在开发终端中报告 目标端口被发送到了它自己,导致通道丢失。

  5. serializeWithTransferResultStructuredSerializeWithTransfer(message, transfer)。 重新抛出任何异常。

  6. 如果没有 targetPort (比如这个 MessagePort 未关联), 或者如果 doomed 为 true,则返回。

  7. targetPort端口消息队列 中添加一个 任务 执行以下步骤:

    1. finalTargetPort 为在当前任务所在的 端口消息队列MessagePort

      这可能与 targetPort 不同,如果 targetPort 自己被传输,它的所有任务都跟着移动。

    2. targetRealmfinalTargetPort相关 Realm

    3. deserializeRecordStructuredDeserializeWithTransfer(serializeWithTransferResult, targetRealm)。

      如果这抛出了异常,捕获它并在 finalTargetPort 上用 MessageEvent 触发一个 名为 messageerror 的事件,然后返回。

    4. messageClonedeserializeRecord.[[Deserialized]]。

    5. newPorts 为一个新的 冻结的数组 该数组包含所有 deserializeRecord.[[TransferredValues]] 中的 MessagePort 对象(如果有的话),保持它们的相对顺序。

    6. finalTargetPort 上用 MessageEvent, 触发一个 名为 message 的事件,其 data 属性初始化为 messageCloneports 属性初始化为newPorts

MessagePort 对象上调用 postMessage(message, options) 方法时,执行以下步骤:

  1. targetPortMessagePort 绑定的端口(如果有的话), 如果没有就令它为 null。

  2. targetPortmessageoptions 执行 消息端口发送消息步骤

MessagePort 对象上调用 postMessage(message, transfer) 方法时必须执行以下步骤:

  1. targetPortMessagePort 绑定的端口(如果有的话), 如果没有就令它为 null。

  2. options 为 «[ "transfer" → transfer ]»。

  3. targetPortmessageoptions 执行 消息端口发送消息步骤


start() 方法必须启用其端口的 端口消息队列,如果还没有启用的话。


在已关联的端口 local port 上调用 close() 方法时, 用户代理必须解关联这两个端口。 如果该方法在一个未关联的端口上调用时,该方法必须什么都不做。


下面是所有实现 MessagePort 接口的对象 必须 支持的 事件处理器 (以及它们相应的事件处理器事件类型), 作为 事件处理器 IDL 属性

事件处理器 事件处理器事件类型
onmessage

MessagePort/onmessage

Support in all current engines.

Firefox41+Safari5+Chrome4+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android41+Safari iOS5.1+Chrome Android18+WebView Android37+Samsung Internet1.0+Opera Android11+
message
onmessageerror

MessagePort/onmessageerror

Firefox57+SafariNoChrome60+
Opera47+Edge79+
Edge (Legacy)18Internet ExplorerNo
Firefox Android57+Safari iOSNoChrome Android60+WebView Android60+Samsung Internet8.0+Opera Android44+
messageerror

第一次设置 MessagePort 对象的 onmessage IDL 属性时, 端口的 端口消息队列 必须被启用, 就像调用了 start() 方法一样。

9.5.4 广播给多个端口

This section is non-normative.

B广播给多个端口原则上相对简单:维护一个要发消息的 MessagePort 对象数组,发消息时遍历这个数组。 然而这有一个不好的效果:它会阻止端口的垃圾回收,即使另一端已经走了。 为了避免这个问题,实现一个简单的协议让对方确认它还存在。 如果它在某段时间没有确认,就假设它已经不在了,关闭 MessagePort 对象并让它被回收。

9.5.5 端口与垃圾回收

当一个 MessagePort 对象 o 被关联时,用户代理的表现必须像是 o 的关联 MessagePort 对象有一个 o 的强引用。 或者就像 o相关全局对象 有一个 o 的强引用。

这样给定一个事件监听器,它收到一个消息端口后可以忘掉它。 只要该事件监听器还可能收到消息,这个通道就仍然被维护着。

当然这在通道的两侧都会发生,只要它们不能从活跃的代码到达,两个端口就都会被垃圾回收。 即使它们之间还有对方的强引用。

进一步地,当 任务队列 中的 任务 仍然有一个要派发在 MessagePort 上的事件时, 或者这个 MessagePort 对象的 端口消息队列 处于启用状态且非空时, MessagePort 对象不得被垃圾回收。

强烈鼓励作者显式地关闭 MessagePort 对象来解关联它们, 这样它们的资源可以被重新回收。 创建很多歌 MessagePort 对象并直接丢弃它们而不关闭可能导致瞬时内存使用率高, 因为垃圾收集不一定能够及时执行,尤其是对于垃圾收集可能涉及跨进程协调的 MessagePort 而言。

9.6 向其他浏览上下文广播

同一个用户,同一个用户代理,同一个 , 但不同 浏览上下文 的页面有时需要相互发送通知, 例如 “嘿,用户在我这里登录了,重新检查你的信任状态”。

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

但对于简单情况,共享 Worker 可能是不合理的开销, 作者可以使用这一章描述的简单的基于通道的广播机制。

[Constructor(DOMString name), Exposed=(Window,Worker)]
interface BroadcastChannel : EventTarget {
  readonly attribute DOMString name;
  void postMessage(any message);
  void close();
  attribute EventHandler onmessage;
  attribute EventHandler onmessageerror;
};
broadcastChannel = new BroadcastChannel(name)

BroadcastChannel/BroadcastChannel

Firefox38+SafariNoChrome54+
Opera41+Edge79+
Edge (Legacy)NoInternet ExplorerNo
Firefox Android38+Safari iOSNoChrome Android54+WebView Android54+Samsung Internet6.0+Opera Android41+

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

broadcastChannel . name

BroadcastChannel/name

Firefox38+SafariNoChrome54+
Opera41+Edge79+
Edge (Legacy)NoInternet ExplorerNo
Firefox Android38+Safari iOSNoChrome Android54+WebView Android54+Samsung Internet6.0+Opera Android41+

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

broadcastChannel . postMessage(message)

BroadcastChannel/postMessage

Firefox38+SafariNoChrome54+
Opera41+Edge79+
Edge (Legacy)NoInternet ExplorerNo
Firefox Android38+Safari iOSNoChrome Android54+WebView Android54+Samsung Internet6.0+Opera Android41+

向为这个通道建立的 BroadcastChannel 对象发送给定的消息。 消息可以是结构化对象,例如嵌套对象和数组。

broadcastChannel . close()

BroadcastChannel/close

Firefox38+SafariNoChrome54+
Opera41+Edge79+
Edge (Legacy)NoInternet ExplorerNo
Firefox Android38+Safari iOSNoChrome Android54+WebView Android54+Samsung Internet6.0+Opera Android41+

关闭 BroadcastChannel 对象,让它可以被垃圾回收。

BroadcastChannel 对象有一个 通道名, 一个 BroadcastChannel 设置对象, 以及一个 关闭标志

BroadcastChannel() 构造函数被调用时, 必须创建和返回一个 BroadcastChannel 对象, 其 通道名 为构造函数的第一个参数, 其 BroadcastChannel 设置对象当前设置对象, 其 关闭标志 为 false。

name 属性必须返回 通道名

postMessage(message) 方法在 BroadcastChannel 对象上被调用时,必须执行以下步骤:

  1. source 为这个 BroadcastChannel

  2. sourceSettingssourceBroadcastChannel 设置对象

  3. 如果 source关闭标志 为 true,则抛出一个 "InvalidStateError" DOMException

  4. sourceChannelsource通道名

  5. targetRealm 为用户代理定义的 Realm。

  6. serializedStructuredSerialize(message)。 重新抛出任何异常。

  7. destinations 为符合以下要求的 BroadcastChannel 对象的列表:

  8. destinations 移除 source

  9. destinations 排序,使得所有 BroadcastChannel 设置对象 指定了同样 负责事件循环BroadcastChannel 对象按照创建顺序排序,老的在先。 (这并没有定义完全排序。在这个约束下,用户代理可以按任意方式排序)

  10. destinations 中的每一个 BroadcastChannel 对象 destination排一个任务 执行以下步骤:

    1. targetRealmdestination相关 Realm

    2. dataStructuredDeserialize(serialized, targetRealm)。

      如果抛出了异常,捕获它,在 destination 上使用 MessageEvent 发生 一个名为 messageerror 的事件, origin 属性初始化为 sourceSettingsorigin序列化,然后返回。

    3. destination 上使用 MessageEvent 发生 一个名为 message 的事件,其 data 属性初始化为 dataorigin 属性初始化为 sourceSettingsorigin序列化

    任务 必须使用 DOM 操作任务源, 而且对于那些目标 BroadcastChannel 对象的 BroadcastChannel 设置对象 指定的 事件循环 是一个 浏览上下文 事件循环 的任务,必须与 那个目标的 BroadcastChannel 对象的 BroadcastChannel 设置对象 指定的 负责文档 相关联。

已关闭标志 为 false 的 BroadcastChannel 对象还有事件处理器注册在 message 事件上时,BroadcastChannel 对象的 BroadcastChannel 设置对象 指定的 全局对象 上必须有一个强引用指向 BroadcastChannel 对象自己。

close() 方法必须 把调用它的 BroadcastChannel 对象的 已关闭标志 设置为 true。

强烈鼓励作者在不需要它们时,显式地关闭 BroadcastChannel 对象, 这样它们就可以被垃圾回收了。 创建很多 BroadcastChannel 对象并在留有一个事件监听器时抛弃但不关闭他们, 会导致明显的内存泄露,因为只要它们有事件处理器(或页面或 Worker 别关闭),这些对象就会继续活着。


下面的例子是所有实现 BroadcastChannel 接口的对象 必须 以 事件处理器 IDL 属性 方式支持的 事件处理器 (以及对应的 事件处理器事件类型):

事件处理器 事件处理器事件类型
onmessage

BroadcastChannel/onmessage

Firefox38+SafariNoChrome54+
Opera41+Edge79+
Edge (Legacy)NoInternet ExplorerNo
Firefox Android38+Safari iOSNoChrome Android54+WebView Android54+Samsung Internet6.0+Opera Android41+
message
onmessageerror

BroadcastChannel/onmessageerror

Firefox57+SafariNoChrome60+
Opera47+Edge79+
Edge (Legacy)NoInternet ExplorerNo
Firefox Android57+Safari iOSNoChrome Android60+WebView Android60+Samsung Internet8.0+Opera Android44+
messageerror

假设一个页面想要知道用户登出,包括从其他标签页登出:

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
  // ...
}