Category Archives: JavaScript

Defensive AJAX and AJAX retries in jQuery

A lot of code that I have seen over the years always assumes success, particularly with AJAX calls. This creates code that is fragile, and entirely dependant on the result of external (to the client) code.

There are a few ways we can attempt to protect against this. The first, is to always depend on the result of the call to make any changes to the DOM or UI. For example, let’s say we have a quantity field in a shopping basket on an e-commerce site. When the user clicks a plus next to the field, we make an AJAX call that increments a value in a basket stored server-side and updates the UI to match. It is very common, and tempting, to increment the value in the UI immediately, and then make the AJAX call. There are a few obvious problems here. If the AJAX call fails, now we have a UI that is inaccurate. We can decrement the value in the UI to make up for it, but now we are starting to confuse the user with the numbers jumping up and down, and if they have clicked the button six times in quick succession (very common use of this kind of control) our chance of error for accurate UI representation of data increases dramatically.

A much safer way to do this is to return the current stored quantity value from the server with each AJAX call, and only update the UI when the AJAX call completes. This results in a less “snappy” feeling UI, and it will be necessary to display some kind of visual cue that something is going on the background, but the user learns quickly how the site works and this is a much more robust process.

Without having any kind of genuine statistics to call upon, I would suggest that nine out of ten AJAX calls that fail are due to an issue that is temporary and would be resolved by a retry. Anything due to network issues, a lost packet somewhere, a brief server glitch, load balancing problems and so on can cause a timeout or 404 without the target resource actually being missing or consistently failing. Often when a website fails to load, I click refresh and there it is. An AJAX call is just the same. (The same applies to database connections, or anything dependant on a network resource.)

Therefore, rather than display an error on an AJAX timeout (“we could not connect to the server” or “there was a problem, please try later”) or worse, doing nothing at all, there are some things we can try to resolve the issue ourselves without bothering the user about it until we’re certain that it is broken.

Let’s look at a typical jQuery AJAX call.

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	success : function(json) {
		//do something
	}
});

The immediate problem with this is it completely assumes (and depends upon) success. There is not even a basic error handler. Something like this is better:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	success : function(json) {
		//do something
	},
	error : function() {
		alert('Oops! There was a problem, sorry.');
	}
});

The error callback function is actually passed three arguments.

  • The XMLHTTPRequest object in use
  • A textual equivalent of the status
  • The actual exception thrown

These allow us to react in a more sophisticated manner:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	success : function(json) {
		//do something
	},
	error : function(xhr, textStatus, errorThrown ) {
		if (xhr.status == 500) {
			alert('Oops! There seems to be a server problem, please try again later.');
		} else {
			alert('Oops! There was a problem, sorry.');
		}
	}
});

Since the error function lives inside the AJAX object itself, and is called in that context, the this keyword very usefully points to the jQuery AJAX instance itself. Using this, and the arguments we are being passed we can very easily set the UI to retry the AJAX call on our behalf. Let’s attach some extra properties to the AJAX object:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	tryCount : 0,
	retryLimit : 3,
	success : function(json) {
		//do something
	},
	error : function(xhr, textStatus, errorThrown ) {
		if (xhr.status == 500) {
			alert('Oops! There seems to be a server problem, please try again later.');
		} else {
			alert('Oops! There was a problem, sorry.');
		}
	}
});

We have added tryCount and retryLimit. These are going to store how many attempts we have made, and how many attempts we will make respectively. Making use of these:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	tryCount : 0,
	retryLimit : 3,
	success : function(json) {
		//do something
	},
	error : function(xhr, textStatus, errorThrown ) {
		if (textStatus == 'timeout') {
			this.tryCount++;
			if (this.tryCount <= this.retryLimit) {
				//try again
				$.ajax(this);
				return;
			}
			alert('We have tried ' + this.retryLimit + ' times and it is still not working. We give in. Sorry.');
			return;
		}
		if (xhr.status == 500) {
			alert('Oops! There seems to be a server problem, please try again later.');
		} else {
			alert('Oops! There was a problem, sorry.');
		}
	}
});

So, now we are trying three times before giving in. Where I have actually used this, some modal dialogue boxes are used instead of window.alert() and I save a copy of the AJAX object when we reach our retry limit. At that point, although I tell the user we have given in, I still provide them with a button to try again themselves.

