Skip to content

vincentbriglia/chaplin

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

77 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Chaplin

A Sample Application Architecture Using Backbone.js

Introduction

Chaplin is an example architecture for JavaScript applications using the Backbone.js library. The code is derived from moviepilot.com, a large single-page application.

  • CoffeeScript class hierarchies as well as object composition
  • Module encapsulation and lazy-loading using RequireJS
  • Cross-module communication using the Mediator and Publish/Subscribe patterns
  • Controllers for managing individual UI views
  • Rails-style routes which map URLs to controller actions
  • An application view as dispatcher and view manager
  • Extended model, view and collection classes to avoid repetition and enforce conventions
  • Strict memory management and object disposal
  • A collection with additional manipulation methods for smarter change events
  • A collection view for easy and intelligent list rendering
  • Client-side authentication using service providers like Facebook, Google and Twitter

Modern Times

While developing web applications like moviepilot.com and salon.io, we felt the need for conventions on how to structure Backbone applications. While Backbone is fine at what it’s doing, it’s not a framework for single-page applications. Yet it’s often used for this purpose.

To promote the discussion on JavaScript applications, we decided to open-source and document our application architecture. Chaplin is mostly derived and generalized from the moviepilot.com codebase.

Chaplin does not intend to provide an all-purpose, ready-to-use library. It’s an example how a real-world application structure might look like. Consider it as a scaffold which needs to be adapted to the needs of a specific application.

This repository is a snapshot of our ongoing efforts. We’re evolving the structure of moviepilot.com over the time and we’re using Chaplin as a testbed and playground. Of course this architecture is not flawless. In fact there are several open issues so your feedback is appreciated!

For simplicity reasons, Chaplin consists of plain HTML, CSS and JavaScript. However, the JavaScripts are originally written in the CoffeeScript meta-language. Most .coffee files contain CoffeeScript “class” declarations. In this repository, you will find both the original CoffeeScripts and the compiled JavaScripts. So there is no need to compile the CoffeeScripts in order to start the demo.

If you would like to modify the CoffeeScripts, you can translate them JavaScript using the CoffeeScript compiler. After installing CoffeeScript, you might run this command:

coffee --bare --output js/ coffee/

The example application uses the following JavaScript libraries:

  • RequireJS for lazy-loading of scripts and dependency management,
  • jQuery for DOM scripting and Ajax,
  • Underscore as a data processing and functional helper,
  • Backbone for models, views, routing and history management,
  • Handlebars for view templates.

We’re using RequireJS to define AMD modules and load JavaScript files automatically. However, the mentioned core libraries are not loaded using RequireJS, they are loaded with normal script elements synchronously. You might want to wrap jQuery, Backbone, Underscore and Handlebars as RequireJS modules to get the full AMD experience.

In our real-world applications, we’re using several tools for compiling and packaging scripts, stylesheets and templates. On moviepilot.com, we’re using the Ruby on Rails 3.2 asset pipeline together with Node.js to compile CoffeeScripts, Sass/Compass stylesheets as well as Handlebars templates on the server. On salon.io, we’re using Brunch which is based on Node.js.

Since this example isn’t about building and deployment, it has no such dependencies. Nevertheless RequireJS allows to pre-build a package with the initial modules. You might build a bootstrap package by running this command in the /js directory:

r.js -o name=application out=built.js paths.text=vendor/require-text-1.0.6 baseUrl=.

While traditional site login using e-mail and password is still around, single sign-on gained popularity. The example application features a client-side OAuth 2.0 login with Facebook Connect. Facebook is just a sample service provider. On moviepilot.com, we’re also using the Google APIs Client Library. We have experimented with Twitter Anywhere which provides a client-side login but doesn’t support OAuth 2.0. (Moviepilot.com allows you to log in with Twitter, but it’s an old-school OAuth 1.0 server-side login.)

This example uses a Facebook application named “Chaplin Example App”. On login, you will be asked to grant access rights to this Facebook app. Of course this app will not post anything to Facebook on your behalf or publish/submit your personal data. You’re free to revoke access rights at any time. You might easily create your own Facebook App and change the app ID in facebook.coffee/facebook.js.

The Facebook login only works if the app runs on the (nonexistent) domain chaplin.moviepilot.com. To access the application, follow these steps:

  • Add a line like 127.0.0.1 chaplin.moviepilot.com to your hosts file.
  • Start a simple local HTTP server (like nginx for example), point the document root to the app folder.
  • Then you’re able to access http://chaplin.moviepilot.com/ in your browser and log in with Facebook.

After successful login, your Facebook likes are fetched from the Open Graph and displayed as a list. You might click a list entry to see more details.

Besides the likes browsers, there’s a second screen which displays some latest posts on the moviepilot.com Facebook page. This is just another controller in order to demonstrate the change between controllers with proper routing and cleanup.

