Skip to content

HTML5 - Web 消息传递

Web 消息通信定义了一些机制,允许不同浏览上下文(如独立的窗口、标签页、iframe 或 Worker)中的脚本相互通信,即使它们源自不同的源(域名、协议或端口),从而在受控的方式下克服同源策略的限制。

这使得安全的跨文档和跨域通信成为可能,对于以下场景至关重要:

  • 一个页面与来自不同域的嵌入式 <iframe> 进行通信(例如,与嵌入的地图、视频播放器或广告互动)。
  • 一个页面打开一个新窗口(window.open())并与它交换数据。
  • 一个页面与其 Web Worker 或 Service Worker 进行通信。
  • 同一页面上的不同 <iframe> 元素通过父页面相互通信。

Web 消息通信最常见形式使用 window.postMessage() 方法。它允许一个窗口(或框架、或 Worker)安全地向另一个窗口(或框架、或 Worker)发送数据消息。

要发送消息,您需要获取目标窗口对象(otherWindow)的引用,并调用其 postMessage 方法:

otherWindow.postMessage(message, targetOrigin, [transfer]);
  • message: 要发送的数据。这可以是任何可以被结构化克隆的对象(字符串、数字、布尔值、数组、对象、Blob、ArrayBuffer 等)。
  • targetOrigin: 一个关键的安全参数。它指定目标窗口必须拥有的源(例如,'https://example.com','*'),消息才会被投递。仅当您不需要限制接收方源时使用 '*'(安全性较低)。发送敏感数据时,务必指定一个具体的源。
  • transfer (可选): 一个 Transferable 对象数组(如 ArrayBuffer、MessagePort、ImageBitmap),它们的所属权会被转移到接收上下文,通过避免复制大数据来提高性能。

要接收消息,目标窗口需要监听 message 事件:

window.addEventListener('message', receiveMessage, false);
function receiveMessage(event) {
// 1. SECURITY CHECK: Verify the origin of the sender
// 1. 安全检查:验证发送方的源
if (event.origin !== 'https://expected-sender.com') {
console.warn('Message received from unexpected origin:', event.origin);
// console.warn('收到来自非预期源的消息:', event.origin);
return; // Ignore messages from untrusted origins
// return; // 忽略来自不受信任源的消息
}
// 2. Access the data
// 2. 访问数据
const receivedData = event.data;
console.log('Message received:', receivedData);
// console.log('收到消息:', receivedData);
// 3. Optional: Access the source window (for sending replies)
// 3. 可选:访问发送方窗口(用于发送回复)
const sourceWindow = event.source;
if (sourceWindow) {
// Example: Send a reply back
// 示例:发送回复
sourceWindow.postMessage('Message received successfully!', event.origin);
// sourceWindow.postMessage('消息接收成功!', event.origin);
}
// 4. Optional: Access transferred MessagePorts (see Channel Messaging)
// 4. 可选:访问已转移的 MessagePorts(参见通道消息通信)
if (event.ports && event.ports.length > 0) {
// Handle MessagePorts
// 处理 MessagePorts
}
// Process the receivedData...
// 处理接收到的数据...
}

事件处理程序中的 event 对象提供:

  • data: postMessage 发送的实际消息对象。
  • origin: 发送消息的窗口的源(例如,'https://sender.com')。对安全检查至关重要。
  • source: 发送消息的 window 对象的引用。用于发送回复。
  • ports: 与消息一起转移的 MessagePort 对象数组(用于通道消息通信)。

parent.html(主页面):

<!DOCTYPE html>
<html>
<head><title>Parent Page</title></head>
<body>
<h1>Parent Page</h1>
<iframe id="childFrame" src="https://other-domain.com/child.html" width="400" height="200"></iframe>
<button onclick="sendMessageToChild()">Send Message to Iframe</button>
<div id="response"></div>
<script>
const childFrame = document.getElementById('childFrame');
const responseDiv = document.getElementById('response');
const childOrigin = 'https://other-domain.com'; // IMPORTANT: Set the correct origin
// const childOrigin = 'https://other-domain.com'; // 重要:设置正确的源
function sendMessageToChild() {
const message = { text: 'Hello from parent!', timestamp: Date.now() };
// const message = { text: '来自父页面的问候!', timestamp: Date.now() };
// Get the iframe's contentWindow
// 获取 iframe 的 contentWindow
if (childFrame.contentWindow) {
console.log(`Sending message to ${childOrigin}:`, message);
// console.log(`发送消息到 ${childOrigin}:`, message);
childFrame.contentWindow.postMessage(message, childOrigin);
}
}
// Listen for messages from the child iframe
// 监听来自子 iframe 的消息
window.addEventListener('message', (event) => {
// SECURITY CHECK!
// 安全检查!
if (event.origin !== childOrigin) {
console.warn('Ignoring message from unexpected origin:', event.origin);
// console.warn('忽略来自非预期源的消息:', event.origin);
return;
}
console.log('Message received from iframe:', event.data);
// console.log('收到来自 iframe 的消息:', event.data);
responseDiv.textContent = `Response from iframe: ${JSON.stringify(event.data)}`;
// responseDiv.textContent = `来自 iframe 的响应: ${JSON.stringify(event.data)}`;
});
</script>
</body>
</html>

