Backbone.js: Hacker's Guide

19 Jul 2012 | By Alex Young | Tags mvc tutorials backbone.js code-review

There’s no denying the popularity and impact that Backbone.js (License: MIT, GitHub: documentcloud / backbone) by Jeremy Ashkenas and DocumentCloud has made. Although the documentation and examples are excellent, I thought it would be interesting to review the code on a more technical level. Hopefully this will give readers a deeper understanding of Backbone, and as the MVC series progresses these code reviews should prove useful in accurately comparing the many competing frameworks.

Follow me on a guided tour through Backbone’s source to really learn how it works and what it provides.

Namespace and Conflict Management

Like most client-side projects, Backbone.js wraps everything in an immediately-invoked function expression:

(function(){
  // Backbone.js
}).call(this);

Several things happen during this configuration stage. A Backbone “namespace” is created, and multiple versions of Backbone on the same page are supported through the noConflict mode:

var root = this;
var previousBackbone = root.Backbone;

Backbone.noConflict = function() {
  root.Backbone = previousBackbone;
  return this;
};

Multiple versions of Backbone can be used on the same page by calling noConflict like this:

var Backbone19 = Backbone.noConflict();
// Backbone19 refers to the most recently loaded version,
// and `window.Backbone` will be restored to the previously
// loaded version

This initial configuration code also supports CommonJS modules so Backbone can be used in Node projects:

var Backbone;
if (typeof exports !== 'undefined') {
  Backbone = exports;
} else {
  Backbone = root.Backbone = {};
}

The existence of Underscore.js (also by DocumentCloud) and a jQuery-like library is checked as well.

Server Support

During configuration, Backbone sets a variable to denote if extended HTTP methods are supported by the server. Another setting controls if the server understands the correct MIME type for JSON:

Backbone.emulateHTTP = false;
Backbone.emulateJSON = false;

The Backbone.sync method that uses these values is actually an integral part of Backbone.js. A jQuery-like ajax method is assumed, so HTTP parameters are organised based on jQuery’s API. Searching through the code for calls to the sync method show it’s used whenever a model is saved, fetched, or deleted (destroyed).

What if jQuery’s ajax API isn’t appropriate for your project? Well, it seems like the sync method is the right place to override for changing how models are persisted, and this is confirmed by Backbone’s documentation:

The sync function may be overriden globally as Backbone.sync, or at a finer-grained level, by adding a sync function to a Backbone collection or to an individual model.

There’s no fancy plugin API for adding a persistence layer – simply override Backbone.sync with the same function signature:

Backbone.sync = function(method, model, options) {
};

The default methodMap is useful for working out what the method argument does:

var methodMap = {
  'create': 'POST',
  'update': 'PUT',
  'delete': 'DELETE',
  'read':   'GET'
};

Events

Backbone has a built-in module for handling events. It’s a simple object with the following methods:

  • on: function(events, callback, context) , aliased to bind
  • off: function(events, callback, context) {, aliased to unbind
  • trigger: function(events) {

Each of these methods returns this, so it’s a chainable object. The comments recommend using Underscore.js to add Backbone.Events to any object:

//     var object = {};
//     _.extend(object, Backbone.Events);
//     object.on('expand', function(){ alert('expanded'); });
//     object.trigger('expand');

This won’t overwrite the existing object, it appends the methods instead. That means it’s easy to add event support to other objects in your project.

Model

Backbone.Model is where things start to get serious. Models use a constructor function that sets up various internal properties for managing things like attributes and whether or not the model has been saved yet. Underscore.js is used to add the methods from Backbone.Events, and then the public model API is defined. This contains most of the frequently used Backbone methods.

Notice that Backbone.Model is actually quite transparent: there aren’t any private methods defined inside the constructor.

The set method supports two different signatures, making it easy to support a single attribute or multiple attributes:

// Handle both `"key", value` and `{key: value}` -style arguments.
if (_.isObject(key) || key == null) {
  attrs = key;
  options = value;
} else {
  attrs = {};
  attrs[key] = value;
}

The save method does something similar. Notice how the authors ensure an object is always set for options:

options || (options = {});

In terms of expressing the programmer’s intent, this seems better than options = options || {}.

The set method triggers validations and prevents the method from progressing if a validation fails:

if (!this._validate(attrs, options)) return false;

Next each attribute is iterated over. If the attribute has changed, according to Underscore’s isEqual method, then the change is recorded. Once the list of changes have been built, the change method is called.

The change method calls trigger for each change. This allows for changes to any attribute to be listened on specifically, allowing the UI to be updated appropriately. For example, let’s say I had a blogPost model instance:

blogPost.on('change:title', function() {
  // Update the HTML for the page title
});

blogPost.set('title', 'All Work and No Play Makes Blank a Blank Blank');

Other methods also trigger change events: unset, clear, and fetch. Since we don’t always care if these cause a change event, a silent option is supported that will be passed from these methods to set. It’s actually quite interesting how each of these methods is implemented by reusing set:

// Clear all attributes on the model, firing `"change"` unless you choose
// to silence it.
clear: function(options) {
  options = _.extend({}, options, {unset: true});
  return this.set(_.clone(this.attributes), options);
},

The fetch method will trigger a sync operation that will retrieve the latest values from the server (or suitable persistence layer if it’s been overridden).

The save method ensures only valid attributes and models are persisted, and calls set if required:

if (options.wait) {
  if (!this._validate(attrs, options)) return false;
  current = _.clone(this.attributes);
}

// Regular saves `set` attributes before persisting to the server.
var silentOptions = _.extend({}, options, {silent: true});
if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
  return false;
}

// Do not persist invalid models.
if (!attrs && !this.isValid()) return false;

The sync method is called to persist the changes to the server. isNew is used to determine if the model should be created or updated. The isNew state is determined by whether an id attribute exists or not. This could be easily overridden if a given persistence layer works a different way. Notice that Backbone internally references this attribute as this.id and doesn’t map it to the value set with idAttribute in isNew.

A parse placeholder method is called whenever models are fetched, or saved. There are examples of people using this to parse other data formats like XML.

Conclusion

After looking at the Backbone.js setup and model code, we’ve already learned quite a lot:

  • Any persistence scheme can be supported by overriding the sync method
  • Models are event-based
  • change events can drive the UI whenever models change
  • Models know when to create or update objects
  • Reusing Backbone’s models, events, and Underscore methods is useful for organising project architecture

Although the Backbone models don’t have a plugin layer, the authors have kept the design open and allowed for just the right hooks to support lots of HTTP services and data types outside the built-in RESTful JSON oriented design.

Backbone relies heavily on Underscore.js, which means applications built with it can build on both of these libraries to create (potentially) well-designed and reusable code.


blog comments powered by Disqus