The following chapters will discuss the core objects and classes of our application structure.

Machine

The root object of the JavaScript application is just called Application. In practise, you might choose a more meaningful name. Application is merely a bootstrapper which starts up three other core modules:

  • SessionController
  • ApplicationController
  • Router

In this sample application we’re using RequireJS to load JavaScript files on demand. While a script might load another object it depends upon or a class (constructor) it inherits from, it normally does not have access to the actual instances. Most objects are encapsulated and not publicly accessible.

Modules communicate and share data using the mediator. That’s just a simple object with some properties:

  • user– If there’s a logged-in user, the user object. null if not logged in.
  • router – The router which maps URLs to controllers.
  • subscribe, unsubscribe, publish – Methods for global Publish/Subscribe

Publish/Subscribe (Pub/Sub) is a versatile pattern to ensure loose coupling of application modules. To inform other modules that something happened, a module doesn’t send messages directly (i.e. calling methods of specific objects). Instead, it publishes a message to a central channel without having to know who is listening. Other application modules might subscribe to these messages and react upon them.

For simplicity, we borrow the functionality from the Backbone.Events mixin. The subscribe, unsubscribe and publish methods are simply aliases for trigger, bind and unbind of the Backbone.Events mixin.

For example, several modules are interested in the user login event and subscribe to the login message. In practice, they load mediator as a dependency and register a callback function for the login event:

mediator.subscribe 'login', @doSomething

A Publish/Subscribe message consists of a name and optional data. The SessionController is in charge for the user login, so it publishes a message with the identifer login and the user object as additional data:

# Publish a global login event
mediator.publish 'login', user

The second and all following arguments are passed as arguments to the handler functions.

The Router in this example application does not inherit from Backbone’s Router. In fact it’s an implementation of its own with several advantages over the standard Backbone Router.

In Backbone’s concept, there are models and views, but no controllers. Backbone’s router maps routes to its own methods, so it’s serves two purposes. Our Router is just a router, it maps URLs to separate controllers, in particular controller actions. Just like Backbone’s standard router, we’re using an instance of Backbone.History in the background.

Our Router does not have a route method, but a match method to create routes:

@match 'likes/:id', 'likes#show'

match works much like the Ruby on Rails counterpart since it creates a proper params hash. If a route matches, the corresponding Route object publishes a matchRoute event passing the route instance and the parameter hash.

Additional fixed parameters and parameter constraints may be specified in the match call:

@match 'likes/:id', 'likes#show', constraints: { id: /^\d+$/ }, params: { foo: 'bar' }

In our concept, a controller is the place where a model and associated views are instantiated. A controller is also in charge of model and view disposal once another controller takes over. Typically, a controller represents a screen of the application. There can be one current controller.

There may be plenty of controllers for specific replaceable application modules, but few persistent meta-controllers with special views.

ApplicationController

The application controller instantiates common models/collections and views which are active the whole time (header, sidebars, footer and so on). Most importantly, it instantiates the ApplicationView.

ApplicationView

Between the router and the controllers, there’s the ApplicationView as a dispatcher. It allows switching between user interface modules by starting up specific controllers. To show a specific module, an app-wide !startupController event is published:

mediator.publish '!startupController', 'controllerName', 'controllerAction', optionalParams

The ApplicationView handles the !startupController event. It creates a controller instance, calls the controller action and may perform a transition from the current to the new controller.

In addition, the ApplicationView changes the interface chrome on application-wide events like login and logout. It also handles the activation of application-internal links. That is, you just have to use a normal <a href="/foo"> element to link to another application module.

Specific Module Controllers

By convention, there is a controller for each application module. A controller may provide several action methods like index, show and so on. These actions are called by ApplicationView when a route matches. In addition to specific actions, a controller might provide a initialize method which is called before a specific action.

For example, this is the stripped-down LikesController:

define ['controllers/controller', 'models/likes', 'models/like', 'views/likes_view', »»
  'views/full_like_view'], (Controller, Likes, Like, LikesView, FullLikeView) ->

  'use strict'

  class LikesController extends Controller

    historyURL: (params) ->
      if params.id then "likes/#{params.id}" else ''

    # initialize method is empty here

    index: (params) ->
      @collection = new Likes()
      @view = new LikesView collection: @collection

    show: (params) ->
      @model = new Like { id: params.id }, { loadDetails: true }
      @view = new FullLikeView model: @model

A typical controller has one model or collection and one associated view. They should be stored in the model/collection and view instance properties so they are disposed automatically on controller disposal.

We extended the standard Backbone models and collections with some new methods. dispose is the destructor for cleaning up. Our Collection also has addAtomic for adding several items while fireing a reset event, and update for updating a collection while fireing several add/remove events instead of a single reset event.

Using these Model and Collection classes, we create a hierarchy of CoffeeScript classes. Many child classes override methods while calling super.

