JS `MessageChannel`:跨 `iframe`、跨 `Worker` 的安全通信

各位观众老爷,晚上好!我是今晚的讲师,咱们今天聊聊JS里的一个神奇玩意儿:MessageChannel。这东西啊,就像是JavaScript世界里的秘密通道,专门用来在不同的“王国”(iframe、Worker)之间安全又方便地传递小纸条(消息)。

咱们先来个开胃菜:

一、啥是MessageChannel

想象一下,你有两个小朋友,一个住在iframe城堡里,另一个在Worker小屋里。他们想聊天,但又不能直接跑到对方家里(因为安全问题)。这时候,你就需要一个中间人,或者说,一个安全的邮递员。MessageChannel就是这个邮递员,它能帮你建立一个安全的通道,让两个小朋友可以互相发消息,而不用担心被坏人偷听或篡改。

简单来说,MessageChannel是一个接口,它创建了一个用于异步双向通信的通道。这个通道有两个端口:port1port2。你可以把这两个端口分别交给不同的执行上下文(例如,一个给iframe,一个给Worker),然后它们就可以通过这两个端口互相发送消息了。

二、为啥要用MessageChannel

你可能会问,为啥不直接用postMessage?嗯,postMessage确实也能跨域通信,但它有一个缺点:你需要知道目标窗口的引用(window对象),并且需要小心地处理消息来源的验证。MessageChannel就解决了这个问题,它提供了一个更安全、更便捷的方式。

特性 postMessage MessageChannel
通信方向 单向 双向
目标对象 需要目标窗口的引用 (window 对象) 通过 port2 间接通信,无需直接引用目标窗口
安全性 需要验证消息来源 通过端口进行通信,安全性更高
使用场景 简单的跨域通信 iframeWorker 等复杂场景的跨上下文通信,需要双向通信
复杂性 相对简单 稍微复杂,需要理解端口的概念

三、怎么用MessageChannel?(代码示例)

好了,光说不练假把式,咱们直接上代码:

3.1 iframe之间的通信

首先,咱们创建两个简单的HTML文件:parent.htmliframe.html

parent.html:

<!DOCTYPE html>
<html>
<head>
  <title>Parent Page</title>
</head>
<body>
  <h1>Parent Page</h1>
  <iframe id="myIframe" src="iframe.html"></iframe>

  <script>
    const iframe = document.getElementById('myIframe');

    iframe.onload = () => {
      const channel = new MessageChannel();
      const port1 = channel.port1;
      const port2 = channel.port2;

      // 监听来自 iframe 的消息
      port1.onmessage = (event) => {
        console.log('Parent received:', event.data);
      };

      // 将 port2 发送给 iframe
      iframe.contentWindow.postMessage({ port: port2 }, '*', [port2]); // 注意第三个参数,必须传递 port2 本身
      //iframe.contentWindow.postMessage(port2, '*');  //这是错误写法,MessagePort 必须作为 transferable object 传递

      // 发送消息给 iframe
      port1.postMessage('Hello from parent!');
    };
  </script>
</body>
</html>

iframe.html:

<!DOCTYPE html>
<html>
<head>
  <title>Iframe Page</title>
</head>
<body>
  <h1>Iframe Page</h1>

  <script>
    window.addEventListener('message', (event) => {
      if (event.data.port) {
        const port = event.data.port;

        // 监听来自 parent 的消息
        port.onmessage = (event) => {
          console.log('Iframe received:', event.data);
        };

        // 发送消息给 parent
        port.postMessage('Hello from iframe!');
      }
    });
  </script>
</body>
</html>

代码解释:

  1. 创建MessageChannelparent.html中,我们首先创建了一个MessageChannel实例。
  2. 获取端口: 然后,我们通过channel.port1channel.port2获取了两个端口。
  3. 监听消息: port1 用于监听来自 iframe 的消息,port2 用于发送消息给 iframe,反之亦然。
  4. 发送端口: 关键的一步是,我们使用 postMessageport2 发送给 iframe。注意,这里 port2 必须作为 transferable object 传递,这意味着我们需要将它放在 postMessage 的第三个参数(transfer 数组)中。 transferable object可以被高效的转移到另一个执行上下文,而不是被复制。
  5. 建立连接:iframe.html 中,我们监听 message 事件,当收到包含 port 的消息时,我们就获取到 port2,然后就可以使用它来与 parent 通信了。

运行结果:

打开 parent.html,你会在控制台中看到:

Iframe received: Hello from parent!
Parent received: Hello from iframe!

重点:

  • transferable object MessagePort 是一个 transferable object,这意味着它可以被高效地转移到另一个执行上下文,而不是被复制。这对于性能至关重要,尤其是在传递大型数据时。
  • postMessage 的第三个参数: 一定要记得将 MessagePort 放在 postMessage 的第三个参数中,否则会报错。

3.2 Worker 和主线程之间的通信

接下来,咱们再看看如何使用 MessageChannelWorker 和主线程之间通信。

index.html (主线程):

