Tech Resources

Complex Single Page Application Architecture with Backbone.js

Over the past few months we’ve been developing the 2nd (and much improved) generation of the SOOMLA dashboard.  Though we use myriad front-side technologies, the jewel in the crown is Backbone.js.  Much has been written about Backbone, though still I find that most articles focus on HOW-TOs of models, views and events.  In this post, I’ll describe our dashboard’s Backbone architecture, with an attempt to zoom out a bit and understand the fundamentals of building complex, heavy single page applications (hereinafter SPA) with Backbone.  Disclaimer: this post is not about code, but about architecture concepts.  Therefore, the code examples won’t be detailed to the bits and bytes, but they are all documented sufficiently to understand the idea.  Here we go:

The (somewhat detailed) frontend stack

We use:

  • Require.js for dependency management and modular code.
  • Backbone.js – for basic application structure and separation of concerns.
  • Marionette.js – for composite application architecture and saving lots of Backbone boilerplate.
  • underscore.js for general object, array and function utilities.
  • undescore.string for extended string manipulation functionality.
  • Handlebars.js – for client side templating.
  • jQuery for DOM manipulation, AJAX, promises, and much more.
  • These jQuery plugins: Qtip, SlimScroll, Isotope, jQuery UI sortable and jQuery validations.
  • imagesloaded – for capturing and synchronizing image load events.
  • Less – for CSS pre-processing.
  • grunt.js – for build automation (tasks like pre-compilation, minifying and concatenating code).

The architecture of Complex Single Page Application

Divide & Conquer

The notion of designing a SPA where everything is actually in one page is a bit daunting, less to say too complicated.  The approach we took is reminiscent of our college algorithms class – if you have a problem that’s too big to solve on its own, break the problem into smaller problems and solve them.  In a single page backbone app this means creating separate modules for each “sub-app” and instantiating the “root app” which instantiates all sub-apps.  The use of separate modules for each sub app can be done with AMD modules, CommonJS modules, or IIFE – immediately invoked function expressions which create a scoped closure.  Since this approach mandates that each sub-app will not be aware of its counterpart sub-apps, it also enforces the use of events to communicate between sub-apps.  For example, in our SPA, each sub-app triggers a started event when it’s completed startup and a closed event when it’s done closing itself.  Whenever the user performs an action that switches between sub-apps, the switchApp event is fired and the root app, being the one listening to this event, orchestrates the stop start of the previously active new sub-app. More on events later.

Single Page Application Architecture Diagram
The stop code of each sub-app isn’t (not included in the code example) is responsible for releasing memory, i.e. unbinding from DOM applicative events, and releasing references to views that would otherwise become –Zombie views– 

Another layer of divide & conquer is the division of the page’s real estate into so-called regions.  For example: a navbar region, a sidebar region, and a workspace region.  Essentially, these are DOM elements designated to contain different content depending on which sub-app is currently active.  In the SOOMLA dashboard, the sidebar changes its role and contents from the “My games” page (games sub-app), where it serves the selection of different games, to the “Storefront Editor” page (storefronts sub-app), where it enables the user to choose from the different editable aspects of the store.

Routes as State Machines

URLs represent resources.  The resource (or nested resource) the user is currently viewing is also the state in which the app currently resides.  The URL http://bookworm.com/authors/10/books is not only an address, but also an instruction to the application to start the authors sub-app and show all books for the author with ID 10.  When the user interacts with the page in a way that navigates to a different author, say with ID 20, he she are actually telling the application “Hey you there – change your state from showing author 10 to showing author 20”. The sub-app’s job is to infer that it doesn’t need to close, but just switch to the resource of the new state, i.e. author 20.  The beauty of building your SPA with resource-specific URLs is that these resources become bookmarkable and linkable.  The down side is that you need to carefully plan your sub-app’s routes and realize that each route should (optionally) spawn a series of initialization sequences.  For example, the route “/authors/10/books” needs to dispatch a request for author 10’s books before rendering the data.
In a composite Backbone SPA divided into sub-apps, each sub-app registers its own routes, relevant only to that sub-app.  An undocumented and well kept secret of the Backbone Router is that you can instantiate multiple Router instances, and all the registered routes will be aggregated into the same underlying route mapping mechanism.  You only need to make sure that:

  1. You don’t repeat two identical routes in two different Router instances, and
  2. You instantiate all Router objects before calling Backbone.history.start()