Models and collections are Publish/Subscribe event subscribers by using the Subscriber mixin. We do not register their methods directly as listeners using mediator.subscribe. Instead, we use subscribeEvent which records the subscription so it might be removed again on model/collection disposal. It’s crucial to remove all external references to model/collection methods to allow them to be garbage collected.

Our View class is a highly extended and adapted Backbone View. All views inherit from this class to avoid repetition.

Views may subscribe to Publish/Subscribe and model/collection events in a manner which allows proper disposal. They have a standard render method which renders a Handlebars template into the view’s root element (@el). The input data for the template is provided by getTemplateData. By default, this method just returns an object which delegates to the model attributes. Views might override the method to process the raw model data for the view.

There are several differences to standard Backbone views:

We don’t use the events hash and the delegateEvents method to register user input handlers, but our own delegate method. The declarative hash approach doesn’t work well for class hierarchies when several initialize methods register their own handlers.

We don’t use @model.bind() directly. We have @modelBind() which records the subscription so the tie can be cut automatically on view disposal. When using Backbone’s naked bind, you have to deregister the handler manually to clear the reference from the model to the view.

CollectionView

On moviepilot.com, CollectionView is one of the main workhorses. It’s responsible for displaying most of the collections. For every item in a collection, it instantiates a given item view and inserts it into the DOM. It reacts to collection change events (add, remove and reset) and provides basic filtering and caching of views.

The current CollectionView implementation is quite simple and could be improved in many ways, for example with regard to filtering, fallback content and loading indicators. Nevertheless the CollectionView is an essential piece. Rendering collections on the UI would be painful without it since Backbone does not provide a clean mechanism to render collections.

Following Backbone’s design that models/collection can fetch themselves from the server or other stores, most of our fetching code is in the model/collection. On moviepilot.com, the actual API calls are located in separate modules, but the whole processing and updating logic resides in the model/collection. Model/collections may fetch themselves on initialization without receiving a call to do so.

Likewise, our views are quite independent. Most of them render themselves on instantiation, on model attribute change or on global events. They might event destroy themselves independently.

As a consequence, our controllers are quite skinny. In most cases, they just instantiate the model/collection an its associated view passing the necessary data. Then the model/collection and the view will handle the rest themselves.

There’s no specific reason for this decision, it’s merely a convention on where to put fetching and rendering code – your mileage may vary.

Dance

For models and views, there are several wrapper methods for event handler registration. In contrast to the direct methods they will save memory because the handlers will be removed correctly once the model or view is disposed.

Global Publish/Subscribe Events

In models and views, there is a shortcut for subscribing to global events:

@subscribeEvent 'login', @doSomething

This method has the advantage of cancelling the subscription on model or view disposal.

The subscribeEvent method has a counterpart unsubscribeEvent. These mehods are defined in the Subscriber mixin, which also provides the unsubscribeAllEvents method.

Model Events

In views, we don’t use the standard @model.bind method to register a handler for a model event. We use a memory-saving wrapper named modelBind instead.

@modelBind 'add', @doSomething

In a model, it’s fine to use bind directly as long as the handler is a method of the model itself.

A view also provides modelUnbind and modelUnbindAll for deregistering. The latter is called automatically on view disposal.

@modelUnbind 'add', @doSomething

User Input Events

Most views handle user input by listening to DOM events. Backbone provides the events property to register event handlers declaratively. But this does not work nicely when views inherit from each other and a specific view needs to handle additional events. Therefore Backbone’s events object and delegateEvents aren’t used.

Our View class provides the delegate method as a shortcut for @$el.on. It has the same signature as the jQuery 1.7 on method. Some examples:

@delegate 'click', '.like-button', @like
@delegate 'click', '.close-button', @skip

delegate registers the handler at the topmost DOM element of the view (@el) and catches events from nested elements using event bubbling. You can specify an optional selector to target nested elements.

In addition, delegate automatically binds the handler to the view object, so @/this points to the view. This means delegate creates a wrapper function which acts as the handler. As a consequence, it’s currently not possible to unbind a specific handler. At the moment, we’re using @$el.off directly to unbind all handlers for an event type for a selector:

@$el.off 'click', '.like-button'
@$el.off 'click', '.close'

This isn’t the best solution but acceptable for now since this doesn’t occur frequently in our application.

One of the core concerns of this architecture is a proper memory management. It seems there isn’t a broad discussion about garbage collection in JavaScript applications, but in fact it’s an important topic. Backbone provides little out of the box so we implemented it by hand: Every controller, model, collection and view cleans up after itself.

Event handling creates references between objects. If a view listens for model changes, the model has a reference to a view method in its _callbacks list. View methods are often bound to the view instance using _.bind(), CoffeeScript’s => or such. When you register a change handler which is bound to the view, the view will remain in memory even if it was already detached from the DOM. The garbage collector can’t free their memory because of this reference.

