65.9K
CodeProject 正在变化。 阅读更多。
Home

HTML5 WebMessaging实验

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (9投票s)

2011 年 9 月 4 日

CPOL

5分钟阅读

viewsIcon

58439

downloadIcon

880

HTML5 WebMessaging 为客户端的跨域通信提供了一种简单、高效、优雅且安全解决方案,本文就是对它的一个实验。

引言

作为 Web 开发者,我们有时会轻易遇到一个问题:跨域通信。遵循 同源策略,JavaScript 代码无法访问存在于不同(或子域)、协议(HTTP/HTTPs)或端口的代码,因此没有直接(或者说:简单)的方法来实现跨域通信。然而,这类需求确实存在:页面 A 和页面 B 存在于不同的域,B 被“嵌入”在 A 中,即页面 A 中有一个“iframe”,其“src”是页面 B 的 URL,现在页面 A 希望控制页面 B,反之亦然。

将解决方案限制在 100% 客户端 JavaScript,HTML5 之前,有一些棘手的“技巧”,例如:

  1. URL 长轮询:容器页面 A 更改 iframe 页面 B 的 URL hash,B 会周期性地检查 hash,一旦 hash 发生变化,B 会根据约定的 hash 值执行相应操作。[顺便说一句,这种模式可以通过 HTML5 的 onhashchange 事件 来改进为非轮询模式]
  2. CrossFrame,一种跨文档、跨域的安全通信机制。
  3. 窗口大小监控:更新 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 图像来演示跨域消息传递:

HTML5 Cross-Domain Messaging Demo

看到消息在不同域之间双向传递了吗?是不是很酷?

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>

整个过程可以描述为:

  1. 容器页面(A)嵌入了一个 iframe,其 src 指向不同域下的页面(B)。
  2. iframe 加载完成后,容器页面向页面 B 发送一条消息,其中包含一个 MessagePortArray
  3. 页面 B 接收到消息以及包含 MessagePort 对象列表的数组。
  4. 页面 B 在端口实例上注册 onmessage 事件。
  5. 页面 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 对象而不关闭它们可能会导致内存占用过高。

延伸阅读

最初发布于 Wayne's Geek Life http://WayneYe.com: http://wayneye.com/Blog/HTML5-WebMessaging-Experiment/

© . All rights reserved.