In HTML5, the history object has been augmented with two new functions: pushState and replaceState. pushState adds a new entry to the browser back history and replaceState replaces the current entry in the browser history, both without reloading the browser. This is very powerful, and makes it extremely easy for ajax applications to use resource-oriented URL schemes. What's a resource-oriented URL and why is it so desirable to use them? I'm glad you asked!
In resource-oriented architecture (ROA), an application must expose one URI per resource, and operations on each resource are performed using standard HTTP operations (GET, POST, PUT, DELETE, etc). For the purpose of this post, I'm going to focus on GETs, which is what web browsers do when you type a URL into the address bar. When a browser performs a GET on a resource, content negotiation is performed (by looking at the request's accept header), and the server responds with the resource in the format that was requested. The browser expects an HTML response, but other consumers may want a different representation (RDF+XML, JSON, etc). This concept is extremely powerful, because it serves different types of consumers in the way they wish to be served via the exact same URL - the URL of the resource.
The trouble starts when you want to combine these resource-oriented URLs with ajax applications (i.e. web apps that enable transitions between different resources without reloading the page). This is a fairly common practice these days, with big players like Google, Facebook and Twitter all using an ajax approach to UIs. Ajax applications are desirable because it means you only have to load common parts of the UI once. How annoying would it be if your Facebook chat windows along the bottom had to disappear and reload everytime you navigated to some other page in Facebook? (Answer: very). It would be a jarring user experience and wasteful of bits over the wire, even with heavy caching and optimized performance. How can we have our cake and eat it too? Enter history.pushState.
Before, because we were unable to manipulate the URL's path without a page reload, we used the following hash-based URL design:
https://example.com/myapp#/resourceUnder the covers, we would register a servlet at /myapp/* that sends down an empty page with some JavaScript. The JavaScript would inspect the hash, figure out what needs to be displayed, fetch it, and then render it. There are a few problems with this approach:
- The servlet is unable to see the value of the hash because the browser does not send it with the request. This makes it impossible to do any pre-processing of the hash on the server-side (forces reliance on a second round trip to fetch the resource payload).
- The page is blank until the JavaScript parses the URL and loads the appropriate resource. Making users wait is bad.
- The URL is ugly.
- It's impossible to do content negotiation to send other resource representations, since the server can't see what resource you're trying to fetch.
https://example.com/myapp/resourceWe still register a servlet at /myapp/*, but this servlet can read the full path, so it can a) do content negotiation, and b) send more meat down with the initial HTML payload. Within the UI, links to other resources follow the same format (i.e. https://example.com/myapp/anotherresource), but we use a body click listener to intercept clicks to links that are within the servlet's namespace, and use history.pushState to update the URL instead of allowing the click to reload the page. Some JavaScript detects that the URL has changed, parses it, and then fetches the resource.
This adds some new considerations: a) how do I detect that the URL has changed? b) how do we gracefully degrade for browsers that don't support history.pushState (IE9-, FF3.6-)? This is where dojo.hash comes in.
A while back, I contributed a little utility to the Dojo Toolkit for manipulating hashes, called dojo.hash (with help from Bill Higgins and John Ryding). dojo.hash shims the HTML5 onhashchange event for browsers that don't support it, and provides convenience methods for getting and setting the hash value. Before history.pushState existed, we lived in this strange dystopia where all useful information that we needed for building up the UI was part of the hash, so we took dojo.hash() for granted and used it everywhere; for triggering transitions to other resources, for subscribing to hash changes, and for reading the current hash for determining context. Rather than reinvent the wheel and come up with new API for these things, I decided to add history.pushState support to dojo.hash.
First off, I had to come up with a convention for distinguishing between dojo.hash() setter calls that should invoke history.pushState versus those that should set the hash. This was easy. Anything that starts with a forward slash is intended to be part of the path, so use history.pushState (if the browser supports it). Otherwise, set the hash. Next, I had to augment the dojo.hash() getter to return the resource specific part of the path. Lastly, I had to define the concept of a context root, so that the getter reads and the setter sets only the part of the path that would have previously been part of the hash.
I had to implement a few other little nitpicky things: 1) the body click listener that listens for clicks on links that begin with the context root and call dojo.hash() instead, 2) a first-load redirect rule that strips off redundant hashes.
In the end:
- All of our URLs are fully qualified and resource-oriented.
- The servlet is able to see the full URL, so it can do content negotiation.
- The first load experience doesn't have to be a blank page anymore!
- Consumers continue to use the same API they always used to get and set hashes and it transparently handles pushState if it can.
- URLs are RESTful and pretty.
- Graceful degradation for older browsers.
Note: these dojo.hash changes have not been contributed to dojo. They're implemented as overrides in Jazz. Stay tuned.