消息通道与广播通信

深入理解 MessageChannel、BroadcastChannel、postMessage API,实现跨窗口、跨标签页、跨 iframe 的高效通信。

消息通道与广播通信(Message Channel and Broadcasting)

一、跨文档消息通信

1.1 postMessage API

postMessage 是最基础的跨文档通信方式,允许不同源之间的窗口进行通信。

// 发送消息
otherWindow.postMessage(message, targetOrigin, [transfer]);

// 接收消息
window.addEventListener('message', (event) => {
  // 验证来源
  if (event.origin !== 'https://expected-origin.com') {
    return;
  }

  console.log('Received:', event.data);
  console.log('Source:', event.source);
});

1.2 安全考虑

// ❌ 不安全:使用 * 允许任何来源
iframe.contentWindow.postMessage(message, '*');

// ✅ 安全:明确指定目标来源
iframe.contentWindow.postMessage(message, 'https://trusted-origin.com');

// ✅ 最安全:验证来源和 source
window.addEventListener('message', (event) => {
  // 验证来源
  if (!allowedOrigins.includes(event.origin)) {
    console.warn('Rejected message from:', event.origin);
    return;
  }

  // 验证来源窗口(可选)
  if (event.source !== expectedSource) {
    console.warn('Unexpected source');
    return;
  }

  console.log('Received:', event.data);
});

二、MessageChannel API

2.1 基本用法

MessageChannel 创建一对专用的消息端口,用于双向通信。

// 创建通道
const channel = new MessageChannel();

// 获取两个端口
const port1 = channel.port1;
const port2 = channel.port2;

// 在发送端设置接收端口
window1.postMessage('init', '*', [port2]);

// 在接收端开始监听
port1.addEventListener('message', (event) => {
  console.log('Received:', event.data);
});

// 开始监听(必须在 addEventListener 之后调用)
port1.start();

// 发送消息
port1.postMessage('Hello from port1');

// 关闭通道
port1.close();

2.2 实际应用:iframe 通信

// parent.html
const iframe = document.getElementById('myIframe');
const channel = new MessageChannel();

// 发送 port 给 iframe
iframe.addEventListener('load', () => {
  iframe.contentWindow.postMessage('init', '*', [channel.port2]);
});

// 使用 port1 通信
channel.port1.addEventListener('message', (event) => {
  console.log('From iframe:', event.data);
  channel.port1.postMessage('Response from parent');
});

channel.port1.start();


// iframe.html
let port1;

window.addEventListener('message', (event) => {
  if (event.data === 'init' && event.ports[0]) {
    port1 = event.ports[0];

    port1.addEventListener('message', (event) => {
      console.log('From parent:', event.data);
      port1.postMessage('Hello from iframe');
    });

    port1.start();
  }
});

三、BroadcastChannel API

3.1 基本用法

BroadcastChannel 允许同源的不同浏览器上下文(窗口、标签页、iframe、worker)之间进行通信。

// 创建广播频道
const channel = new BroadcastChannel('my-channel');

// 发送消息
channel.postMessage({ type: 'update', data: 'some data' });

// 接收消息
channel.addEventListener('message', (event) => {
  console.log('Received:', event.data);
});

// 关闭频道
channel.close();

3.2 与 MessageChannel 的对比

特性 MessageChannel BroadcastChannel
通信范围 两个端口之间 同源所有上下文
方向 双向 广播(所有订阅者)
连接建立 需要手动传递 port 自动发现同源订阅者
适用场景 iframe ↔ parent tab ↔ tab, worker ↔ window

3.3 实际应用:多标签页同步

// 所有标签页共享的 BroadcastChannel
const syncChannel = new BroadcastChannel('app-sync');

// 存储当前标签页的状态
let appState = { count: 0 };

// 当状态变化时广播
function updateState(newState) {
  appState = newState;
  syncChannel.postMessage({
    type: 'state-update',
    state: appState
  });
}

