Backbone Routers Are Configuration
This is the third post in my series on Backbone.js Make sure to catch up on the first two if you haven’t already.
In my last post I covered how using the Model View Presenter design pattern will help you write cleaner apps without cluttering your Routers with unnecessary application or presentation logic. In this post, I’m going to get into the guts of the Backbone Router object, demonstrate how I’ve used it, and hopefully dispel some misunderstandings about its use.
Routers are really just configuration files
When we look at other MVC-like frameworks (especially on the server side), they all use a router to control how incoming URLs get mapped to the appropriate controller action method. Backbone.js is no different in that respect.
Let’s consider a couple examples from Backbone’s server-side cousins. In ASP.NET MVC, you set up your route tables in Global.asax.cs
:
public class FooApplication : MvcApplication { protected void Application_Start() { RegisterRoutes(RouteTable.Routes); } public static void RegisterRoutes(RouteTable routes) { // In ASP.NET, you have to define routes explicitly via routing table // definitons. These work similarly to how Backbone does things. routes.MapRoute( "Foos", "foos/{action}/{id}", new { controller = "Foo", action = "Index", id = UrlParameter.Optional } ); routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } }
Or in Rails, your routes.rb
file probably looks something like this:
FooApp::Application.routes.draw do # In Rails, however, you can define a `resource` via a model symbol name and # the framework just knows how to map RESTful URLs to it. resources :foos # You can also define individual URLs and map them manually. match '/about' => 'pages#about' root :to => 'pages#home' end
When we consider these two popular frameworks and how they treat their respective routing systems, we can draw the same conclusion about both of them. Routers are configuration. Yes, they’re code files and not XML, Yaml, JSON or some other structured data file, but actual imperative code. However, any sane developer would never put any sort of business logic in these files, right? Right?
So if the established use pattern for routers in an MV* framework is to treat them as configuration files, then why treat Backbone Routers in any other way?
Not to sound all soap-boxy, but here’s how I’ve been setting up my Routers in Backbone (with comments to hopefully help explain the important bits).
// Here's our Router "class" definition. There are a few things we need to wire up // before it's ready for action... var Router = Backbone.Router.extend({ // `initialize` is a standard part of every Backbone construct. It behaves // kind of like a constructor that accepts an object called `options`. initialize: function (options) { // Since I often use a presenter, I will instantiate it here and delegate // all application logic to it. Note that we're passing the options object // strait through to the Presenter. this.presenter = new Presenter(options); }, // This is our route table. In this JSON object, we can define any URL we // want to handle on the left side of the ':', and on the right side, we // list the name of the method that will handle that URL. routes: { '': 'index', // Empty string will represent "home" 'about': 'about', 'foos': 'listFoos', 'foos/:id': 'showFoo', // Note the 'id' URL param used here '*options': 'notFound' // `*options` is a catchall route in Backbone }, // Here's our handler for our home page. So if our website's address is // `http://foo.com`, a route with an empty string would catch the root // URL of the site. index: function () { this.presenter.showHomePage(); }, // Handler for the `http://foos.com/foos` URL listFoos: function () { this.presenter.showFooList(); }, // Here's the handler for `http://foos.com/foos/:id`. Notice that the URL parameter // `id` is mapped below as a function argument. showFoo: function (id) { this.presenter.showFoo(id); }, // The `*options` catchall route is a well known value in Backbone's Routing // internals that represents any route that's not listed before it. It should to // be defined last if desired. We'll just have the presenter render a 404-style // error view. notFound: function () { this.presenter.showPageNotFound(); } }); // To actually use our router, we'll just "new it up" and pass in any options necessary. // Let's pretend we already have a model populated with data that we're passing to it. var router = new Router({ model: foos }); // Now that we have a router object loaded in memory, we can start recording URL // history. Backbone.history worries about watching the URL and calling the appropriate // functions listed in the router's `routes` object. Here we're going to use HTML5 // PushState, so that we can generate cleaner looking URLs rather than hash-based ones. Backbone.history.start({ pushState: true });
Hopefully that helps demystify Backbone’s routing system for those unfamiliar with how it works. When you really break it down to its most important bits, the Backbone.Router construct really is just fancy looking configuration. Think this’ll help you write cleaner code? Let me know in the comments.