The Responsibilities Of The Various Pieces Of Backbone.js
In my last post on Backbone, I offered my opinion on why I don’t look at Backbone as an MVC framework. I left off with a statement about forgetting the MV* family for a moment, and focusing on what’s really important: how the pieces of Backbone fit together to help us create better JavaScript application. What this really comes down is responsibility. What are each of the pieces of Backbone really responsible for?
Backbone.History
The primary responsibility of the History object is managing the browser’s navigation history, which facilitates the forward and back buttons and more.
This is one of the little-known pieces of Backbone as it sits behind the scenes 99% of the time. Typically, the only time you see it come in to play is when you have a Router and you call the history object to kick off the browser’s history manipulation:
In truth, the History object does most of the work that you think a Router does. It facilitates the browser’s forward and back buttons by manipulating the browser’s history. It updates the URL’s route (either hash fragment or pushState) and it monitors changes in the URL’s route. It basically does everything that needs to be done with history, so that we can define and use one or more Routers in our application.
Backbone.Router And Routes
A router’s primary responsibility is to organize the route definitions and callback methods into logical groupings (typically based on related objects or related functionality). In reality, the Router organizes an app’s code for doing this but actually making the update to the route and paying attention to the route happen through the History object. Hence, the Router is used to organize our code and facilitate history manipulation and response, but not to directly manipulate or manage history.
A Router has a few secondary responsibilities, as well: 1) tokenizing the application’s state in to the browser’s URL, and 2) rehydrating an application based on a token found in the browser’s URL.
On a related not: A route is the tokenized representation of our application’s state.
Tokenizing State:
As our application progresses, it moves through various states. Some of those states can be represented by simple tokens. For example, viewing an email in GMail can be represented with a token like this:
This token is a bookmark-able state that the application transitioned into. When I hit refresh on my browser window, GMail will display the same email message to me that I saw when I clicked on the mail item originally.
When our Backbone applications reach various states, such as viewing an email, we use a router to update the browser’s URL. We do that through `router.navigate`:
Rehydrating From A Tokenized State:
A router responds to changes in the route and calls the least amount of code possible to put our application in to the state we need. We do this through the use of the router’s callback methods (or events):
But the router is not in control the application, the application’s state, or anything else. It does not control the views or models. A router simply takes the route and figures out which part of the application to call.
Think about a Rails or ASP.NET MVC router. Would you have the router in either of those generate some HTML and send it back to the browser? Not a chance! You would use a controller and view for that. In the same way, we shouldn’t use a backbone router to “control” our application. Rather, we should let the router figure out which route callback needs to be fired, and from there we should call some part of our application that can be in control of the application state.
Sometimes a route callback is a 1-liner that calls out to another objects. Sometimes it is 2 or 3 lines to find an item by id and then call out to another part of our app to get things running. But a router should never instantiate a view and manipulate the DOM directly with jQuery or placing the view in to the DOM. That’s the responsibility of the application and it’s state, and a route callback is only one possible entry point into the application’s state.
Backbone.Model And Backbone.Collection
Models and collections have one primary responsibility: maintaining application state. They do this by remaining in memory, with all of their associated data remaining in tact, until we change that data or de-reference the object allowing it to be cleaned up. They also do this by managing persistence with a back-end server, when needed.
In-Memory State
A stateful object is one that lives in memory with a set of specified data, even after we’re done calling methods on it. A backbone model and collection are typically stateful objects because we hold references to them in our app. This keeps them in memory and allows us to read and change the state of the object. Changing the state of an object may change the state of the overall application.
Persistent State
When the state changes, it sticks around in that state until we explicitly change it again. But the state of a model or collection in Backbone can be rather volatile. If a user leaves our application, closes their browser or hits the refresh button on the browser, the state of our application may be lost. If we need to maintain the application state between sessions of our app, we have to persist the state somewhere. We do this by calling various persistence methods on our models and collections:
Backbone.Sync
Backbone.Sync is responsible for the network communications of the persistence operations found in models and collections. It does this by delegating to jQuery’s $.ajax method, and by default will communicate using JSON data over a RESTful API on a server.
You typically don’t touch or notice Backbone.Sync because it’s hidden behind the model and collection APIs for persistence. When Backbone’s persistence mechanisms don’t fit your needs, though, Backbone.Sync is where you want to change things to match what your back-end server requires. There are many different implementations of Backbone.Sync, including some for non-RESTful server, for use with Socket.io, and much much more.
Backbone.View
The primary responsibility of a view is to coordinate interactions between the end user and the application’s services.
The interactions between the user and the application’s services are facilitated through many different means, including the use of jQuery or Zepto.js to handle DOM level events, calling in to models and collections directly through the user of `this.model` and `this.collection` respectively, and generating new HTML that will be displayed to the end-user in response to changes in the application state.
In spite of their importance and having their fingers in all the pies of Backbone, views are not in control. They respond to changes in the application state in order to render the right HTML at the right time. They also facilitate changing the state by calling other objects that can change the state, but only on behalf of the user who initiates a state change by interacting with the DOM. Views are effectively the middle-man of a Backbone app.
Of course views can be used in some very simple scenarios where they respond to, manipulate, and maintain the state of an application. But this should only be done when you’re using a Backbone view to help organize some jQuery code without building a full Backbone application.
Backbone.Events
The primary responsibility of Backbone.Events is to decouple the knowledge of state changes from the response to those state changes, through the use of the observer pattern.
Backbone.Events is the little powerhouse that facilitates nearly every aspect of a Backbone application. This is the one piece of Backbone that is found in every other piece of Backbone. Every time you call “bind” or “trigger” on any Backbone object, you’re using Backbone.Events.
Events let us decouple the various pieces of our app while still providing a unified way for all of those pieces to communicate. Aside from being used in every other piece of Backbone, we can also use Backbone.Events in our own objects. One of my personal favorite ways to do this is through the use of an event aggregator to decouple and facilitate communication between higher level application concerns.
Your Code
Understanding what each of these pieces is responsible for will help guide our decisions in how we use them. Can I put this jQuery selector and DOM manipulation directly in my model? Sure, I can. Should I? Heh – no. A clear separation between DOM manipulation and data manipulation is important. Separating the other concerns of the application are equally as important, of course.
Here’s the kicker, though. You’ll notice that none of what I’ve talked about in any of these responsibilities includes workflow, large scale structure, managing dependencies or any of a number of other subjects. There’s a lot that Backbone gives us, and it’s a great tool set to own. But beyond a specific set of tools that should be used to facilitate a specific set of responsibilities in our applications, you still need to know how to write good JavaScript. And you still need to write your own code to handle the rest of your applications’ needs.