Events = Separation of Concerns

Everything, and I mean everything (!!!) should be event driven in a single page application.  One step further, DOM events should be mapped to applicative events for separating the user’s interaction with the page and the application’s reactions to it.  This practice is paramount for true modularity and allows for separation between small jQuery-style effects and massive operations such as form submission and file uploads.  Here are two example that explain this:
Form submission:

  • Bad: A Backbone view that listens to the form “submit” event, collects values from the “First Name” and “Last Name” fields, and issues a post request to complete the submission.
  • Good: A backbone view that listens to the form submission event, generically collects data from the form DOM, and triggers the data in an event, thus notifying the parent component holding the view instance.

What we gain is a modular form component that can be reused in any form independent of form’s contents or how it’s actually submitted to the server.

File upload:

  • Bad: a Backbone view that listens to a file “drop” event and uploads the file via an AJAX request to a certain URL.
  • Good: a backbone view that listens to a file “drop” event, collects all data regarding the file(s), and triggers an event with that data available for listeners to process.

What we gain is a reusable drag-drop view, whose sole responsibility is handling a dropped file, and passing it on to a parent component.  The instantiator of the view can then listen to the event and do as it wishes with the file: file uploading, image insertion, text parsing, or anything else.

In the SOOMLA dashboard, views are responsible only for capturing DOM interaction.  In certain cases, a view is “granted” more responsibility like the ability to interact with other views, or issuing server requests.  This is mostly a matter of modularity and convention, but generally the key for deciding how much authority a view is given is to ask “will this view be the only one doing this?” .  If the answer is yes, let it be.  If the answer is no, then the special actions that view is performing should be pulled out of the view and the view becomes only but a messenger of user interaction.  In some cases, such as with collection views, you’re likely even better off bubbling events more than one level up the view hierarchy.  This is because a collection view is just another form of views, and we’re trying to force views to mind their own business, i.e render HTML and listen to user interaction.  The following diagram illustrates event bubbling from leaf views all the way up to the sub-app.

Event Flow in a Single Page Application

Backbone provides us with a generic event bus for adding event listeners and triggering events on all of it’s components.  It’s worthwhile to check out the Backbone annotated source code to see how all of its components – Model, View, Collection, Router – all have events mixed into them.

Declaration vs. Instantiation vs. Cleanup

When writing a large scale single page application, a good practice is to separate between modules that only declare (define) object prototypes and those that create instances of those prototypes.  This separation promises that modules whose business is only to define stuff are stateless, and requiring them multiple times has no side effect.  Modules that actually instantiate objects are those responsible for holding references to those objects and maintaining their state.  In our dashboard for example, all Backbone views are defined in a views.js module, and the .js module is responsible for instantiating them.

Cleanup is one of Backbone’s most common pitfalls.  Generally speaking, it is the developer’s responsibility to destruct his or her Backbone objects.  This is especially important for Backbone views.  I’m purposely stressing the word “destruct” even though it gives me the C++ shivers, because lots of Javascript devs don’t realize this.  Yes – Javascript is a memory managed language that garbage collects after you.  No – that doesn’t mean that it can guess what to clean up.  If a Backbone view binds event handlers to the model provided to it (a common idiom to say the least), destroying that model or losing direct reference to it WON’T garbage collect the model as long as the view is still listening to it.  Effectively, listening to events means holding a reference to the element that we’re listening to.  And so, in a SPA, when transitioning between the different states, you’re required to close views from the previous sub-app and construct new views for the new active sub-app.  This kind of view closing functionality with event cleanup has excellent support in the Marionette.js framework, but can be easily coded independently.

Graduating to a Higher Level Framework

After dabbling with Backbone for a while, you’ll probably find yourself writing tons of boilerplate code over and over again.  Your views will have tons of event handlers, you’ll have lots of functions whose sole purpose is to trigger events, and you’ll get nauseous from coding collection views that render item views over and over again.  This is a common symptom I’ve experienced myself and its remedy is to start using more advanced Backbone-centric frameworks that wrap Backbone and provide clever ways to manage views and reduce boilerplate.  Our choice was to use Marionette.js.  Marionette has served us very well and has greatly contributed to making our code modular and concise.  There are plenty of other Backbone wrapper frameworks out there – Chaplin, Layout Manager and Thorax to name a few – but we’ve found Marionette to be the best in terms of documentation and vibrant community.  The learning curve is a bit steep, but once you get the hang of it you’ll never go back to writing plain vanilla Backbone.