I am convinced that implementing techniques like this will rid us of many unnecessary bad user experiences, and many support calls. Keep the user informed (in simple language!), and assume things will fail. I hope this is helpful.

Destructuring assignments in JavaScript 1.7

Destructuring assignments are great for time saving methods of assigning values to variables. They make for shorter code, and shorter code is less error prone.

A destructuring assignment essentially allows you to arrange variables in an arrayesque manner, and assign them an array. What this does is actually assign the items in the array to each variable in one go. This is much easier to explain with an example.

Say we have two variables:

var variable1 = 10, variable2 = 20;

Now, we want to assign them two new values. By putting those variables inside an array-like structure before the assignment operator, we can do this in one statement instead of two:

[variable1, variable2] = [30, 40];

Now, variable1 is 30, and variable2 is 40. We can use this to take all of the items of an array returned from a function and put them in individual variables:

var color1, color2, color3;
var fnGetColors = function() {
	var arrColors = ['red', 'green', 'blue'];
	return arrColours;
};
[color1, color2, color3] = fnGetColors();

Or to swap the values of two variables, which previously would have required a temporary variable to use for storage:

var strBlack = '#FFFFFF';
var strWhite = '#000000';
//Oops, wait - that's not right
[strBlack, strWhite] = [strWhite, strBlack];

Sadly, support for JavaScript 1.7 is patchy. Internet Explorer, even in version 8, still only supports JavaScript 1.5. This kind of code could be useful if you were writing extensions for Firefox though, or had control over your user base (perhaps in an Intranet). I include it here just for interest – one day we’ll be able to use it!

Further reading:
New in JavaScript 1.7 – MDC
JavaScript Versions on Wikipedia

Regular Expression to match UK residential telephone numbers

A regular expression to match UK residential telephone numbers. It understands the difference between 02 and 01 numbers. It will accept all common formats and internationally formatted numbers.

Examples of accepted numbers:

  • 02081234567
  • 0208 123 4567
  • 020 8123 4567
  • 0208 123-4567
  • +44 208 123 4567
  • +44 (0) 208 123 4567
  • 01234 567 890
  • +44 0 1234 567-890
  • 07712 123 456

Examples of numbers that will not be accepted:

  • 020812345678
  • 123456789
  • 07612 123 4567
  • +33 345 876 1298

This is my first submission to the excellent regexlib.com regular expression library – I’d appreciate if you could vote for it!

/^(((44))( )?|((+44))( )?|(+44)( )?|(44)( )?)?((0)|((0)))?( )?(((1[0-9]{3})|(7[1-9]{1}[0-9]{2})|(20)( )?[7-8]{1})( )?([0-9]{3}[ -]?[0-9]{3})|(2[0-9]{2}( )?[0-9]{3}[ -]?[0-9]{4}))$/

Let me know if I’ve missed anything!

Update: This has been amended as per comments below, to allow 020 as a prefix for London, 07624 (Isle of Man) and 074xx xxxxxx.

Centre div in window no matter what – Version 2

In a previous post I showed you how to center a div on screen regardless of if it was inside an iframe or not. This is the second version, which should work regardless of if the div is in the top level window, an iframe, or a regular frame.

/******************************************************************
	Name: center
	Description: Center a div on page, no matter what!
	Author: AK (www.zeroedandnoughted.com)
	Date: 22nd April 2009
	Version: 0.2
	Dependencies: jQuery
	Notes:
	******************************************************************/
	(function($) {
	    $.fn.center = function() {
	        return this.each(function() {
				var $this = $(this);
				var frameXOffset = 0, frameYOffset = 0, windowHeight = 0, windowWidth = 0;
				//Are we in a frame?
				if (top.document != window.document) {
					var frm = $('iframe',top.document.body);
					if (frm.length == 0) {
						//regular frame
						frm =  $('frame',top.document.body);
					}
					var i=frm.length;
					while (i--) {
						if (frm[i].contentDocument) {
							doc = frm[i].contentDocument;
						} else {
							doc = frm[i].contentWindow.document;
						}
						if (doc === document) {
							//located our frame!
							frameXOffset = $(frm[i]).offset().left;
							frameYOffset = $(frm[i]).offset().top;
							break;
						}
					};
					if (jQuery.browser.msie) {
						windowWidth = top.window.document.documentElement.clientWidth;
						windowHeight = top.window.document.documentElement.clientHeight;
					} else {
						windowWidth = top.window.innerWidth;
						windowHeight = top.window.innerHeight;
					}
				} else {
					//we are not in a frame
					if (jQuery.browser.msie) {
						windowWidth = window.document.documentElement.clientWidth;
						windowHeight = window.document.documentElement.clientHeight;
					} else {
						windowWidth = window.innerWidth;
						windowHeight = window.innerHeight;
					}
				}
				var elHeight = $this.height();
				var newTop = ((windowHeight/2) - (elHeight/2)) - frameYOffset + $(parent.document.documentElement).scrollTop();
				if ((newTop + elHeight) &amp;amp;amp;amp;gt; $(document).height()) {
					newTop = $(document).height() - elHeight;
				}
				$this.css ({
					left: ((windowWidth/2) - ($this.width()/2)) - frameXOffset + $(parent.document.documentElement).scrollLeft(),
					top: newTop
				});
			});
		};
	})(jQuery);