Before a new controller takes over and the user interface changes, the dispose method of the current controller is invoked. The controller calls dispose on its models/collections and then removes references to them. On disposal, a model clears all its attributes and disposes all associated views. A view removes all DOM elements and unsubscribes from DOM or model/collection events. Models/collections and views unsubscribe from global Publish/Subscribe events.

This disposal process is quite complex and many objects needs a custom dispose method. But this is the least we could do. In Internet Explorer, moviepilot.com gets slow and memory consumption rises after several module changes despite all these efforts.

Most processes in a client-side JavaScript application run asynchronously. It is quite common that an applications is communicating with different external APIs. API bridges are established on demand and of course all API calls are asynchronous. Lazy-loading code and content is a key to perfomance. Therefore, handling asynchronous dependencies was one of the biggest challenges for us. We’re using the following techniques to handle dependencies, from bottom-level to top-level.

Backbone Events

Of course, model-view-binding, Backbone’s key feature, is still a building block in our structure. If you’re familiar with Backbone, you certainly know how to set up such a binding: A view can listen to model changes by subscribing to change event or other custom model events. In addition, collection and collection views are able to listen for events which occur on their items. This works because model events bubble up to the collection.

jQuery Deferreds

Many objects like models, collections and APIs have a loaded state. At the beginning, they aren’t ready to use. They have to wait for the user login or other asynchronous I/O. These objects are jQuery Deferreds. You can register load callbacks using the done method. They will be processed when the Deferred is resolved.

Deferreds are a versatile pattern which can be used on different levels in an application. Deferreds can also be chained using jQuery.when to create a super-Deferred which is resolved when all sub-Deferreds are resolved.

Wrapping Methods to Wait for a Deferred

On moviepilot.com, methods of several Deferreds are called everywhere throughout the application. It would not be feasible for every caller to check the resolved state and register a callback if necessary. Instead, these methods are wrapped so they can be called safely before the Deferred is resolved. In this case, the calls are automatically saved as done callbacks, from later on they are passed through immediately. Of course this wrapping is only possible for asynchronous methods which don’t have a return value but expect a callback function.

The helper method utils.deferMethods wraps methods so calls are postponed until a given Deferred object is resolved. The method is quite flexible and we’re using it in several situations.

Method Call Accumulators

On moviepilot.com, several pieces of information are loaded from external APIs like the Facebook OpenGraph. To reduce the number of HTTP requests, we use again functional magic to automatically accumulate API calls, send them in one batch and distribute the response to the callers. This is a valuable tool for loading additional data without interfering with more important rendering and HTTP communication.

This functionality can be found in utils.wrapAccumulators and utils.createAccumulator.

Publish/Subscribe

As mentioned above, Publish/Subscribe is a powerful pattern to promote loose coupling of application modules. Our implementation using Backbone.Events is totally simply but highly beneficial.

The Publish/Subscribe pattern is the most important glue in our application because it’s used for most of the cross-module interaction.

Ending

By releasing this code into the public, we’d like to start a community discussion on top-level application architecture. “Application” means everything above simple routing, individual models, views and their binding.

Backbone is an easy starting point, but provides only basic, low-level patterns. Especially, Backbone provides little to structure an actual application. For example, the famous “Todo list example” is not an application in the strict sense nor does it teach best practices how to structure Backbone code. In addition, we could not use many of Backbone’s features on moviepilot.com and were forced to re-implement others. For us, Backbone got usable by deriving, extending or even replacing its classes. To be fair, Backbone doesn’t intend to be an all-round framework so it wouldn’t be appropriate to blame Backbone for this deliberate limitations. Nonetheless, most Backbone use cases clearly need a sophisticated application architecture.

The Chaplin structure replaces the Backbone Router completely because it’s likely to become the kitchen sink of a Backbone application. Instead we’re using a routing approach similar to Rails. Our router looks like routes.rb in a Rails application. Our advice is to separate routing from the actual code which instantiates models and views. For this purpose, we chose to introduce controllers. Further, an application needs to separate the business logic from application state management and view management. For handling top-level UI changes, we introduced the ApplicationView.

Due to the asynchronous nature of JavaScript Web applications, much glue is necessary to handle dependencies. We can recommend the techniques we chose: Publish/Subscribe, Deferreds and additional functional magic to wrap, accumulate and defer methods calls.

Last but not least, you will need to think about memory management and garbage collection. We suggest to write proper destructors for every application component. To avoid repetition, you might want to create core classes which allow for automatic disposal.

Apparently, other projects experienced the same Backbone shortcomings and took a similar approach to build an application framework on top of Backbone. See for example:

About

A Sample Application Architecture Using Backbone.js

Resources

License

Stars

Watchers

Forks

Packages

No packages published