Backwards compatible window.postMessage()
Simple cross-domain messaging
This blog post explains how to implement a backwards compatible version of window.postMessage() to handle all your cross-domain messaging needs. If you’re in a hurry, you can skip directly to the demo or just grab the following files:
Background
One of the trickiest things you will ever run into on the web is the same origin policy. The same origin policy basically limits how scripts and frames on different domains can talk to each other. The same origin policy is an important part of your security on the web. For example, it prevents someone from being able to steal your password from another frame on the page. The annoying thing is there are sometimes perfectly valid reasons for frames on different domains to need to talk to one another. One good example of this would be the Facebook Connect library where facebook.com needs to be able to communicate with non-facebook.com domains. Over the years we’ve developed a series of hacks to work around this browser limitation. Some developers have used flash while others have relied on a window.location.hash hack. Facebook worked around it by getting people to install a cross domain communication channel. It got pretty ridiculous until the browser makers finally decided to give us a way to do cross-domain messaging without all the nonsense. The result was window.postMessage() which is supported by the latest browsers like Firefox 3, Safari 4, Chrome and IE 8. Unfortunately, as usual we’re going to need a backwards compatible version before we can take advantage of this new functionality.
I found a couple great examples of people who have tackled this already. Luke Shepard wrote xd.js which is part of the open-sourced Facebook Connect code. I also found Ben Alman’s jQuery plugin which does a really nice job. Both of these scripts are great, but neither fits quite right with my needs. For one, I wanted the smallest possible script written in pure JavaScript. I’m a fan of jQuery, but since I’ll be installing this code on other people’s domains I can’t assume that jQuery will be available and while I could load it up it’s important to keep the file size small. So what I did was start with Ben’s code and took out all of the jQuery dependencies. Here is the result:
The code
var XD = function(){
var interval_id,
last_hash,
cache_bust = 1,
attached_callback,
window = this;
return {
postMessage : function(message, target_url, target) {
if (!target_url) {
return;
}
target = target || parent; // default to parent
if (window['postMessage']) {
// the browser supports window.postMessage, so call it with a targetOrigin
// set appropriately, based on the target_url parameter.
target['postMessage'](message, target_url.replace( /([^:]+:\/\/[^\/]+).*/, '$1'));
} else if (target_url) {
// the browser does not support window.postMessage, so use the window.location.hash fragment hack
target.location = target_url.replace(/#.*$/, '') + '#' + (+new Date) + (cache_bust++) + '&' + message;
}
},
receiveMessage : function(callback, source_origin) {
// browser supports window.postMessage
if (window['postMessage']) {
// bind the callback to the actual event associated with window.postMessage
if (callback) {
attached_callback = function(e) {
if ((typeof source_origin === 'string' && e.origin !== source_origin)
|| (Object.prototype.toString.call(source_origin) === "[object Function]" && source_origin(e.origin) === !1)) {
return !1;
}
callback(e);
};
}
if (window['addEventListener']) {
window[callback ? 'addEventListener' : 'removeEventListener']('message', attached_callback, !1);
} else {
window[callback ? 'attachEvent' : 'detachEvent']('onmessage', attached_callback);
}
} else {
// a polling loop is started & callback is called whenever the location.hash changes
interval_id && clearInterval(interval_id);
interval_id = null;
if (callback) {
interval_id = setInterval(function() {
var hash = document.location.hash,
re = /^#?\d+&/;
if (hash !== last_hash && re.test(hash)) {
last_hash = hash;
callback({data: hash.replace(re, '')});
}
}, 100);
}
}
}
};
}();
Usage:
There are two parts to using this code: posting and listening. Both are relatively simple. To post a message we call XD.postMessage with a message, a URL and the frame that we want to talk to. Notice that we start off by passing the URL of the parent page to the child frame. This is important so the child knows how to talk back to the parent.
src = 'http://joshfraser.com/code/postmessage/child.html#' + encodeURIComponent(document.location.href);
document.getElementById("xd_frame").src = src;
function send(msg) {
XD.postMessage(msg, src, frames[0]);
return false;
}
Setting up the listener on the child is also easy to do:
XD.receiveMessage(function(message){
window.alert(message.data + " received on "+window.location.host);
}, 'http://onlineaspect.com');
I recommend taking a look at this barebones example to understand better how the various pieces fit together. This is still a work in progress and I’d love any feedback you have on it. I’m particularly interested in adding Flash as an alternative method before falling back to fragments. This is what the Facebook code does and I like it because it eliminates the nasty polling every 100ms.
Got other thoughts on how to make this better? Let me know in the comments.