HTML5-Web Messaging

作为web开发,我们常常会碰到一个问题:  跨域通讯Cross-Domain communication, 面对同源原则问题 Same-Origin-Policy, JavaScript无法访问不同 域(或子域 协议(HTTP/HTTPs)或 端口中的代码   , 所以也没有直接的方法 (或者说简单) 解决跨域通讯问题. 但这样的场景还是会出现: A页面和B页面在不同的域,  B “嵌在”A上, 比如., 有个 "iframe" 在 A页面上,而其“源”却在B上, A页面需要控制这个B源,或者B源需要处理A页面.

限制解决方案是100%由JavaScript客户端实现的,在HTML5之前,有许多棘手的hacks方式,例如:

  1. URL长轮询:容器页面A改变iframe页面B的URL hash,B周期性的检查hash,一旦hash发生变化,它将根据约定的hash值采取行动。[随便说一下,这个模式可以被改进为非轮询的HTML  onchashchange event]

  2. CorssFrame,一种跨文档和跨域的安全通讯机制

  3. 窗口大小监视:一旦改变iframe的窗口大小,包含的窗口触发onresize事件,采取相应的行动。Google Mapplets采用了这种方式。

嗯,这些方式我个人都不喜欢,也许是不优雅,或者侵犯了DOM元素的原始功能,或者是太复杂。我相信许多人也不喜欢它们,我打赌甚至这些模式的发明者也不喜欢它们。这就是WHATWG在HTML5中创建跨域通讯:Web Messaging的原因。

作为一个HTML5的疯狂倡导者,我非常喜欢它,完全的客户端通讯,不影响服务器端,高效的,安全的(至少在理论上)

如何做

"Child" 可以是iframe 或通过window.open触发的弹出窗口, "parent page" 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>

    Notice, make sure to post message only when the iframe is loaded, otherwise thecontentWindowwill be still in the same domain with the container page.

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消息初步

简单的说 HTML5 Web消息机制就是JavaScript开放给浏览器的API, 以便在不同的内容间通讯 browsing context, 当JavaScript 从其中一个 tab/window 向另一个tab/window发消息时, 浏览器会定位指定域中的 tab/window , 然后触发消息事件 MessageEvent (继承自 DOMEvent)把消息发送到目标tab/window, 所以一旦源 tab/window已经触发了消息事件,它会受收到提示信息,最终通过MessageEvent.data传送消息.

真实环境

我在自己机器上试了下,改了一下hosts 文件,加上了Container.com, DomainA.com, DomainB.com 和 DomainC.com ,都指向 127.0.0.1:

127.0.1.1 Container.com
127.0.0.1 DomainA.com
127.0.0.1 DomainB.com
127.0.0.1 DomainC.com

然后准备了一个Containerpage :

<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>

然后是3个页面: A.htm放在 DomainA.com, B.htm 放在DomainB.com, C.htm 放在 DomainC.com, 实际上都是放在 C:\inetpub\wwwrooot, 然后在浏览器里输入DomainA/B/C.com  Smile | :) , 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>

关于跨域的消息流转,我截了个图:

HTML5-Web Messaging_第1张图片

消息通道

为了在不同的浏览器上下文下支持独立的通讯。HTML5引入了消息通道独立的发送消息。官方的定义如下:

通讯通道的机制实现了一个双向的管道,每端有一个端口。消息从一个端口发送到另一个端口,反之亦然。消息是异步的,并作为DOM事件发送。

我花费了大约半天的时间研究消息通道,最终实现了它。我的代码如下:

容器页的源码

<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,iframe的src指向不同域的页面(B)

  2. 一旦iframe加载,容器页发送一个带MessagePortArray的消息到页面B

  3. 页面B接收消息,数组包含了MessagePort对象的列表

  4. 页面B在端口实例中注册onmessage事件

  5. 页面B调用port.postmessage通过已建立的消息通道发送消息到页面A

目前,好像仅有Opera能正确的支持消息通道,Google Chrome和IE10平台预览版2在发送ports array的时候失败了,Safari不能触发onmessage事件。Firefox 7.0 beta版不支持MessageChannel对象。

注意:端口也能在HTML5 Web Workers之间通讯

还有一点(借用Steve Jobs 的话 ), 在使用Message Channel 时一定要注意,当使用完后一定记得要关闭通道,否则两个页面间会有强引用, W3 官方说明如下:

使用者记得要明确关闭MessagePortobjects以便其资源重新分配. 如若不然,或造成内存的大量浪费.

延伸阅读

  • HTML5 Web Messaging

  • window.postMessage - MDN Docs

  • HTML5 Demo: postMessage (cross domain)

  • HTML5's window.postMessage AP

  • Internet Explorer 10 Platform Preview: HTML5

  • http://ithelp.ithome.com.tw/question/10057709

原文在此:http://wayneye.com/

原文链接:http://www.codeproject.com/Articles/248264/HTML-WebMessaging-Experiment

你可能感兴趣的:(HTML5-Web Messaging)