Fixing the Back Button and Enabling Bookmarking for AJAX Apps

by Mike Stenhouse

published 13 June 2005

The problem

Everyone's favourite AJAX technology app is Google Maps. Google have done a stunning job... But when I came to try to bookmark a page and I had to hunt around for 'link to this page' over on the right hand side. Why have they broken such a basic function of the web? I use bookmarks A LOT and the extra effort bothered me. I got over it though, and life went on.

Then I came to flick through the drop down on Kottke.org (now removed) and I kept hitting the back button on my mouse by accident, taking me off the site. Really irritating. The most fundamental online behaviour - click then back, is broken.

I've not picked on these sites for any particular reason. They both happen to be great sites that I visit regularly enough to notice these flaws, which will be common to many AJAX-based applications.

After a chat with Jeremy Keith, Rich Rutter and Andy Budd about exactly this problem I decided to take a shot at fixing it.

Read on for the explanation or go straight to the demo to see it in action.

Standing on the shoulders of giants

I'm not the first person to tackle this type of problem. I've drawn inspiration and know-how from several places to get this up and running:

The original bookmark/back button fix, as used by Flash developers for a little while now:
www.robertpenner.com/experiments/backbutton/flashpage.html
I've not actually looked at how they implemented their solution but this is where I got the idea for replacing Robert Penner's frames with iframes:
dojotoolkit.org/intro_to_dojo_io.html#so-about-that-thorny-back-button
Rich Rutter's use of the hash for bookmarking:
www.clagnut.com/sandbox/slideshow.html#5
For this little experiment I've used Harry Fuecks' JPSpan
It's a fantastic framework that makes the methods you define in your server-side PHP classes available to your Javascript via XmlHttpRequest. It's the simplest way I've come across to get started with AJAX. I had the guts of my demo up and running in about 10 minutes!
I'm using Algorithm's Timer object:
www.codingforums.com/archive/index.php/t-10531.html
And Scott Andrew's cross-browser event handler:
www.scottandrew.com/weblog/articles/cbs-events

Setting things up

I created a PageLocator object to act as an interface to both real querystrings and my hash pseudo-querystrings. It's nothing complicated but it allows me to access both using the same methods...

function PageLocator(propertyToUse, dividingCharacter) {
  this.propertyToUse = propertyToUse;
  this.defaultQS = 1;
  this.dividingCharacter = dividingCharacter;
}
PageLocator.prototype.getLocation = function() {
  return eval(this.propertyToUse);
}
PageLocator.prototype.getHash = function() {
  var url = this.getLocation();
  if(url.indexOf(this.dividingCharacter)>-1) {
    var url_elements = url.split(this.dividingCharacter);
    return url_elements[url_elements.length-1];
  } else {
    return this.defaultQS;
  }
}
PageLocator.prototype.getHref = function() {
  var url = this.getLocation();
  var url_elements = url.split(this.dividingCharacter);
  return url_elements[0];
}
PageLocator.prototype.makeNewLocation = function(new_qs) {
  return this.getHref() + this.dividingCharacter + new_qs;
}

I also have a setContent function that simply takes whatever you pass it and inserts it into the content container div on the page. I'm using the innerHtml property because it's really not the point of this demo. If I was doing it properly I'd probably go for something a little cleverer.

function setContent(new_content) {
  if(!document.getElementById || !document.getElementsByTagName) return;
  var container = document.getElementById("content");
  container.innerHTML = new_content;
}

In the spirit of 'proper' scripting my demo will work with javascript turned off. The links on the page point to content.php, which loads the same content as the AJAX app, but server side.

It's all too easy

So, I'm trying to store the session state in the address bar to allow bookmarking. What can be changed in the URL that won't trigger a page reload? The hash portion. So what I need to do is add my AJAX application's parameters after a #.

There is an additional benefit to this approach: When you click on page anchors, these points are added to the browser's history object so that when you press the back button you're taken back to these points within the same page. That's important because items are being added to the history without leaving the current page.

To make use of this behaviour I wrote written a simple DOM script to change the links on my page into # anchors, with the # portion containing the argument that would have been passed to content.php (the server-side equivalent of this app). This effectively maps the real querystring to a hash pseudo querystring.

Now when the links are clicked, the address bar is changed but the page itself doesn't change. To sync the page content with the URL I've set a Javascript timer to poll the window.location.href property on a regular basis and use any changes to trigger an AJAX content-load action. This effectively de-couples the default browser functionality, so instead of changing the page when a link is pressed, this now happens whenever something in the address bar changes. This means that if we change the URL manually or, importantly, with a bookmark, the page's content is automatically changed to reflect the new url.

To my surprise, it was very simple to get this all set up and working in Firefox. I was even more surprised to see that it worked reasonably well in IE6 as well. Reasonably, but not completely. For some reason, IE wasn't adding my anchors to the history so when I hit back, I was taken off my page and the AJAX app reset its state.

function AjaxUrlFixer() {
  this.fixLinks();

  this.locator = new PageLocator("window.location.href", "#");
  this.timer = new Timer(this);
  this.checkWhetherChanged(0);
}
AjaxUrlFixer.prototype.fixLinks = function () {
  var links = document.getElementsByTagName("A");
  for(var i=0; i<links.length; i++) {
    var href = links[i].getAttribute("href");
    var hash = href.substr(href.indexOf("hash=")+5);
    links[i].setAttribute("href","#"+hash);
  }
}
AjaxUrlFixer.prototype.checkWhetherChanged = function(location){
  if(this.locator.getHash() != location) {
    doGetPage(this.locator.getHash());
  }
  this.timer.setTimeout("checkWhetherChanged", 200, this.locator.getHash());
}

