消息通道与广播通信(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');