HTML5 WebMessaging Experiment
HTML5 WebMessaging provides a simple, efficient, elegant and secure solution for Cross-Domain communication in client side, this article is an experiment for it.
Introduction
As web developers, sometimes we easily encounter one problem: Cross-Domain communication, conforming Same-Origin-Policy, JavaScript code cannot access code stay in different domain (or sub-domain) or protocol (HTTP/HTTPs) or port, so there was no direct (or I can say: simple) way to achieve Cross-Domain Communication. However, those kinds of requirements do happen: page A and page B are in different domains, B is "embedded" in A, i.e., there is an "iframe
" in page A whose "src
" is page B's URL, now page A wants to control page B and vice-versa.
By limiting the solution to be done by 100% client JavaScript, beforehand HTML5, there are a number of tricky "hacks", such as:
- URL long polling: Container page A changes the
iframe
page B's URL hash, and B periodically checks the hash, once the hash changed, it takes action according to the contracted hash value. [BTW, the pattern can be revised to be non-polling by HTML5 onchashchange event] - CrossFrame, a Safe Communication Mechanism Across Documents and Across Domains.
- Window Size Monitoring: Update the
iframe
's window size once, and the containing window subscribes its "onresize
" event then takes corresponding action(s). Google Mapplets adopted this pattern.
Well, personally I really don't like all of them... either inelegant, or violates the original functionalities of DOM elements, or too complicated. I believe many people don't like them too even the pattern inventors I bet... That why WHATWG created Cross-Domain communication in HTML5: Web Messaging.
As an HTML5 crazy advocator I like it very much, complete client communication, no server impact, efficient, secure (at least in theory).
How To
"Child" can be an iframe
or an popup window by invoking window.open
, "parent page" A contains source code like below:
<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 the
contentWindow
will be still in the same domain with the container page.
Child page B contains code like below:
<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>
The demo code above is one direction: parent sends message to child (iframe
), actually bi-directional message transfer can also be done, similar with "Parent control child", child page posts message to container window, the only difference is to call "parent.postMessage
".
function receiveMessage(evt) {
evt.source.postmessage("Hello caller");
// or parent.postmessage("Hello parent");
}
Web Messaging Essential
In a nutshell, HTML5 Web Messaging is a suite of JavaScript API exposed by web browser, to communicate between different browsing context, when JavaScript code in one browser tab/window tries to deliver a message to another tab/window, web browser locates the target tab/window under the specified domain, and file a MessageEvent (which inherits from DOMEvent) to the target tab/window, so if the target tab/window already subscribed the message event, it will gets notified, eventually the message got delivered through MessageEvent.data
.
Live Demo
I've done a demo in my dev machine, I override my local hosts file to let Container.com, DomainA.com, DomainB.com and DomainC.com all point to 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
I prepared a Container
page which contains the code below:
<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>
And three pages: A.htm located in DomainA.com, B.htm located in DomainB.com, C.htm located in DomainC.com, physically they are all located under C:\inetpub\wwwrooot, while in the browser I manually type DomainA/B/C.com to make the cheat :), the code in the A/B/C pages are similar, shown below:
<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>
I recorded a GIF image below to demonstrate the Cross-Domain messaging:
See the message passed through different domains in bi-direction? Isn't it cool?
MessageChannel
To support independent communication under different browsing context, HTML5 introduced Message Channel to post message independently, its official definition is shown below:
Communication channels in this mechanisms are implemented as two-way pipes, with a port at each end. Messages sent in one port are delivered at the other port, and vice-versa. Messages are asynchronous, and delivered as DOM events.
I spent about half a day investigating the Message Channel and finally got it working, my code is shown below.
Container page source code
<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 page source code
<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>
The entire process can be described as:
- Container page (A) embedded an
iframe
whosesrc
is pointing to a page (B) in different domain. - Once the
iframe
loaded, the container posts a message to page B with a MessagePortArray. - Page B received the message as well as the array containing a list of
MessagePort
objects. - Page B registers
onmessage
event on port instance. - Page B invokes
port.postmessage
to send a message to page A through thisMessageChannel
.
At this timestamp, I seems only Opera correctly supportsMessageChannel
, Google Chrome and IE10 Platform Preview 2 failed to deliver the ports array, Safari cannot gets theonmessage
event fired. Firefox 7.0 beta event doesn't supportMessageChannel
object.
Note: Ports can also enable communication between HTML5 Web Workers.
One more thing (plagiarize Steve Jobs :) ), to use Message Channel one noticeable point is, web developer should explicitly close the channel once there is no need, otherwise there will be a strong reference between two pages, as W3 official page emphasizes below:
Authors are strongly encouraged to explicitly close
MessagePort
objects to disentangle them, so that their resources can be recollected. Creating manyMessagePort
objects and discarding them without closing them can lead to high memory usage.
Further Reading
- 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
Originally posted at Wayne's Geek Life http://WayneYe.com: http://wayneye.com/Blog/HTML5-WebMessaging-Experiment/.