An Experiment with Backbone.js
We are launching a new mobile web interface for Harvest today. It introduces a redesigned mobile time tracking interface and a new Team Status page, both of which are kept up to date regardless of updates being made on other devices. It is also technically implemented differently than most of the Harvest UI. We wanted to experiment with some cutting-edge technologies, and this is the result!
On the initial page load we serve a thin shell along with some bootstrapping data, then use JavaScript to draw user interface on the client side. Updates to the timesheet happen through background requests to the server using a REST-like API. Driving it all is Backbone.js, a framework that brings some order to client-side web-app development. It does this by bringing a few concepts to the browser: Models, Collections and Views.
Backbone Models
Models are the basic objects the application operates upon. In Harvest’s case, one such Model could be Project
. Usually there is a 1:1 mapping between the Backbone Models and backend Models (database tables), but this is not strictly necessary. For example, on the frontend we have a RecentProjectTask
Model used to ease project task selection that does not have a direct match in the database.
The best part about Backbone Models is actually related to another Backbone feature called Events and how they interact with Views. In a framework like Rails, a Model is a passive object that gets operated on by a controller or perhaps another Model. Backbone Models, on the other hand broadcast events that are processed by Views subscribing to them. There is a useful decoupling between action and reaction.
Backbone Collections
Collections are a set of Models basically corresponding to a set of rows in a backend database, though again this need not be so. They also have the same Event love that Models have.
Backbone Views
Views are responsible for presenting the user interface and responding to user events. For this reason Backbone Views, are more than just passive string generators. In fact, as a best practice, the HTML for the user interface should be stored in Templates instead. The choice of how Templates are implemented is not determined by Backbone. Unlike some opinionated frameworks, not much is set in stone by Backbone.
Beside being able to react on events, the next best thing about Views is the ability to nest them. This nesting drives the structure of the code starting from the UI mockup, for example:
This screen suggests at least three different kinds of Views. First, there is the top container responsible for the header, the navigation to other days, the opening of the menu, etc. Second, there is the View shown in pink that draws out all the entries. This View is tied to a Backbone Collection.
On the third level, there are small individual Views tied to each entry present on the screen. Some of these are outlined in blue in the above picture. Nesting occurs naturally, the top container View creates the next level of Views below. The containing view may also referrences to its child Views in instance variables, but this is usually no necessary. The a nested View takes on an active role and does not strictly need to be operated on by its parent.
For example, when you delete timesheet entry the corresponding View (outlined in blue above) will get notified via a destroy
event and will have the opportunity to remove itself accordingly. The deleted timesheet entry was part of a Collection that also gets notified, which in turn notifies the containing View, ultimately allowing a different Total Hours value to be displayed on screen. Control can flow entirely through events without needing instance variables.
It Is a Sour Cherry
The advantage of using a framework like Backbone is its ordered method for creating fluid interactions. Ajax made partial screen updates possible, but making this fluid is difficult if you’re rendering parts of the UI on the backend.
For example, on the current non-mobile Harvest Timesheet interface, when you start a timer the response is instant, even though the save operation may actually happen a few milliseconds later. The JavaScript will redraw the UI before the operation completes, especially when we know no reasons why the backend should return with an error in the future. Since everything related to the UI is in JavaScript, there is no longer a risk of redrawing differently on page load compared to redrawing on UI interactions.
It is not all that sweet, though. JavaScript, for all its expressiveness (and lately speed), is still no match for the wide body of helpers and libraries available on the backend, especially when than backend is as full-featured as Rails. If you want to textilize timesheet notes, it is easy on the backend but most likely it is a missing library or a buggy one on the frontend. Want to format timestamps in a friendly way like “9 minutes ago”, easy in Rails, but not implemented on the frontend.
Even worse: error handling on the browser is primitive at best. Whereas we capture a full stack trace along with headers and other information for later analysis upon a Ruby exception, there is nothing universally supported and as comprehensive in JavaScript. Rails functional tests are suddenly no longer enough and you need to look into tools like capybara-webkit to exercise your JavaScript programmatically.
If you compare the development speed of a Backbone-based UI to one driven entirely by Rails, then the latter will always win. However, we rarely create interfaces in pure Rails these days, so the choice is actually between using a framework like Backbone or writting ad-hoc JavaScript to do much of the same. Backbone is not a replacement for Rails; rather it brings order to your JavaScript code.