child.html(Iframe 内容 - 托管在 https://other-domain.com):

<!DOCTYPE html>
<html>
<head><title>Child Iframe</title></head>
<body style="background-color: lightblue;">
<h2>Iframe Content</h2>
<div id="received">Waiting for message...</div>
<!-- <div id="received">等待消息...</div> -->
<script>
const receivedDiv = document.getElementById('received');
const parentOrigin = 'https://parent-domain.com'; // IMPORTANT: Set the correct origin of the parent
// const parentOrigin = 'https://parent-domain.com'; // 重要:设置父页面的正确源
window.addEventListener('message', (event) => {
// SECURITY CHECK!
// 安全检查!
if (event.origin !== parentOrigin) {
console.warn('Ignoring message from unexpected origin:', event.origin);
// console.warn('忽略来自非预期源的消息:', event.origin);
return;
}
console.log('Message received from parent:', event.data);
// console.log('收到来自父页面的消息:', event.data);
receivedDiv.textContent = `Received: ${JSON.stringify(event.data)}`;
// receivedDiv.textContent = `收到: ${JSON.stringify(event.data)}`;
// Send a reply back to the parent window (event.source)
// 发送回复给父页面(event.source)
if (event.source) {
const reply = { confirmation: 'Message processed by iframe!' };
// const reply = { confirmation: '消息已由 iframe 处理!' };
console.log(`Sending reply to ${event.origin}:`, reply);
// console.log(`发送回复到 ${event.origin}:`, reply);
event.source.postMessage(reply, event.origin);
}
});
</script>
</body>
</html>

通道消息通信提供了一种更直接的双向通信通道,用于在两个浏览上下文之间进行通信。它使用 MessageChannel API。

创建 MessageChannel 实例会给您两个连接的 MessagePort 对象(port1 和 port2)。其中一个端口留在发起通信的上下文,而另一个端口使用 postMessage 的可选第三个参数转移到目标上下文。

一旦端口被转移,两个上下文就可以通过各自的端口使用 port.postMessage() 和监听 port.onmessage(或 port.addEventListener('message', ...))直接通信。这样就无需使用 window 引用,也无需对在该特定通道上的后续消息进行源检查。

概念示例:页面使用 MessageChannel 与 Iframe 通信

Section titled “概念示例:页面使用 MessageChannel 与 Iframe 通信”

parent.html:

// ... (get iframe reference as before)
// ... (像之前一样获取 iframe 引用)
function setupChannel() {
const channel = new MessageChannel();
const port1 = channel.port1;
const port2 = channel.port2;
// Send port2 to the iframe, transferring its ownership
// 将 port2 发送给 iframe,转移其所有权
if (childFrame.contentWindow) {
console.log(`Sending port2 to ${childOrigin}`);
// console.log(`发送 port2 到 ${childOrigin}`);
childFrame.contentWindow.postMessage('init-channel', childOrigin, [port2]);
}
// Listen for messages on port1 (from the iframe)
// 监听 port1 上的消息(来自 iframe)
port1.onmessage = (event) => {
console.log('Message received via channel port1:', event.data);
// console.log('通过通道 port1 收到消息:', event.data);
responseDiv.textContent = `Channel response: ${JSON.stringify(event.data)}`;
// responseDiv.textContent = `通道响应: ${JSON.stringify(event.data)}`;
// Send message back via channel
// 通过通道发送消息回去
port1.postMessage({ reply: 'Got your channel message!' });
// port1.postMessage({ reply: '收到了你的通道消息!' });
};
// Start the port (needed for onmessage to fire)
// 启动端口(onmessage 事件触发需要)
port1.start();
}
// Call setupChannel() when iframe is loaded, e.g.:
// 例如,当 iframe 加载完成时调用 setupChannel():
// childFrame.addEventListener('load', setupChannel);

child.html:

// ... (setup parentOrigin)
// ... (设置 parentOrigin)
let channelPort = null;
window.addEventListener('message', (event) => {
// SECURITY CHECK on origin!
// 对源进行安全检查!
if (event.origin !== parentOrigin) return;
// Check if this is the channel initiation message
// 检查这是否是通道初始化消息
if (event.data === 'init-channel' && event.ports && event.ports.length > 0) {
channelPort = event.ports[0];
console.log('Received channel port2');
// console.log('已接收通道 port2');
// Listen for messages on the received port
// 监听接收到的端口上的消息
channelPort.onmessage = (portEvent) => {
console.log('Message received via channel port2:', portEvent.data);
// console.log('通过通道 port2 收到消息:', portEvent.data);
receivedDiv.textContent = `Channel msg: ${JSON.stringify(portEvent.data)}`;
// receivedDiv.textContent = `通道消息: ${JSON.stringify(portEvent.data)}`;
};
// Start the port
// 启动端口
channelPort.start();
// Send an initial message back through the channel
// 通过通道发送一条初始消息回去
channelPort.postMessage({ status: 'Channel ready in iframe!' });
// channelPort.postMessage({ status: '通道在 iframe 中已准备就绪!' });
} else {
// Handle regular postMessage events if needed
// 如果需要,处理常规的 postMessage 事件
console.log('Regular message received from parent:', event.data);
// console.log('收到来自父页面的常规消息:', event.data);
}
});

通道消息通信特别适用于建立专用的通信线路,尤其是在处理多个 iframe 或 Worker 时,因为它避免了与通用 window 消息监听器潜在的冲突。