HTML5 - Web 消息传递
HTML5 - Web 消息通信
Section titled “HTML5 - Web 消息通信”Web 消息通信定义了一些机制,允许不同浏览上下文(如独立的窗口、标签页、iframe 或 Worker)中的脚本相互通信,即使它们源自不同的源(域名、协议或端口),从而在受控的方式下克服同源策略的限制。
这使得安全的跨文档和跨域通信成为可能,对于以下场景至关重要:
- 一个页面与来自不同域的嵌入式
<iframe>进行通信(例如,与嵌入的地图、视频播放器或广告互动)。 - 一个页面打开一个新窗口(
window.open())并与它交换数据。 - 一个页面与其 Web Worker 或 Service Worker 进行通信。
- 同一页面上的不同
<iframe>元素通过父页面相互通信。
跨文档消息通信(postMessage)
Section titled “跨文档消息通信(postMessage)”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对象数组(用于通道消息通信)。
示例:页面与 Iframe 通信
Section titled “示例:页面与 Iframe 通信”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)
Section titled “通道消息通信(MessageChannel)”通道消息通信提供了一种更直接的双向通信通道,用于在两个浏览上下文之间进行通信。它使用 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 消息监听器潜在的冲突。