Checking for Duplicates in a JavaScript Array

JavaScript has an in operator, that allows us to look through an object to see if we can find a property. If we create an object containing particular items in an array, we can then look through those items to see if we find a match.

Using prototypes, we can easily create a new method that will apply to all arrays. Let’s look at this in pieces.

The first thing we want to do is attach a method to the array prototype.

Array.prototype.containsDuplicates = function() {};

We can now do something like:

var dupesFound = [1,2,3].containsDuplicates();

…although this will won’t yet return anything, of course.

The next thing we’ll want to do, is loop through the contents of our array. For each item we will need to create an array with every item except our current one to check against (otherwise we will match against our current item, and always find a match).

In my previous post on iterating through arrays we already know the fastest way to iterate through an array (assuming we don’t mind doing it in reverse) so we’ll do that, and for each iteration create a new array containing all but the current item using the slice() and concat() methods.

var i=this.length;
var a;
while (i--) {
	a = this.slice(0,i).concat(this.slice(i+1,this.length));
}

Next, we’ll create an object and loop through our new array, creating an empty property of the object for each item in the array.

o = {};
j = a.length;
while (j--) {
	o[a[j]] = '';
}

Finally, we can now check this object for our current item, and return true if we found a match.

if (this[i] in o) {
	return true;
}