Extending Backbone

A time will come where even the most comprehensive framework or Backbone plugin won’t serve your exact needs.  This has happened for us especially when dealing with mixins and multiple inheritance.  For example, sometimes you find that a group of views or models all contain the same group of functions, and those functions are repeated across all of them.  In our case extracting them up into a common prototype was not an option because they extended different ancestors and that would have broken their prototype inheritance chain.  Another spoke in the wheel for us was that single attributes in views needed to be extended across several different view prototypes but not overridden.  For example, imagine ListView and ItemView prototypes that both have different events hashes, and both need to be extended with a common events: {“click a”: “doSomething”}.  Our solution was to add a static “mixin” function to the Backbone.View that receives an object and extends the view prototype, while selectively dealing with special properties such as events and the initialize function.  Courtesy goes to Kim Joar Bekkelund’s excellent article on Github: https://github.com/kjbekkelund/writings/blob/master/published/backbone-mixins.md

var BackboneViewExtensions = {

    mixin : function(from) {
        var to = this.prototype;

        // we add those methods which exists on `from` but not on `to` to the latter
        _.defaults(to, from);

        // ...and we do the same for events and triggers
        _.defaults(to.events, from.events);
        _.defaults(to.triggers, from.triggers);

        // we then extend `to`'s `initialize`
        BackboneExtensions.extendMethod(to, from, "initialize");
        
        // … and its `render`
        Utils.extendMethod(to, from, "render");
    },

    // Helper method to extend an already existing method
    extendMethod : function(to, from, methodName) {

        // if the method is defined on from ...
        if (!_.isUndefined(from[methodName])) {
            var old = to[methodName];

            // ... we create a new function on to
            to[methodName] = function() {

                // wherein we first call the method which exists on `to`
                var oldReturn = old.apply(this, arguments);

                // and then call the method on `from`
                from[methodName].apply(this, arguments);

                // and then return the expected result,
                // i.e. what the method on `to` returns
                return oldReturn;
            };
        }
    }
};
_.extend(Backbone.View.prototype, BackboneViewExtensions);

Final Notes

What I’ve tried to do here is to share some architectural experience gained in the course of the last few months.  Single page applications can be built in a multitude of ways, and the beauty of Backbone is that it gives you succinct structure but gets out of your way.  You get the building blocks, but planning the whole construction is up to you the developer.  For those interested in digging deeper, I highly recommend reading blog posts by Derick Bailey, creator of Marionette.js.

Join 7724 other smart people who get email updates for free!

We don't spam!

Unsubscribe any time

Feel free to share:

Related Posts

7 Comments

  1. […] The SOOMLA Blog: Complex Single Page Application Architecture with Backbone http://blog.soom.la/2013/10/complex-single-page-application.html?m=1 […]

    Reply

  2. test

    April 7, 2014 at 9:58 am

    test123

    Reply

  3. […] The SOOMLA Blog: Complex Single Page Application Architecture with Backbone http://blog.soom.la/2013/10/complex-single-page-application.html?m=1 […]

    Reply

  4. Vladimir

    October 30, 2014 at 11:49 am

    Hey! Thanks for the article!
    I have one question. What part of the app is responsible for triggering ‘switchApp’ events? And how does it determine whether it is needed or current module can take care of navigating to the desired state?

    Reply

    • Gur Dotan

      November 1, 2014 at 3:10 pm

      In our app it was triggered by the routers themselves. If I have two sub-apps, A & B, and each one has its own router, then when navigating from a route in A to a route in B, the route in B would trigger that event, which would eventually cause the closing of sub-app A and the starting of sub-app B. In fact, all routes in all of our routers had a pre-filter which would do this to make sure that the sub-app is started. The coordination of all this, I admit, is quite complicated, and there’s extensive use of Promises.

      Reply

      • Vladimir

        November 2, 2014 at 2:15 pm

        Thank you for the reply. Basically, I’ve come up with almost the same solution so it was nice to hear that you already use that approach.

        Reply

  5. […] 原文:Complex Single Page Application Architecture with Backbone ~ The SOOMLA Blog […]

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *