Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Building a Scalable App With Backbone.js

by

Backbone.js is a small library (~5kb minified) that allows you to build single page web applications. Unlike many of its peers, Backbone is not very opinionated about the way you use it. Aside from some basic concepts, the design of your application is left widely up to you.

This tutorial will offer some insight on one of the popular patterns that the community has started to embrace: the Backbone Boilerplate. We will use this boilerplate to create a simple library of books, which you could easily extend into a much more robust application.


A Quick Overview of Libraries

It's very flexible and incredibly lightweight.

Backbone.js is a JavaScript framework that allows us to easily create single page web applications. It's very flexible and incredibly lightweight, which is why it has become one of the most popular JavaScript frameworks available.

Require.js is a module loader (leveraging the AMD design pattern) that allows you to asynchronously load your JavaScript modules and their dependencies.

Underscore.js a library that provides a set of utility functions you would come to expect when working with a programming language. Among other things, it gives you the ability to iterate over collections, to test if code is a function, and has a templating language built-in.


What is Backbone Boilerplate?

The Backbone Boilerplate is simply a set of best practices and utilities for building Backbone web applications. It is not an additional library, but it merges together a few libraries to encourage some structure when creating Backbone projects.

The Backbone Boilerplate is not an additional library.

There are a couple of ways to install the Backbone Boilerplate. The easiest (and preferred) method is the grunt-bbb plugin. However, that requires the use of Node.js and NPM, which is out of the scope of this tutorial. We will be doing a manual install instead.

To get started, head on over to the Github repository and download a copy of the code (you should see the .zip icon near the top). The copy you are downloading has been modified from the original with a lot the example code removed. There are a bunch of very useful comments in the example code (from the original boilerplate) - feel free to read over them in your spare time.

That's it! We can get starting with creating our application.


Your First Module, the Book!

When you are working with the Backbone Boilerplate (or any project using AMD/Require.js), you will be grouping functionality into modules, and generally putting each module in its own file. This creates a "separation of concerns" and allows you (and anyone else who reads your code) to easily understand what the code should be doing.

To create your first module, simply put the following code into the file app/modules/book.js.

define([
  "namespace",
  "use!backbone"
],

function(namespace, Backbone) {

  var Book = namespace.module();

  // Router
  Book.Router = Backbone.Router.extend({
    routes: {
      "book/:p"   : "details"
    },

    details: function(hash){
      var view = new Book.Views.Details({model: Library.get(hash)});
      view.render(function(el){
        $("#main").html(el);
      });
    }
  });

  // Instantiate Router
  var router = new Book.Router();

  // Book Model
  Book.Model = Backbone.Model.extend({});

  // Book Collection
  Book.Collection = Backbone.Collection.extend({
    model: Book.Model
  });  

  // This will fetch the book template and render it.
  Book.Views.Details = Backbone.View.extend({
    template: "app/templates/books/details.html",

    render: function(done) {
      var view = this;

      // Fetch the template, render it to the View element and call done.
      namespace.fetchTemplate(this.template, function(tmpl) {
        view.el.innerHTML = tmpl(view.model.toJSON());

        if (_.isFunction(done)) {
          done(view.el);
        }
      });
    }
  });

  // This will fetch the book list template and render it.
  Book.Views.List = Backbone.View.extend({
    template: "app/templates/books/list.html",

    render: function(done){
      var view = this;

      namespace.fetchTemplate(this.template, function(tmpl){
        view.el.innerHTML = tmpl({books: view.collection.toJSON()});

        if (_.isFunction(done)){
          done(view.el);
        }
      });
    }
  });

  // Required, return the module for AMD compliance
  return Book;

});

This might look like a lot, but it is really quite simple. Let's break it down below:

The AMD Module Definition

define([
  "namespace",
  "use!backbone"
], function(namespace, Backbone){
  var Book = namespace.module();

  return Book;
});

This is the standard format for any AMD module definition. You are telling the module loader that this module needs access to your namespace and backbone, which are defined in app/config.js. Inside the callback function, you are registering your module, and returning it at the end (which follows AMD compliance).

The Module's Router

Book.Router = Backbone.Router.extend({});
var router = new Book.Router();

Whenever the browser is directed to a route in the routes hash, the associated function is called. This is usually where you instantiate the view and call its render function. We instantiate the router so Backbone knows to start picking up the associated routes.

The Module's Data

Book.Model = Backbone.Model.extend({});
Book.Collection = Backbone.Collection.extend({
  model: Book.Model
});

This is where your book data and business logic is defined. You will create new instances of your Book.Model to store each book and its attributes (title, author, etc). Book.Collection is associated with Book.Model, and it is how you represent your models as grouped entities. In other words, a library has many books, and a collection is a lot like a library.

These are pretty bare, but you can place any of your business logic in the objects that are passed to the extend methods. If, for instance, you wanted to create a function that would filter books from the collection based on the author, you would do something like the following:

Book.Collection = Backbone.Collection.extend({
  model: Book.Model,

  filterByAuthor: function(author){
    return this.filter(function(book){
      return book.get('author') === author;
    });
  }
});

"Underscore functions can be called directly on a Backbone collection."

This is leveraging the Underscore filter function, which (like most of the Underscore functions) can be called directly on the collection itself. Feel free to read the Backbone documentation for more information on what Underscore functions you can call on your collections.

The same idea applies to your models. You should ideally push all of your business logic to the model. This might be something like adding the ability for your users to set up a book as a 'favorite.' For now, you can remove the filterByAuthor method from your collection, as we won't be using that in this tutorial.

The Module's Views

Book.Views.Details = Backbone.View.extend({
  template: "app/templates/books/details.html",

  render: function(done) {
    var view = this;

    // Fetch the template, render it to the View element and call done.
    namespace.fetchTemplate(this.template, function(tmpl) {
      view.el.innerHTML = tmpl(view.model.toJSON());

      if (_.isFunction(done)) {
        done(view.el);
      }
    });
  }
});

Your module will contain multiple views. In our example, we have a list view and a details view. Each of these has its own template, and a render function which calls fetchTemplate (defined in namespace.js), sets the result to the views innerHTML, and calls the associated callback function (done). One thing to notice, the list view is passing a collection to its template function, while the details view is passing the model to its template function. In both cases, we are calling toJSON() on the parameter. This helps us ensure that we are simply dealing with data at the template level.


Templates, With Little to No Logic

In app/templates/books/list.html

<h1>Listing of Books</h1>

<ul>
  <% _.each(books, function(book){ %>
    <li><a href="book/<%= book.id %>"><%= book.title %></a></li>
  <% }); %>
</ul>

In app/templates/books/details.html

<h1><%= title %></h1>

<ul>
  <li><b>Author: </b><%= author %></li>
  <li><b>Year Published: </b><%= published %></li>
</ul>

<a href="/">Back to List</a>

Since we have a details view and a list view, we will need a template for each of them. In the list view, we will iterate over our collection and render a link to each book's details view. In our details view, we display individual pieces of data pertaining to the book that was clicked. We are able to use the properties directly because we are passing the data into the template function with their toJSON() methods, which converts standard models/collections to their JSON representations.

Notice the fact that we didn't have to call preventDefault() for any of the links that were on the page? That is because of the code at the bottom of app/main.js. We are saying any link on the page without data-bypass="true" will automatically invoke preventDefault(), using our Backbone routes instead of default link behaviour.


Bootstrapping Your Data and Setting Your Default Route

At the top of main.js, replace the code with the following:

require([
  "namespace",

  // Libs
  "jquery",
  "use!backbone",

  // Modules
  "modules/book"
],

function(namespace, $, Backbone, Book) {
  window.Library = new Book.Collection([
    { id: 1, title: "A Tale of Two Cities", author: "Charles Dickens", published: 1859 },
    { id: 2, title: "The Lord of the Rings", author: "J. R. R. Tolkien", published: 1954 },
    { id: 3, title: "The Hobbit", author: "J. R. R. Tolkien", published: 1937 },
    { id: 4, title: "And Then There Were None", author: "Agatha Christie", published: 1939 }
  ]);

  // Defining the application router, you can attach sub routers here.
  var Router = Backbone.Router.extend({
    routes: {
      "":   "index"
    },

    index: function(){
      var view = new Book.Views.List({collection: Library});
      view.render(function(el){
        $("#main").html(el);
      })
    }
  });

  // Everything after the Router stays the same
});

Typically, your server side component would pass data to your Backbone application through its API (you set these up in your Collections and Models). However, in this tutorial we are simply bootstrapping your views with a few static books, and creating the default route, which passes the Library to the Book list view as its collection.

The only other thing that has to change is the fact that you are passing your module into the module loader (notice the require instead of the define at the top of main.js). By doing this, you are telling your application to load the file and passing it into the callback function so you have access to all of the Book properties.


Wrapping Up

There are a number of other pieces to truly having a scalable web application.

You might be thinking that this looks very similar to every other Backbone tutorial you have read, so what makes this one different? Well, the key to this is the fact that all functionality relating to the Book is stored in one module, which is in its own file. Let's say you decided to start sharing your movie collection on this site as well. It would be as simple as creating app/modules/movie.js, the associated templates, and telling main.js to load modules/movie.

There are a number of other pieces to truly having a scalable web application, the biggest of which is a robust API on the server. However, if you remember to create a separation of concerns when dealing with different modules in your application, you will find it much easier to maintain, optimize, and grow your code without running into too many issues as a result of unruly spaghetti code.

Additional Backbone learning on Nettuts+.

Advertisement