Now you're just being difficult

So, IE won't add my modified anchors to the history object. There is a fix that's been in use by Flash developers for a little while that uses frames to trick the browser into thinking that it's loading new pages, and use that to trap and mimic the back button action within the Flash apps. See www.holler.co.uk for an example of this in action.

After reading through the Flash back button fix and remembering Dojo Toolkit's reference to their own back button fix I decided to give iframes a shot. The catch is that the iframe has to be present on the page before the DOM tree is complete so it has to be document.written out inline. It's not the cleanest solution because it means that I need to have a script block in the BODY, which I like to avoid, but as far as I know there's no way around that.

With the iframe in, the mechanism is roughly the same...

Instead of changing the links on the page to # anchors, they are modified to change the src attribute of the iframe, loading a page called mock-page.php with a querystring that contains the same argument as the hash did before. A timer polls the iframe for it's location and if it detects a change then a content load is triggered in the AJAX app, same as before.

This is complicated by the fact that the iframe's src property doesn't change when the back button is pressed. To get around this I've written a little function to sit within mock-page.php and report it's location when asked.

function getLocation() {
  return 'http://www.contentwithstyle.co.uk/index.php?';
}

When a change in mock-page.php's url is detected an AJAX content load is triggered and the parameters from it's querystring are duplicated into the url, to make sure that bookmarking will still work. It's sounds a bit convoluted but the code is quite straight forward. Except for one thing... IE wouldn't let me access the window.location immediately. I have no idea why... As a simple workaround, I've added a tiny 100ms delay to the firing of the script, which seems to sort it out.

function AjaxIframesFixer(iframeid) {
  this.iframeid = iframeid;
  if (document.getElementById('ajaxnav')) {
    this.fixLinks();

    this.locator = new PageLocator("document.frames['"+this.iframeid+"'].getLocation()", "?hash=");
    this.windowlocator = new PageLocator("window.location.href", "#");
    this.timer = new Timer(this);

    this.delayInit(); // required or IE doesn't fire
  }
}
AjaxIframesFixer.prototype.fixLinks = function (iframeid) {
  var links = document.getElementsByTagName("A");
  for(var i=0; i<links.length; i++) {
    var href = links[i].getAttribute("href");
    var hash = href.substr(href.indexOf("hash=")+5);
    links[i].setAttribute("href", "Javascript:document.getElementById('"+this.iframeid+"').setAttribute('src', 'mock-page.php?hash="+hash+"');");
  }
}
AjaxIframesFixer.prototype.delayInit = function(){
  this.timer.setTimeout("checkBookmark", 100, "");
}
AjaxIframesFixer.prototype.checkBookmark = function(){
  window.location = this.windowlocator.makeNewLocation(this.locator.getHash());
  this.checkWhetherChanged(0);
}
AjaxIframesFixer.prototype.checkWhetherChanged = function(location){
  if(this.locator.getHash() != location) {
    doGetPage(this.locator.getHash());
    window.location = this.windowlocator.makeNewLocation(this.locator.getHash());
  }
  this.timer.setTimeout("checkWhetherChanged", 200, this.locator.getHash());
}

It's not what you said it's how you said it

I hate branching scripts but I couldn't find a method that would work across all browsers. To make things as easy as possible I've made the fixes objects so they can be plugged or removed as easily as possible.

function FixBackAndBookmarking() {
  if(!document.getElementById || !document.getElementsByTagName) return;
  if(document.iframesfix) {
    fix = new AjaxIframesFixer('ajaxnav');
  } else {
    fix = new AjaxUrlFixer();
  }
}

Moving on

What I've produced here is a simple illustration of a method for storing the browser state in the URL bar, to allow bookmarking, replaced the default browser linking mechanism to use that url-stored state and mimic traditional web behaviour. For a real world application the querystring used is likely to be far more complicated... I've separated out my code into distinct objects to try and make customisation easier.

To see it in action, I have put up a demo page with everything hooked together.

This method could be applied to Flash as well. As far as I know most people are still using the full frames method documented by Robert Penner. Using some Javascript we can do away with that extra complexity to make going back and bookmarking a plug-and-play addition.

Reservations

As I was writing this it occurred to me that what I've been doing here is remarkably like the old Frames hacks from back in the day. Should we be trying so hard to duplicate traditional browser behaviour? If it's important enough to put in the effort to duplicate it then should we really be breaking it in the first place? It's a question that can only be answered on a project-by-project basis but it seems important enough to be worth asking...

Components

index.html
The important part, from the point of view of this article. Contains:
  • Timer object
  • addEvent fix
  • JPSpan AJAX functions
  • PageLocator object
  • AjaxIframesFixer object for IE
  • AjaxUrlFixer object for Firefox and others
test.php
Sets up JPSpan server side.
mock-page.php
Loaded into iframe. Has getLocation function that reports it's current URL.
pageholder.class.php
Simple class used to serve content.
Browser support
BrowserBookmarkingBack button
IE6/PCYesYes
IE5.5/PCYesYes
IE5/PCYesYes
IE5/MacNoNo
Firefox/PCYesYes
Firefox/MacYesYes
Safari1.2/MacYesNo
IE5/MacNoNo

If you want to have a play with this yourself, I've zipped up the whole lot ready for download.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值