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);
}
</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");
}
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
whose src
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 this MessageChannel
.
At this timestamp, I seems only Opera correctly supports MessageChannel
, Google Chrome and IE10 Platform Preview 2 failed to deliver the ports array, Safari cannot gets the onmessage
event fired. Firefox 7.0 beta event doesn't support MessageChannel
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 many MessagePort
objects and discarding them without closing them can lead to high memory usage.
Further Reading
Originally posted at Wayne's Geek Life http://WayneYe.com: http://wayneye.com/Blog/HTML5-WebMessaging-Experiment/.