HTML5 WebMessaging实验
HTML5 WebMessaging 为客户端的跨域通信提供了一种简单、高效、优雅且安全解决方案,本文就是对它的一个实验。
引言
作为 Web 开发者,我们有时会轻易遇到一个问题:跨域通信。遵循 同源策略,JavaScript 代码无法访问存在于不同域(或子域)、协议(HTTP/HTTPs)或端口的代码,因此没有直接(或者说:简单)的方法来实现跨域通信。然而,这类需求确实存在:页面 A 和页面 B 存在于不同的域,B 被“嵌入”在 A 中,即页面 A 中有一个“iframe
”,其“src
”是页面 B 的 URL,现在页面 A 希望控制页面 B,反之亦然。
将解决方案限制在 100% 客户端 JavaScript,HTML5 之前,有一些棘手的“技巧”,例如:
- URL 长轮询:容器页面 A 更改
iframe
页面 B 的 URL hash,B 会周期性地检查 hash,一旦 hash 发生变化,B 会根据约定的 hash 值执行相应操作。[顺便说一句,这种模式可以通过 HTML5 的 onhashchange 事件 来改进为非轮询模式] - CrossFrame,一种跨文档、跨域的安全通信机制。
- 窗口大小监控:更新
iframe
的窗口大小一次,然后包含它的窗口订阅其“onresize
”事件,并执行相应的操作。 Google Mapplets 就采用了这种模式。
好吧,我个人确实不喜欢这些方法……要么不够优雅,要么违反了 DOM 元素的原始功能,要么太复杂。我相信很多人也不喜欢它们,即使是这些模式的发明者,我敢打赌……这就是为什么 WHATWG 在 HTML5 中创建了跨域通信:Web Messaging。
作为 HTML5 的狂热倡导者,我非常喜欢它:完整的客户端通信,无服务器影响,高效,安全(至少理论上如此)。
操作指南
“子”可以是 iframe
,也可以是通过调用 window.open
打开的弹出窗口,“父页面” A 包含如下源代码:
<iframe id="ifr" src="http://domainB.com/B.htm" onload="sendCommand();">
No frame!
</iframe>
<script type="text/javascript">
function sendCommand() {
var ifr = document.getElementById("ifr");
ifr.contentWindow.postMessage("Hello", "http://domainB.com");
}
</script>
注意,务必仅在 iframe 加载完成后发送消息,否则
contentWindow
仍将与容器页面处于同一域。
子页面 B 包含如下代码:
<input type="button" value="Cross domain call" onclick="sendMsg();" />
<script>
window.addEventListener("message", receiveMessage, false);
function receiveMessage(evt) {
console.log("Page B received message from origin: %s.", evt.origin);
console.log("Event data: %s", evt.data);
//evt.source will be a window who sent the message,
//it can be used to post message to it
// Take action(s)
}
</script>
上面的演示代码是单向的:父页面发送消息给子页面(iframe
)。实际上,也可以实现双向消息传递,与“父页面控制子页面”类似,子页面将消息发送给容器窗口,唯一的区别是调用“parent.postMessage
”。
function receiveMessage(evt) {
evt.source.postmessage("Hello caller");
// or parent.postmessage("Hello parent");
}
Web Messaging 精要
总而言之,HTML5 Web Messaging 是 Web 浏览器公开的一套 JavaScript API,用于在不同的 浏览上下文之间进行通信。当一个浏览器标签页/窗口中的 JavaScript 代码尝试向另一个标签页/窗口传递消息时,Web 浏览器会在指定的域下定位目标标签页/窗口,并向目标标签页/窗口发送一个 MessageEvent(它继承自 DOMEvent)。因此,如果目标标签页/窗口已经订阅了消息事件,它就会收到通知,最终消息通过 MessageEvent.data
传递。
实时演示
我在我的开发机器上做了一个演示,我修改了本地的 hosts 文件,让 Container.com, DomainA.com, DomainB.com 和 DomainC.com 都指向 127.0.0.1。
127.0.0.1 Container.com
127.0.0.1 DomainA.com
127.0.0.1 DomainB.com
127.0.0.1 DomainC.com
我准备了一个 Container
页面,其中包含以下代码:
<h3>HTML5 Cross-Domain post message demo</h3>
<p id="infoBar">
</p>
<div id="wrapperA">
<input type="text" id="txtA" />
<input type="button" value="Post Message"
onclick="postMsgToIfr('A');" />
<iframe id="ifrA" src="http://DomainA.com/A.htm"></iframe>
</div>
<div id="wrapperB">
<input type="text" id="txtB" />
<input type="button" value="Post Message"
onclick="postMsgToIfr('B');" />
<iframe id="ifrB" src="http://DomainB.com/B.htm"></iframe>
</div>
<div id="wrapperC">
<input type="text" id="txtC" />
<input type="button" value="Post Message"
onclick="postMsgToIfr('C');" />
<iframe id="ifrC" src="http://DomainC.com/C.htm"></iframe>
</div>
<div style="CLEAR: both">
</div>
<script type="text/javascript">
window.addEventListener("message", receiveMessage, false);
var infoBar = document.getElementById("infoBar");
function receiveMessage(evt) {
infoBar.innerHTML += evt.origin + ": " + evt.data + "";
}
function postMsgToIfr(domain) {
switch (domain) {
case "A":
var ifr = document.getElementById("ifrA");
ifr.contentWindow.postMessage(document.getElementById
("txtA").value, "http://DomainA.com");
break;
case "B":
var ifr = document.getElementById("ifrB");
ifr.contentWindow.postMessage(document.getElementById
("txtB").value, "http://DomainB.com");
break;
case "C":
var ifr = document.getElementById("ifrC");
ifr.contentWindow.postMessage(document.getElementById
("txtC").value, "http://DomainC.com");
break;
default:
throw ("No such domain!");
}
}
</script>
以及三个页面:A.htm 位于 DomainA.com,B.htm 位于 DomainB.com,C.htm 位于 DomainC.com。实际上它们都位于 C:\inetpub\wwwrooot,而在浏览器中我手动输入 DomainA/B/C.com 来进行欺骗 :),A/B/C 页面的代码类似,如下所示:
<h4>DomainA/A.htm1</h4>
<input type="button" value="Cross domain call" onclick="doClick();" />
<div id="d"></div>
<script>
window.addEventListener("message", receiveMessage, false);
function doClick() {
parent.postMessage("Message sent from " + location.host, "http://container.com");
}
var d = document.getElementById("d");
function receiveMessage(evt) {
d.innerHTML += "Received message \"<span>" + evt.data + "</span>\" from domain: "
+ evt.origin + "";
}
</script>
我在下面录制了一个 GIF 图像来演示跨域消息传递:
看到消息在不同域之间双向传递了吗?是不是很酷?
MessageChannel
为了支持不同 浏览上下文下的独立通信,HTML5 引入了 **Message Channel** 来独立地发送消息,其官方定义如下:
此机制中的通信通道实现为双向管道,每端有一个端口。在一个端口发送的消息会在另一个端口接收到,反之亦然。消息是异步的,并以 DOM 事件的形式传递。
我花了大約半天時間研究 Message Channel,終於讓它工作了,我的代碼如下。
容器页面源代码
<iframe id="ifr" src="http://wayneye.me/WebProjects/HRMS/Opener.html"
önload="initMessaging()"></iframe>
<input type="button" value="Post Message" onclick="postMsg();" />
<div id="d"></div>
<script>
var d = document.getElementById("d");
var channel = new MessageChannel();
channel.port1.onmessage = function (evt) {
d.innerHTML += evt.origin + ": " + evt.data + "";
};
function initMessaging() {
var child = document.getElementById("ifr");
child.contentWindow.postMessage('hello', 'http://wayneye.me', [channel.port2]);
}
function postMsg() {
channel.port1.postMessage('Message sent from ' + location.host);
}
</script>
iframe 页面源代码
<div id="info"></div>
<input type="button" value="Post Message" önclick="postMsg();" />
<script>
var info = document.getElementById("info");
var port = null;
window.addEventListener("message", function (e) {
console.log(e);
if(e.ports && e.ports.length > 0) {
port = e.ports[0];
port.start();
port.addEventListener("message", function (evt) {
info.innerHTML += "Received message \"" + evt.data + "\"
from domain: " + evt.origin + "";
}, false);
}
}, false);
function postMsg() {
if(port) {
port.postMessage("Data sent from " + location.host);
}
}
</script>
整个过程可以描述为:
- 容器页面(A)嵌入了一个
iframe
,其src
指向不同域下的页面(B)。 iframe
加载完成后,容器页面向页面 B 发送一条消息,其中包含一个 MessagePortArray。- 页面 B 接收到消息以及包含
MessagePort
对象列表的数组。 - 页面 B 在端口实例上注册
onmessage
事件。 - 页面 B 调用
port.postmessage
,通过此MessageChannel
将消息发送到页面 A。
此时,我似乎只有 Opera 正确支持MessageChannel
。Google Chrome 和 IE10 Platform Preview 2 未能传递 ports 数组,Safari 无法触发onmessage
事件。Firefox 7.0 beta 版本不支持MessageChannel
对象。
注意:端口还可以实现 HTML5 Web Workers 之间的通信。
还有一件事(抄袭史蒂夫·乔布斯 :)),使用 Message Channel 时需要注意的一点是,Web 开发者应该在不再需要时显式关闭通道,否则两个页面之间会保持强引用,正如 W3 官方页面强调的那样:
强烈建议作者显式关闭
MessagePort
对象以解除它们之间的关联,从而可以回收它们的资源。创建许多MessagePort
对象而不关闭它们可能会导致内存占用过高。
延伸阅读
- HTML5 Web Messaging
- window.postMessage - MDN 文档
- HTML5 演示:postMessage (跨域)
- HTML5 的 window.postMessage API
- Internet Explorer 10 Platform Preview:HTML5
- http://ithelp.ithome.com.tw/question/10057709
最初发布于 Wayne's Geek Life http://WayneYe.com: http://wayneye.com/Blog/HTML5-WebMessaging-Experiment/。