Single Page Web Apps with Backbone.js
About a month ago, we decided that we needed to give the front-end of sendhub.com a face-lift. The site looked a bit dated, and it just didn’t feel as snappy as we wanted it to. All pages were rendered almost entirely on the server, and only a couple of places used Ajax to pull in data asynchronously. In parallel to this, we were also in the process of releasing our developer API which we knew badly needed hardening before we would be willing to open it up to the developer community.
Seeing an opportunity here, we decided to re-build the front end of our site using a single-page application model. The goal was to leverage the API as much as possible in the process to iron out the finer details. As it turns out, not only did this serve us well in our efforts to create what we think is a pretty good API V1, it also changed the way we go about creating great web applications for our users.
What follows is an explanation of the things we learned along the way. Hopefully it will help you to do the same.
REST + Backbone = Gravy
We built our app using Backbone.js. Backbone.js was built to play well with REST. The more that you follow proven REST design patterns in your API design, the less you have to do to get your backbone collections talking to your server. Specifically, you should aim to follow the common pattern of designing URI’s around nouns rather than verbs. Furthermore, try to leverage HTTP methods GET, POST, PUT, DELETE etc to perform their respective CRUD actions on those nouns. This ends up simplifying things quite a bit in model and collection objects. As an example, here is the entire collection object for SendHub messages:
SH.collections.MessagesCollection = Backbone.Collection.extend({
model: SH.models.Message,
url: SH.config.apiPrefix + "messages/"
});
When we wanted to write the code to send a new message out, all we had to do was create the object to be sent to the server, add it to the collection, and call save() that object.
this.model = new SH.models.Message();
this.model.on("sync", this.messageSent, this);
this.model.on("error", this.showErrorMessages, this);
var collection = new SH.collections.MessagesCollection();
collection.add([this.model]);
var msgData = {}; //... populate msgdata here...
this.model.save({...data goes here...});
That’s it! Backbone handles all of the request magic for us on save(). All we had to do was provide the url for the collection. Without an API that was designed around common REST patterns, there would be a lot more code here to make Backbone fit into the request/response life-cycle. Yes, for brevity, we did skip the code for the Message model. And to prove that we are not just collecting underpants and ending up with profit, here is the list of functions and the declaration for the Messages model…
SH.models.Message = Backbone.RelationalModel.extend({
validate: function(){ ... },
isScheduled: function(){ ... },
wasSentSameDayAsMessage: function(){ ... }
});
You’ll notice that these functions have nothing to do with saving to or reading things from the server, but rather, encapsulate business logic. This is also a good idea. Be sure to push as much business logic into your models as you can. You’ll find yourself with a much more testable and re-usable code-base in the end. The trick is understanding the difference between view logic, and business logic.
What we like about backbone is that we less often have to deal with nesting or chaining asynchronous callbacks in our code. Nesting callbacks, while sometimes necessary, can be dangerous. It will quickly turn your code-base into a mess of spaghetti code if abused. Instead, we merely listen for sync and error events on our models to respond to asynchronous events in our view code. No callbacks needed!
Client Side Template Rendering
One thing that was important for us at the beginning of our journey towards a single-page app was understanding the tools available for rendering things on the client side. We looked at Mustache, Handlebars and a couple of other rendering libraries. However, at the end of the day, we ended up just using Underscore’s template compiler. Because of Backbone’s dependency on underscore, it meant that there was one less library to load in the page. Even still, we didn’t find ourselves needing all of the features that the other template libraries provided. In fact, we would argue that a lot of logic in the template itself can be a bad thing. Its hard to test logic when its mixed into a template, and even harder to debug when things go wrong. For example, here is our generic template rendering function…
Backbone.View.prototype.renderTemplate = function() {
var getTemplate = function(id) {
return _.template($("#"+id).html());
};
var compiledTemplates = {};
var fn = function(id, context) {
if(!_(compiledTemplates).has(id)){
compiledTemplates[id] = getTemplate(id);
}
return compiledTemplates[id](context);
};
return fn;
}();
…where templates would be referenced by ID, using script tags to contain them….
<script type="text/html" id="template-messages-inbox-thread-row">
<div>
<span class="messagesRecipientsWrapper"></span>
<span class="messagesTime floatRight"><%= time_sent_readable %></span>
</div>
<div class="messagesBodyText"><%= text %></div>
</script>
to render this, template we merely call…
this.renderTemplate("template-messages-inbox-thread-row", {
“text”:”Hey There”,
“time_sent_readable”:”March 15th, 2012”
});
As far as loading templates into the page is concerned there are two methods that people generally employ. Some load all of their templates into script tags at the bottom of the page which makes up the shell of the single page app. Others dynamically load them in asynchronously. Being that this was our first time writing an app with client side rendering, we didn’t know which one would be better in the end. Our end result was that we actually did a little bit of both in different places to experiment and understand the pros and cons of each approach. Of course this also means it prevented us from realizing the full potential in each. However, in this instance we felt that learning was more important than being consistent.
Embedding templates within the page
Pros: This is a much easier approach because there is far less that you have to manage during the creation and destruction of a view. When it comes time to render a template, you can be 100% certain that it will be there ready for you when you need it. For small projects, this makes a lot of sense.
Cons: Although the jury is still out on this, we have concerns that this approach may not scale to very large sites. As an application becomes larger and more templates become required, its easy to see how the main page load may take more time and increases the amount of load on your servers.
Dynamic Loading
Pros: Dynamic loading seems to promise a much more scalability in terms of application size as it only requires the page to load just what it needs instead of everything up front (Loading bars remind us too much of Adobe Flash. Yuck. Oh and yes, we are JavaScript bigots. Thanks for asking). There is an additional benefit here in that if you use dynamic loading, the content no longer has to be hosted and delivered by your servers. Instead, this could all be pushed to a CDN when you deploy new code.
Cons: We found out quickly that this is not as easy as it seems. For example, if you are iterating over a list of items on a page, its possible that the template will be re-download more than once the first time through if you are not careful. This will slow down the page significantly. To do this correctly, you have to add much more logic into the application to make sure that the rendering always comes after all of the templates have loaded. This is easier said than done.
Staying DRY with Sub-views
When working with backbone views, you’ll quickly find out that some of your views need to have “sub-views”. We ended up calling these sub-views “widgets” to differentiate them from top-level views that represent pages.
A basic (and often recurring) example of this is when a view is meant to represent a collection, as opposed to a single model. Naturally, we built a widget to represent the model (ItemWidget), and a widget to represent the list (ListWidget). The ListWidget contains a list of ItemWidgets.
It’s often pretty clear how the ItemWidget should update itself: it listens to changes on the model and updates itself accordingly. However, with the ListWidget, things weren’t so clear. Obviously it boils down to listening to changes to the collection and reacting accordingly, but how should we react? The collection can be updated in several ways: models can be added, models can be removed, and then the whole collection could be reset. How do we deal with that?
After much fiddling around, we settled for the simplest and laziest solution: every time the collection changes, clear out our widgets list and recreate it. More specifically, we do this every time we render the ListWidget.
The general structure of render methods in ListWidget views looks somewhat like this:
render: function() {
// render the "layout" template for this list
this.renderLayout()
// clear out the current widget list and closes the views
// (see section below on zombies)
this.clearWidgets();
// create a list of instances of ItemWidgetClass,
// each of them bound to a model from the collection
this.setWidgets(this.collection, ItemWidgetClass)
// tell each widget to render itself
this.renderWidgets();
// attach widgets to some element in our layout
this.displayWidgetsIn(".container");
}
There are other situations where a view needs to have several sub-views. In each instance of such a situation, we found that this was the easiest way to manage it: just kill previous sub-views and create them again every time we render to top-level view.
Avoiding the Zombie Apocalypse
One big learning opportunity we had during the transition to the single-page application model was in understanding the importance of killing zombies. What’s a zombie you ask? Zombies have multiple meanings in software and Call of Duty. However in this case, we mean zombie objects that you can’t access directly, but live inside the application and take up memory resources. At worst, these zombies live on long after they should have died, responding to events that they no longer care about.
In backbone, Zombies happen when you forget to clean up event listeners after objects are removed from the page (usually they are view objects) . The best pattern we found online for avoiding zombies was at lostechies.com. Most notably, the author of the article, Derick Bailey, recommends the introduction of a “close()” method on the Backbone.View prototype. This close method is responsible for doing three things: Removing the object from the page (calling the backbone remove() function on the view) Calling the off/unbind method on the target object (in backbone unbind is an alias for off). Calling off() will prevent any event listeners bound to the view to be unbound. Calling an optional onClose() method on the object so that it may propagate the unbinding of events to sub-views or sub-objects.
If you read the section above on “Staying DRY with Sub-views” you should note that making good use of onClose() to clean up sub-views was critical to ensure that our program was working smoothly and free of bugs. The pattern that we followed was to balance all of our rendering functions with cleanup functions. If render() was ever likely to be called twice on the same object, we made sure to call our cleanup methods before moving on with the rendering function.
We should say that when we didn’t abide by proper cleanup patterns we ended up with some our toughest bugs to find and fix. For example, in one case we had events being re-bound exponentially as old objects would still rebind themselves after their contents had been removed from the page. These types of defects proved tricky to debug, especially when they resulted in infinite event loops. Careful code review, inspection and effective logging are the best remedy for these types of things.
Staying Loose with a Dispatch
So far, we’ve talked a lot about binding views to models and collections. Furthermore, we’ve talked about careful binding (and unbinding!) as the key to success when building a single page application in Backbone js. However, the one thing we have not talked a lot about yet is how to get one Backbone view talking to another without tightly coupling them to each other.
Another pattern to the rescue!
For this, we created a global dispatch object that views can use to trigger and listen for events.
SH.dispatch = _.extend({}, Backbone.Events);
A good example for using the dispatch object was how views interact when we send messages out from the compose message form on Sendhub.com. When a user composes a message and submits the form (ComposeMessageFormWidget), we render the new message in the ThreadView, which is a different view object. The ThreadView object listens for new messages to be sent during render…
render: function(){
...
SH.dispatch.on("message:sent", this.addThreadMessage, this);
...
},
This means that when it views the “mesage:sent” event, this.addThreadMessage will be called and that function will be bound to “this”. Note that the thread view has no idea “what” will be sending the message. To trigger this, the ComposeMessageFormWidget triggers the event on the dispatch and passes the model representing the message that was sent along for the ride.
… // within our sendMessage() function this.dispatch.trigger("message:sent", this.model); …
In short, we’ve found that When two views need to talk to each other, this is an effective way to keep them self contained.
Conclusions
- Using Backbone.js was a huge win, especially when hitting RESTful backend services.
- Do minimal rendering on the server side, leave that to the clients. This not only reduces load on your app servers, but it also decouples the entire architecture between services and the application.
- Event Binding will be the death of you if you don’t clean up after yourself and understand the scope of what you are binding and un-binding.
If you have comments on how we are doing or want to add your thoughts, we’d love to hear!
21 Notes/ Hide
-
pixel67 reblogged this from sendhub
-
pixel67 likes this
-
kanzakikikuchi likes this
-
fevangelou likes this
-
y-web reblogged this from sendhub
-
boonpeel likes this
-
aainslie likes this
-
juev likes this
-
azcode reblogged this from sendhub
-
sudo-code likes this
-
anushshetty reblogged this from sendhub
-
shitsiread reblogged this from sendhub
-
chrisramakers likes this
-
waynesutton likes this
-
dustyprogrammer likes this
-
tim-mcnamara reblogged this from sendhub
-
seanoliver likes this
-
solarlion likes this
-
scoutzie likes this
-
zackgilbert likes this
-
lrvillarreal reblogged this from sendhub
-
sendhub posted this