<!DOCTYPE html>
<html>
<head>
  <title>Worker Example</title>
</head>
<body>
  <h1>Worker Example</h1>
  <script>
    const worker = new Worker('worker.js');

    const channel = new MessageChannel();
    const port1 = channel.port1;
    const port2 = channel.port2;

    // 监听来自 worker 的消息
    port1.onmessage = (event) => {
      console.log('Main thread received:', event.data);
    };

    // 将 port2 发送给 worker
    worker.postMessage({ port: port2 }, [port2]);

    // 发送消息给 worker
    port1.postMessage('Hello from main thread!');
  </script>
</body>
</html>

worker.js (Worker 线程):

self.addEventListener('message', (event) => {
  if (event.data.port) {
    const port = event.data.port;

    // 监听来自 main thread 的消息
    port.onmessage = (event) => {
      console.log('Worker received:', event.data);
    };

    // 发送消息给 main thread
    port.postMessage('Hello from worker!');
  }
});

代码解释:

这段代码和 iframe 的例子非常相似,主要的区别在于:

  1. 创建 Worker 我们使用 new Worker('worker.js') 创建了一个 Worker 实例。
  2. 发送端口: 我们使用 worker.postMessageport2 发送给 Worker。同样,port2 必须作为 transferable object 传递。
  3. Worker 线程:worker.js 中,我们监听 message 事件,当收到包含 port 的消息时,我们就获取到 port2,然后就可以使用它来与主线程通信了。

运行结果:

打开 index.html,你会在控制台中看到:

Worker received: Hello from main thread!
Main thread received: Hello from worker!

四、MessageChannel 的高级用法

除了简单的消息传递,MessageChannel 还有一些高级用法,例如:

  • 传递复杂数据: 你可以使用 MessageChannel 传递任何可以被序列化的数据,包括对象、数组、甚至 ArrayBuffer
  • 流式数据处理: 你可以将 MessageChannelReadableStreamWritableStream 结合使用,实现高效的流式数据处理。
  • 实现 RPC (Remote Procedure Call): 你可以使用 MessageChannel 实现简单的 RPC 机制,让不同的执行上下文可以互相调用函数。

五、注意事项

  • 兼容性: MessageChannel 的兼容性非常好,几乎所有现代浏览器都支持它。
  • 安全性: 使用 MessageChannel 可以提高跨域通信的安全性,因为它避免了直接操作 window 对象。
  • 性能: MessageChannel 使用了 transferable object 机制,可以高效地传递数据,减少内存复制。

六、总结

MessageChannel 是一个非常强大的工具,它可以让你在不同的 JavaScript 执行上下文之间建立安全、高效的通信通道。无论是在 iframe 之间、Worker 和主线程之间,还是在其他需要跨域通信的场景中,MessageChannel 都能发挥重要作用。

希望今天的讲解能帮助大家更好地理解和使用 MessageChannel。如果大家有什么问题,欢迎随时提问!

代码补充:

下面是一个使用 MessageChannel 传递 ArrayBuffer 的例子:

index.html (主线程):

<!DOCTYPE html>
<html>
<head>
  <title>ArrayBuffer Example</title>
</head>
<body>
  <h1>ArrayBuffer Example</h1>
  <script>
    const worker = new Worker('worker.js');

    const channel = new MessageChannel();
    const port1 = channel.port1;
    const port2 = channel.port2;

    // 监听来自 worker 的消息
    port1.onmessage = (event) => {
      console.log('Main thread received:', event.data);
    };

    // 创建一个 ArrayBuffer
    const buffer = new ArrayBuffer(1024 * 1024); // 1MB
    const view = new Uint8Array(buffer);
    for (let i = 0; i < view.length; i++) {
      view[i] = i % 256;
    }

    // 将 port2 和 ArrayBuffer 发送给 worker
    worker.postMessage({ port: port2, buffer: buffer }, [port2, buffer]); // 注意,buffer 也必须作为 transferable object 传递

    // 发送消息给 worker
    port1.postMessage('Hello from main thread!');
  </script>
</body>
</html>

worker.js (Worker 线程):

self.addEventListener('message', (event) => {
  if (event.data.port && event.data.buffer) {
    const port = event.data.port;
    const buffer = event.data.buffer;
    const view = new Uint8Array(buffer);

    // 监听来自 main thread 的消息
    port.onmessage = (event) => {
      console.log('Worker received:', event.data);
    };

    // 修改 ArrayBuffer
    for (let i = 0; i < view.length; i++) {
      view[i] = 255 - view[i];
    }

    // 发送消息给 main thread
    port.postMessage('Hello from worker!');
  }
});

在这个例子中,我们创建了一个 1MB 的 ArrayBuffer,并将其作为 transferable object 传递给了 WorkerWorker 修改了 ArrayBuffer 的内容,然后主线程就可以看到修改后的数据了。 这展示了 transferable object 的强大之处,它允许我们在不同的执行上下文之间高效地共享数据,而无需进行昂贵的复制操作。

好了,今天的讲座就到这里。希望大家有所收获,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注