JavaScript Architecture: Backbone.js Routers

Updated Aug 11, 2012 to reflect current library versions.

In JavaScript Architecture: Backbone.js Views we discussed how to build dynamic apps that change views on the fly using JavaScript. Because view-switching is done without reloading the page or transferring control to a separate page, these are called single-page applications. Single-page applications pose a few issues we need to address:

  • When users hit their browser’s back button, they will be taken away from the app completely rather than back to a previous view within the app itself.
  • Users are only able to link to or bookmark the app itself — not a specific view within the app.
  • Deep views within the app may not be crawlable by search engines.

We want a great experience for our users. Successful apps behave as users would logically expect and users should feel like they can easily navigate back to where they were previously.

Like the topics we’ve addressed before, these issues aren’t specific to Backbone. It’s an issue that naturally arises in any single-page app. Fortunately, Backbone does a great job at addressing it and has a simple API.

Examples

This concept is most easily taught by example so let’s assume we’re building an app that sells shirts. We’ve built it out so we show a grid of shirt images and when the user clicks on a shirt, a panel slides out that covers half of the window and contains more details regarding that specific shirt. Without some extra work, if users are on this extra details view and click the back button in the browser, they will be taken out of the app completely rather than just hiding the extra details view. This is because, technically, the details view is not a new html page — it’s just another “view” within the same html page.

The browser has no concept of views that are changing within your app and therefore cannot automatically register history steps in this regard. As we discussed in JavaScript Architecture: Backbone.js Views, the definition and granularity of a view can become very blurry. For example, if we pop up a settings panel after a user clicks on a gear icon, does the settings panel merit a history step in the browser so that if the user clicks the back button the panel closes? As much as we would like browsers to figure this out automatically, it’s really a user experience judgement call on our part and we must tell the browser when to register a new history step. This gives us a lot of power.

For starters, let’s take a look at a simple example with Backbone:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Backbone Example</title>
</head>
<body>
<script src="js/libs/jquery.js"></script>
<script src="js/libs/underscore.js"></script>
<script src="js/libs/backbone.js"></script>
<script type="text/javascript">
$(function() {
var AppRouter = Backbone.Router.extend({
routes: {
"shirt/id/:id": "showShirt"
},

showShirt: function(id) {
alert('Show shirt with id ' + id + '.');
}
});

var appRouter = new AppRouter();
Backbone.history.start();
});
</script>

<a href="#shirt/id/5">Shirt with id of 5</a><br>
<a href="#shirt/id/10">Shirt with id of 10</a><br>
<a href="#shirt/id/15">Shirt with id of 15</a><br>
</body>
</html>

View Demo

You can see we have three links each linking to a shirt with a different id. Notice that the pound sign at the beginning of each link url (e.g., #shirt/id/5) is important as it tells the browser we’re neither moving to a new html page nor refreshing the current page. It’s formally known as a fragment identifier and has been a web standard for quite a while.

In our example, We’ve also set up a backbone router with “routes” which are essentially url patterns that are meaningful to our app. In this case, we have set up a single route, shirt/id/:id, with a handler of showShirt(). The :id portion is called a parameter part. If a user navigates to the url <current_page>/#shirt/id/123123, the fragment would qualify as a match and the id (123123) would be passed as a parameter to the showShirt() function. In our example, if the user clicks the first link we set up, the url will be changed to <current_page>/#shirt/id/5 and Backbone will subsequently call showShirt(5).

To have a little fun, let’s go nuts for donuts and get crazy up in here:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Backbone Example</title>
</head>
<body>
<script src="js/libs/jquery.js"></script>
<script src="js/libs/underscore.js"></script>
<script src="js/libs/backbone.js"></script>
<script type="text/javascript">
$(function() {
var AppRouter = Backbone.Router.extend({
routes: {
":product/:attribute/:value": "showProduct"
},

showProduct: function(product, attribute, value) {
alert('Show ' + product + ' where ' + attribute + ' = ' + value + '.');
}
});

var appRouter = new AppRouter();
Backbone.history.start();
});
</script>

<a href="#shoe/size/12">Size 12 shoes</a><br>
<a href="#shirt/id/5">Shirt with id of 15</a><br>
<a href="#hat/color/black">Black hats</a>
</body>
</html>

View Demo

This time we set up our route with three parameter parts. Because there are three parameter parts, three parameters will be passed into showProduct(). Easy enough.

Deep-linking

Now that we have our routes set up, we can deep-link to a specific view within our app. For example, a user might click on the size 12 shoes link which changes the url in their browser to http://code.aaronhardy.com/backbone-router-multi-param/#shirt/id/5. The user may want to share that shirt with a friend so they copy the link and send it in an email. When the user clicks on the link, the app loads, and backbone sees that the url already matches a route. showProduct() will then be automatically called which will immediately show the product details panel to the user. Go ahead, click on the link above and see the concept in action.

Showing requested content

In the above examples we’re just alerting information about the parameter parts. In a real app, the user would expect the app to switch to an appropriate view and load appropriate data. How you handle going from the showProduct() function to switching views has been left up to you. However, one option is, rather than calling a function within the router itself, views can watch the router for route events and update themselves accordingly:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Backbone Example</title>
</head>
<body>
<script src="js/libs/jquery.js"></script>
<script src="js/libs/underscore.js"></script>
<script src="js/libs/backbone.js"></script>
<script type="text/javascript">
$(function() {
var AppRouter = Backbone.Router.extend({
routes: {
":product/:attribute/:value": "showProduct"
}
});

var appRouter = new AppRouter();

var MyView = Backbone.View.extend({
initialize: function(options) {
options.router.on('route:showProduct', function(product, attribute, value) {
alert('Update to show ' + product + ' where ' + attribute + ' = ' + value + '.');
});
}
});

window.myView = new MyView({router: appRouter});

Backbone.history.start();
});
</script>

<a href="#shoe/size/12">Size 12 shoes</a><br>
<a href="#shirt/id/5">Shirt with id of 5</a><br>
<a href="#hat/color/black">Black hats</a>
</body>
</html>

View Demo

Another option is to share a backbone model amongst the router and views. The router can then set a property on the model and the views can then respond to change events coming from the model.

Fragment identifier vs. pushState

Using fragment identifiers (#) or hashbangs/shebangs (#!) in the manner described above or as used by Twitter (https://twitter.com/#!/Aaronius), Grooveshark (http://grooveshark.com/#!/artist/Gotye/49212), or GMail (https://mail.google.com/mail/ca/#label/isys) doesn’t come without controversy, controversy, and more controversy.

In the end, the answer to the controversy is to use the new HTML5 History (more commonly known as pushState) standard that allows us to change the browser url and manage history steps without using fragment identifiers or forcing a page refresh on the user. The biggest roadblock to this new standard is obtaining browser support, particularly Internet Explorer support. For browsers that don’t support HTML5 History, a fallback must be available which usually entails page refreshes as the user navigates through the app.

Once you’re ready to support pushState, you’ll be glad to know Backbone supports it on an opt-in basis. Read more about how to set up Backbone for pushState.

More Juiciness

Backbone’s router and history have even more options and features so I suggest you check out their respective documentation. Thanks for reading and, as always, feel free to post a comment.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.