// 监听其他标签页的状态变化
syncChannel.addEventListener('message', (event) => {
  if (event.data.type === 'state-update') {
    appState = event.data.state;
    renderUI();
  }
});

// 监听标签页关闭
window.addEventListener('beforeunload', () => {
  syncChannel.postMessage({
    type: 'tab-closed'
  });
});

// 页面加载时广播
syncChannel.postMessage({
  type: 'tab-opened',
  tabId: generateTabId()
});

四、SharedWorker 通信

4.1 SharedWorker 简介

SharedWorker 是一种特殊的 Worker,可以被多个脚本同时访问。它适合需要在不同窗口间共享状态或逻辑的场景。

// shared-worker.js
const connections = new Set();

self.addEventListener('connect', (event) => {
  const port = event.ports[0];
  connections.add(port);

  port.addEventListener('message', (event) => {
    // 广播消息给所有连接的端口
    connections.forEach(p => {
      if (p !== port) {
        p.postMessage(event.data);
      }
    });
  });

  port.start();
});
// main.js - 连接到 SharedWorker
const worker = new SharedWorker('/shared-worker.js');

worker.port.addEventListener('message', (event) => {
  console.log('From worker:', event.data);
});

worker.port.start();
worker.port.postMessage('Hello from client');

五、Web Worker 消息传递

5.1 Worker 基本通信

// main.js
const worker = new Worker('/worker.js');

worker.postMessage({ type: 'task', data: 'some data' });

worker.addEventListener('message', (event) => {
  console.log('Result:', event.data);
});

// 终止 worker
worker.terminate();
// worker.js
self.addEventListener('message', (event) => {
  const { type, data } = event.data;

  if (type === 'task') {
    // 处理任务
    const result = processData(data);
    self.postMessage({ type: 'result', data: result });
  }
});

5.2 Transferable 对象

// 转移所有权而不是复制
const buffer = new ArrayBuffer(1024 * 1024);

worker.postMessage(buffer, [buffer]);  // buffer 在主线程中变为空

// 使用 Transferable 接口的对象
class MyTransferable {
  constructor(data) {
    this.data = data;
  }
}

六、综合示例:实时协作编辑

class CollaborationManager {
  constructor(channelName) {
    this.channel = new BroadcastChannel(channelName);
    this.localChanges = [];
    this.pendingOperations = [];

    this.init();
  }

  init() {
    // 监听远程变化
    this.channel.addEventListener('message', (event) => {
      this.handleRemoteMessage(event.data);
    });

    // 监听本地编辑
    document.addEventListener('input', (e) => {
      this.handleLocalInput(e);
    });

    // 定期同步
    setInterval(() => this.syncPendingOperations(), 1000);
  }

  handleLocalInput(e) {
    const operation = {
      type: 'edit',
      position: e.target.selectionStart,
      value: e.target.value,
      timestamp: Date.now()
    };

    this.localChanges.push(operation);
    this.pendingOperations.push(operation);

    // 广播本地变化
    this.channel.postMessage({
      type: 'operation',
      operation
    });
  }

  handleRemoteMessage(message) {
    switch (message.type) {
      case 'operation':
        this.applyRemoteOperation(message.operation);
        break;
      case 'sync-request':
        this.sendFullState(message.tabId);
        break;
      case 'sync-response':
        this.receiveFullState(message.state);
        break;
    }
  }

  applyRemoteOperation(operation) {
    // 应用远程操作到编辑器
    console.log('Applying remote operation:', operation);
  }

  syncPendingOperations() {
    if (this.pendingOperations.length > 0) {
      console.log('Syncing', this.pendingOperations.length, 'operations');
      this.pendingOperations = [];
    }
  }

  sendFullState(tabId) {
    this.channel.postMessage({
      type: 'sync-response',
      tabId,
      state: {
        content: document.querySelector('textarea').value,
        timestamp: Date.now()
      }
    });
  }

  receiveFullState(state) {
    console.log('Received full state:', state);
  }
}

// 使用
const collab = new CollaborationManager('editor-sync');

参考资料

延展阅读