Maybe you know, maybe you don’t, maybe you just don’t care but jQuery-JSONP passed the 2.0 landmark.
What makes v2 so special is that the plugin no longer uses iframes internally while still allowing concurrent requests with the same callback name, a feature that was mandatory to me and caused much delay.
And if you have no clue what I’m talking about, go make some JSONP or, better yet, close this very tab and forget you ever heard about the UHKAJSONWP (Ugly Hack Known As JSON With Padding) a.k.a JSONP: you’re better off without this thing, believe me.
If you’re still here then I guess you broke two or three teeth on some fuglily designed “Data API” or on some script tag injection trickeries… or both. You may be a veteran but the hack I’m about to talk about is not for the faint of heart. It is the ugliest thing that works as intended I ever uncovered. Though you have to be as out of your mind as I am to expect it to behave like it does.
the problem
You make a bunch of JSONP requests with the same callback name using script tag injections. Successful requests will all call the same function at one point or another but you have no clue in which order.
So you’ll end up with a bunch of script tag nodes in your document at one point:
With omg.php being something like this:
usleep( mt_rand( 0 , 3000000 ) );
echo( "$_GET[callback]( $_GET[param] );" );
You have no idea how long it will take the omg.php script to answer each request (a wild guess is something roughly between 0 and 3 seconds but that’s because OMG PHP IS SO SLOW!!!111).
Anyway, thing is you also have no idea in what order the calls to myFunc will come.
why it is a problem
Well, you could get around it using iframes like jQuery-JSONP v1 did.
Though it means you accept the following limitations:
- ugly, lenghty code,
- no document.domain in IE (unless you provide a specific document like Ben Alman does for his hashchange plugin, something I’m not comfortable with — the document, not the plugin which is awesome like anything benalmanian ought to be),
- a beautiful, and as intrusive as browserly possible, loading spinner everytime you make a request (especially annoying for background pollers), very poor performance (mainly because of the spinner above). Note that some browsers decided recently that it was ok to throw the spinner at us for simple script tag injection too so meh.
How predictable of you, by the way.
Well, here are two reasons for the naysayer in you:
- what if the Data API you use has a fixed callback name (you would be surprised how many of them expect the authentification process, in particular, to be so unique it needs its very own, fixed, callback name — too bad if you want to make an aggregator which needs to log in with several accounts),
- you cannot take advantage of the browser’s cache if you change the callback name at each request (some big services, like youtube, issue 304 HTTP responses for recently issued requests… but only if the callback name is the same, of course).
the solution
Whatever the browser, you cannot intervene before a script is executed. You can only act when the script has been loaded and executed or an error occured.
Since you cannot intervene before the execution and you don’t know in which order the scripts will be loaded and executed, the callback has to be generic. The trick here is to set a (scoped) global variable in this generic callback and test for it (and reset it) in each of your requests callback:
// some generic code
// common to all requests
var lastValue;
function genericCallback( value ) {
lastValue = [ value ];
}
// then the request specific closure
function request( options ) {
window[ options.callback ] = genericCallback;
function requestCallback() {
var tmp = lastValue;
lastValue = undefined;
if ( ! tmp ) {
options.error();
} else {
options.success( tmp[ 0 ] );
}
}
// create the script tag
// "attach" requestCallback to the script tag
// put the script tag in the DOM
}
Attaching the request callback to the script tag is a no-brainer and also a one-liner in Firefox and Webkit browsers:
scriptTag.onload = scriptTag.onerror = requestCallback;
Of course, Mr Pedantic… I mean Opera, doesn’t provide the non-standard error callback hook. Please note that the engineers behind this oh so standard browser took the time to develop a completely proprietary and bloated Scope protocol for debugging… or, at least, that’s what I gathered from the highly esoteric introductions I took the time to read. I dunno, maybe they hired some transfuges from Redmond lately.
Anyway, the trick here is to use Opera’s “in-order” execution of injected script tags and add a secondary script tag with some inline code calling the first script tag onload handler (yeah, thank you Opera). To make things clear, you end up with something like this in your DOM:
The script are executed in order. So if you reach the second script tag, something went wrong (it’s your onerror callback). Of course you need to add the appropriate cleanup code and so on but you get the idea.
the problem under internet explorer
Your first instinct when it comes to Internet Explorer would be to do something like this:
scriptTag.onreadystatechange = function() {
if ( /loaded|complete/.test( scriptTag.readyState ) ) {
requestCallback();
}
};
And guess what? It works! Well, until you CTRL-F5 the hell out of quite a simple PHP script.
Then the truth hits you: onreadystatechange is not called right after the script has been executed or has failed. It is just called “after”… an “after” that may very well be after another script tag has finished loading and executing.
the solution for internet explorer
Now fasten your seat belts because it’s were everything goes haywire the Roland Emmerich way.
At this point, we’re pretty much doomed unless we dig deep down the msdn. Let’s have a look at the code below:
id="scriptId"
for="divId"
event="onclick"
src="script.js">
What it does is pretty straight-forward actually: it attempts to load script.js. If it succeeds, it creates an onclick property on the div and, whether it succeeds or not, it fires onreadystatechange with a “loaded” readyState (“complete” if already in the cache). The big plus is that the script has not been executed yet!
But let’s take it a little further: do we really need the div? Could we target the script tag itself? The answer is yes:
id="scriptId"
for="scriptId"
event="onclick"
src="script.js">
So here we are, with our magic script tag loading a script as its own onclick handler. Now the solution for Internet Explorer is as follows:
scriptTag.event = "onclick";
scriptTag.id = scriptTag.htmlFor = generateNewId();
scriptTag.onreadystatechange = function() {
if ( /loaded|complete/.test( scriptTag.readyState ) ) {
try {
scriptTag.onclick();
} catch( e ) {}
requestCallback();
}
};
the downside
Will this ugly ugly hack work in IE9? Somehow I hope not, as this is probably the weirdest thing I ever witnessed in a browser. Somehow I hope it will because I’m so damn tired of finding hacks for your “products”, Microsoft devs.
thank you and good night
Sleep well. It’s been far too long a post for far too ugly a hack.
@jaubourg