So, putting it all together (remembering to return false if we don’t find a match:

Array.prototype.containsDuplicates = function() {
	var i=this.length;
	var a, o, j;
	while (i--) {
		a = this.slice(0,i).concat(this.slice(i+1,this.length));
                o = {};
                j = a.length;
                while (j--) {
                    o[a[j]] = '';
                }
		if (this[i] in o) {
			return true;
		}
	}
	return false;
};

I generally don’t advocate modifying the base prototypes in JavaScript – it can cause all sorts of problems later on, so here’s the same methodology as a function.

var containsDuplicates = function(a) {
	var i=a.length;
	var a2, o, j;
	while (i--) {
		a2 = a.slice(0,i).concat(a.slice(i+1,a.length));
                o = {};
                j = a2.length;
                while (j--) {
                    o[a2[j]] = '';
                }
		if (a[i] in o) {
			return true;
		}
	}
	return false;
};

I hope this helps someone. Feel free to suggest ways to make this more efficient.

Using previously highlighted text in TextMate snippets

In a previous (and very verbose) post on TextMate snippets, I showed a snippet to easily and simply use a tab trigger to insert a try... catch statement. If you missed it feel free to catch up here. The idea was that you type in try, press tab and TextMate spits out:

try {

} catch(e) {

}

Now, the cursor is automatically positioned inside the try block, and the next time you press tab it moves to the catch block. Pressing tab again moves you outside the block. This is great, but not hugely useful in the real world. What would be more likely than writing an empty try... catch statement up-front would be to write some code, and choose to surround it in the try block. I promised to tell you how to do that, and it is much simpler than I thought.

The answer is the TextMate variable $TM_SELECTED_TEXT. It does exactly what it says it does, but you can’t use a tab trigger to achieve our goal here. Highlight some text, and start typing and of course, the text is overwritten. In this instance, a tab trigger will not do. You need to use a "Key Equivalent" (or hotkey or shortcut key to the rest of us). In my instance, I have more than enough of those to remember already, thankyouverymuch, and so I highlight the text and do one of the following:

  • Click the Bundle menu item at the top of the screen and drill down to my snippet
  • Press Ctrl+Escape and drill down to my snippet
  • Or, my favourite, press Ctrl+Command+T to bring up a list of all the snippets, type "try" to filter the list, and press return

There is nothing to stop you just typing try and pressing return either.

Here is the final snippet:

Name:
try ... catch
Tab trigger:
try
Snippet:
try {
	$TM_SELECTED_TEXT$1
} catch(e) {
	$2
}


try... catch TextMate Bundle Editor Example

Note that there should be a trailing extra carriage return, to ensure the final tab takes us out on to the line following the block.

Six Ways to Iterate Through an Array (or Chrome’s V8 is a Monster)

There are actually all kinds of ways to iterate through an array in JavaScript. Here, I will concentrate on six of them, describe their advantages, and do performance testing to actually prove which of them is the faster.

Loop 1 – the native for loop

for (var i=0; i < arrTestData.length; i++) {
	/* do stuff */
};

We’ve all seen this a million times. It is the easiest to read, and the most common. It is also the least efficient.

Loop 2 – the native for loop improved

for (var i=0,j=arrTestData.length; i<j; i++) {
	/* do stuff */
};

The improvement here, is that a second variable j is employed, to stop us having to query the length property of the array with every iteration. Getting the length can be a costly process and it makes more sense to just do this once. Where it is important that iterate incrementally, and in ascending order (i.e. 0,1,2,3,4,5) this is the fastest method.

Loop 3 – the native for loop in reverse

for (var i = arrTestData.length - 1; i >= 0; i--){
	/* do stuff */
};

The main advantage of this method is that only one variable is employed. It is slightly faster than the other for loops, but goes in descending order (i.e. 5,4,3,2,1,0) which may make it inappropriate for your needs.

Loop 4 – the native while loop

var i = 0;
while (i < arrTestData.length) {
	/* do stuff */
	i++;
};

This is essentially the same as Loop 1. We are still querying the length of the array with every iteration.

Loop 5 – the native while loop in reverse

var i = arrTestData.length - 1;
while (i > 0) {
	/* do stuff */
	i--;
}

This is essentially the same as Loop 2. We are storing the length of the array, to avoid querying with every iteration.

Loop 6 – the native while loop in reverse improved

var i=arrTestData.length;
while (i--) {
	/* do stuff */
}

This is the tidiest code, and the fastest. Since a zero integeric value is falsy, our condition is as simple as it could be, and only one variable is employed. I give credit to Richard Hubbard (get a blog already, Richard!) for showing me this.

Below, are the results of my testing.

OS Browser Test Result 1 Result 2 Result 3 Average
OSX 10.5.6 Firefox 3.0.5 Loop 1 46ms 47ms 46ms 46ms
Loop 2 31ms 32ms 31ms 31ms
Loop 3 34ms 33ms 33ms 33ms
Loop 4 46ms 45ms 46ms 45ms
Loop 5 32ms 32ms 32ms 32ms
Loop 6 29ms 29ms 29ms 29ms
OSX 10.5.6 Safari 3.2.1 Loop 1 44ms 45ms 43ms 44ms
Loop 2 33ms 32ms 34ms 33ms
Loop 3 31ms 33ms 31ms 31ms
Loop 4 45ms 45ms 45ms 45ms
Loop 5 33ms 33ms 33ms 33ms
Loop 6 29ms 29ms 29ms 29ms
OSX 10.5.6 Camino 1.6.4 Loop 1 106ms 93ms 91ms 96ms
Loop 2 67ms 64ms 61ms 64ms
Loop 3 61ms 64ms 61ms 62ms
Loop 4 91ms 90ms 90ms 90ms
Loop 5 61ms 64ms 67ms 64ms
Loop 6 60ms 60ms 60ms 60ms
OSX 10.5.6 Opera 9.61 Loop 1 34ms 32ms 31ms 32ms
Loop 2 23ms 24ms 29ms 25ms
Loop 3 24ms 23ms 23ms 23ms
Loop 4 32ms 31ms 32ms 31ms
Loop 5 22ms 26ms 23ms 23ms
Loop 6 26ms 25ms 25ms 25ms
Windows XP SP2 Chrome 1.0.154.36 Loop 1 2ms 3ms 2ms 2ms
Loop 2 2ms 2ms 2ms 2ms
Loop 3 2ms 2ms 2ms 2ms
Loop 4 3ms 3ms 3ms 3ms
Loop 5 2ms 2ms 2ms 2ms
Loop 6 2ms 2ms 2ms 2ms
Windows XP SP2 IE 7.0.5730.13 Loop 1 171ms 172ms 172ms 171ms
Loop 2 125ms 125ms 125ms 125ms
Loop 3 125ms 125ms 125ms 125ms
Loop 4 1172*ms 172ms 172ms 172ms
Loop 5 125ms 125ms 141ms 130ms
Loop 6 125ms 125ms 125ms 125ms
Windows XP SP2 FF 3.0.5 Loop 1 37ms 37ms 37ms 37ms
Loop 2 26ms 26ms 26ms 26ms
Loop 3 25ms 26ms 25ms 25ms
Loop 4 37ms 36ms 37ms 36ms
Loop 5 25ms 25ms 25ms 25ms
Loop 6 27ms 26ms 27ms 26ms
Windows XP SP2 Safari 3.2.1 Loop 1 53ms 52ms 53ms 52ms
Loop 2 40ms 39ms 40ms 39ms
Loop 3 39ms 40ms 40ms 39ms
Loop 4 55ms 55ms 54ms 54ms
Loop 5 41ms 41ms 40ms 40ms
Loop 6 37ms 37ms 37ms 37ms
Windows XP SP2 IE 6 (multiple IEs) Loop 1 156ms 172ms 156ms 161ms
Loop 2 109ms 125ms 109ms 114ms
Loop 3 110ms 125ms 125ms 120ms
Loop 4 156ms 1313*ms 156ms 541ms
Loop 5 125ms 109ms 125ms 119ms
Loop 6 125ms 110ms 125ms 120ms
Windows XP SP2 Opera 9.63 Loop 1 16ms 31ms 16ms 21ms
Loop 2 16ms 15ms 16ms 15ms
Loop 3 15ms 16ms 16ms 15ms
Loop 4 31ms 16ms 31ms 26ms
Loop 5 15ms 16ms 16ms 15ms
Loop 6 0ms 15ms 16ms 10ms

I did every test three times, and took the average as the result. Internet Explorer 6 and 7 both presented a confirm dialogue box, asking if I wanted to keep running the script, which paused execution of the code. This resulted in two vastly bloated results, which I have generously ignored. It didn’t help – IE is the obvious loser here in every instance, surprisingly showing very little improvement between IE6 and IE7.

Copying the data was a pain – although in OSX I could copy and paste from pretty much any source, including alert dialogue boxes. In Windows only Firefox would let me do this.

A shock in all of this, was just how fast Chrome performed, clocking in at 100,000 iterations per millisecond! I actually wrote different versions of the code just for Chrome, just to ensure it was actually doing something. That V8 engine they have in there is amazing.

The point of this, though, is not to compare browser speed. Of course, we can’t compare between OSX and Windows XP in this instance, the hardware was different. Also, this is just a very specific and small set of tests and cannot be considered a fair test of browser speed. Really, what we wanted to achieve was to see which method of iterating through an array is the most efficient.

To that end, here is a final table, showing the average result across browsers for each loop.

Test Average Result (ms)
Loop 1 73.6
Loop 2 52.6
Loop 3 52.7
Loop 4 73.1
Loop 5 53.6
Loop 6 51.4

The obvious conclusion is that although Loop 6 is the fastest, there is not that much in it – whereas Loop 1 and Loop 4 are dramatically slower than the others, demonstrating just how expensive the querying of the length property is.

Centre div in window, in iFrame, from iFrame, without an iFrame ID

Update: This code has been replaced.

Centring a div is not so hard. Centring a div on a window regardless of the windows scroll position is a little harder. Centring a div in the viewport in an iFrame that is full height (i.e. does not scroll) in a window that does scroll when that window is scrolled and you do not know the ID of the iFrame is a bit of a pain in the arse. So I record it here, in case it helps anyone. This function accepts a jQuery object as its only argument.

function centerIt($el) {
	var frm = $('iframe',top.document.body);
	var iframeXOffset = 0, iframeYOffset = 0, windowHeight = 0, windowWidth = 0;
	var i=frm.length;
	while (i--) {
		if (frm[i].contentDocument) {
			doc = frm[i].contentDocument;
		} else {
			doc = frm[i].contentWindow.document;
		}
		if (doc === document) {
			//located our iframe!
			iframeXOffset = $(frm[i]).offset().left;
			iframeYOffset = $(frm[i]).offset().top;
			break;
		}
	};
	if (jQuery.browser.msie) {
		windowWidth = top.window.document.documentElement.clientWidth;
		windowHeight = top.window.document.documentElement.clientHeight;
	} else {
		windowWidth = top.window.innerWidth;
		windowHeight = top.window.innerHeight;
	}
	var elHeight = $el.height();
	var newTop = ((windowHeight/2) - (elHeight/2)) - iframeYOffset + $(parent.document.documentElement).scrollTop();
	if ((newTop + elHeight) > $(document).height()) {
		newTop = $(document).height() - elHeight;
	}
	$el.css ({
		left: ((windowWidth/2) - ($el.width()/2)) - iframeXOffset + $(parent.document.documentElement).scrollLeft(),
		top: newTop
	});
}

Of course, this makes much more sense as a jQuery chainable plug-in:

/******************************************************************
Name: center
Description: Center a div on page, even in an iframe
Author: AK (www.zeroedandnoughted.com)
Date: 8th Jan 2009
Version: 0.1
Dependencies: jQuery
Notes:
******************************************************************/
(function($) {
    $.fn.center = function() {
        return this.each(function() {
			var $this = $(this)
			var frm = $('iframe',top.document.body);
			var iframeXOffset = 0, iframeYOffset = 0, windowHeight = 0, windowWidth = 0;
			var i=frm.length;
			while (i--) {
				if (frm[i].contentDocument) {
					doc = frm[i].contentDocument;
				} else {
					doc = frm[i].contentWindow.document;
				}
				if (doc === document) {
					//located our iframe!
					iframeXOffset = $(frm[i]).offset().left;
					iframeYOffset = $(frm[i]).offset().top;
					break;
				}
			};
			if (jQuery.browser.msie) {
				windowWidth = top.window.document.documentElement.clientWidth;
				windowHeight = top.window.document.documentElement.clientHeight;
			} else {
				windowWidth = top.window.innerWidth;
				windowHeight = top.window.innerHeight;
			}
			var elHeight = $this.height();
			var newTop = ((windowHeight/2) - (elHeight/2)) - iframeYOffset + $(parent.document.documentElement).scrollTop();
			if ((newTop + elHeight) > $(document).height()) {
				newTop = $(document).height() - elHeight;
			}
			$this.css ({
				left: ((windowWidth/2) - ($this.width()/2)) - iframeXOffset + $(parent.document.documentElement).scrollLeft(),
				top: newTop
			});
		});
	};
})(jQuery);

Yes, I’m British. But yes, I spelt it “center”. I hate inconsistency worse than bastardised spelling. Comments welcomed, as always.

Update: It was possible, if you resized the frame, to get the centred box to appear off-screen. This has been fixed.

Bookmarklet to de-cache CSS and images – Version 2

You may recall, in this previous post I showed you how to use a bookmarklet (typically a small piece of JavaScript you can save as a bookmark and run against a page) to get the latest versions of CSS files and images, and not use the cached files. The previous version had some limitations. Namely:

  • Background images set in CSS would not be refreshed
  • Frame and iframe contents would not be refreshed
  • In some instances it was one-hit only – i.e. you could only run it once, without having to manually refresh the page anyway

So, I would like to present to you – CacheBuster version 2. Or CacheBusterizer the second. Or MegaCacheKiller 2. You get the idea.

This time, I will step through the code and how it works.

Getting the current values of styles set via CSS can be a pain. Unless they were explicitly set via JavaScript, we can only fetch them using currentStyle or getComputedStyle, complete of course with their own browser idiosyncrasies. Therefore, the first thing we need is a function to do this for us in a cross-browser fashion. I have amended a version I found on Incoherent Babble.

function gcs(e,s) {
	if (typeof e.currentStyle != 'undefined') {
		return e.currentStyle[s];
	} else {
		return document.defaultView.getComputedStyle(e, null)[s];
	}
}

In this instance, e represents the element, and s is a string representing the style we are looking for. Remember that we are looking JavaScript termed styles, not CSS – so backgroundImage, rather than background-image.

Since I am going to loop through all of the frames in the page, as well as the main window itself, I have decided to join these together in a single array, rather than writing the code out twice.

var z =[];
z.push(self);
z.concat(self.frames);

To get the latest version of the files, I will just amend a querystring with the current time on the end of the filename.

var y = new Date().getTime();

And finally, we do the work:

for (k=0,l=z.length;k<l;k++) {
	x=z[k].document.getElementsByTagName('*');
	for (i=0,j=x.length;i<j;i++){
		bi = gcs(x[i],'backgroundImage');
		if (bi.indexOf(')') > -1){
			bi = bi.replace(/'/gi,'').replace(/"/gi,'');
			x[i].style.backgroundImage=bi.replace(')',(bi.indexOf('?')==-1?'?':'')+y+')');
		}
	}
	x=z[k].document.getElementsByTagName('link');
	for(i=0,j=x.length;i<j;i++){
		x[i].href+=(x[i].href.indexOf('?')==-1?'?':'')+y;
	}
	x=z[k].document.getElementsByTagName('img');
	for(i=0,j=x.length;i<j;i++){
		x[i].src+=(x[i].src.indexOf('?')==-1?'?':'')+y;
	}
}

This code:

  • Loops through all elements on the page, checks to see if they have a background image set and appends the time on the end of the URI if they do
  • Loops through all the link tags on a page for stylesheets, and appends the time on the end of the href of any it finds
  • Loops through all the img tags on a page, and appends the time on the end of the src of any it finds

Finally, we wrap the whole lot in a closure to make sure we don’t mess with the main window namespace, tidy up, and we get:

(function(){
	var gcs = function(e,s) {
	  if (typeof e.currentStyle != 'undefined')
	    { return e.currentStyle[s]; }
	  else
	    { return document.defaultView.getComputedStyle(e, null)[s]; }
	};
	var i,j,k,l,x,y,z,bi;
	z =[];
	z.push(self);
	z.concat(self.frames);
	y = new Date().getTime();
	for (k=0,l=z.length;k<l;k++) {
		x=z[k].document.getElementsByTagName('*');
		for (i=0,j=x.length;i<j;i++){
			bi = gcs(x[i],'backgroundImage');
			if (bi.indexOf(')') > -1){
				bi = bi.replace(/'/gi,'').replace(/"/gi,'');
				x[i].style.backgroundImage=bi.replace(')',(bi.indexOf('?')==-1?'?':'')+y+')');
			};
		};
		x=z[k].document.getElementsByTagName('link');
		for(i=0,j=x.length;i<j;i++){
			x[i].href+=(x[i].href.indexOf('?')==-1?'?':'')+y;
		};
		x=z[k].document.getElementsByTagName('img');
		for(i=0,j=x.length;i<j;i++){
			x[i].src+=(x[i].src.indexOf('?')==-1?'?':'')+y;
		};
	};
})();

The last thing to do, is stick it all on one line, and prefix javascript: and we’re done.

javascript:(function(){var gcs=function(e,s){if(typeof e.currentStyle!='undefined'){return e.currentStyle[s];}else{return document.defaultView.getComputedStyle(e,null)[s];}};var i,j,k,l,x,y,z,bi;z=[];z.push(self);z.concat(self.frames);y=new Date().getTime();for(k=0,l=z.length;k<l;k++){x=z[k].document.getElementsByTagName('*');for(i=0,j=x.length;i<j;i++){bi=gcs(x[i],'backgroundImage');if(bi.indexOf(')')>-1){bi=bi.replace(/'/gi,'').replace(/"/gi,'');x[i].style.backgroundImage=bi.replace(')',(bi.indexOf('?')==-1?'?':'')+y+')');}};x=z[k].document.getElementsByTagName('link');for(i=0,j=x.length;i<j;i++){x[i].href+=(x[i].href.indexOf('?')==-1?'?':'')+y;};x=z[k].document.getElementsByTagName('img');for(i=0,j=x.length;i<j;i++){x[i].src+=(x[i].src.indexOf('?')==-1?'?':'')+y;};};})();

Save this result as a bookmark, and any page you run the bookmark against will load all images and CSS anew, bypassing the cache! Phew.

The only remaining caveat I can think of: this won’t work for CSS included with the @import directive. Any ideas?

Comments, as always, are welcome.