Organizing Backbone.js Applications
- 3/6/2012
- ·
- #index
When it comes to organizing applications, Backbone.js doesn’t come with an exhaustive list of “thou shalts”. As near as I know, no definitive guidelines have emerged for putting the pieces together. But just because there isn’t a right way, doesn’t mean that there aren’t plenty of approaches available.
To these, add the rut I’ve been stuck in recently. As I’ve worked more and more with Backbone, I’ve settled into an organizational scheme that:
- Ensures consistency by recycling common themes, names, and functions
- Maximizes portability through modularization
- Improves sanity by simplifying maintenance and encouraging reuse in the codebase
Phwaw—what a load of buzzword hooey. Let’s get down to business (fiddle here).
Application Layout
At the top level, each applications tends to follow a pretty predictable, granular structure:
application/
|--modules/
| |--MyModule.js
| `--AnotherModule.js
`--application.js
No surprises there.
Building a module
Each of the project modules will generally define at least a model
, collection
, and view
s for both. Sometimes the collection isn’t needed; sometimes more than one model will find its way in. There are plenty of ways to write each of these components, but I typically bundle them up as follows:
// modules/MyModule.js
(function(module) {
// a model
module.model = Backbone.Model.extend({
// model options
});
// a collection of models
module.collection = Backbone.Model.extend({
model: this.model
});
// the view for a single model
module.modelView = Backbone.View.extend({
render: function() {
// do some rendering
return this;
},
template: _.template('...')
});
// the view for a collection of models
module.collectionView = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.views = [];
_.bindAll(this);
this.collection.bind('add', this.add);
this.collection.bind('remove', this.remove);
},
// create a view for the model and redraw the collection
add: function(model) {
var view = new module.modelView({ model: model, tagName: 'li' });
this.views.push(view);
this.render();
},
// remove the view for the model and redraw
remove: function(model) {
var view = _.find(this.views, function(view){ return view.model == model; });
this.views = _.without(this.views, view);
this.render();
},
// redraw the entire collection
render: function() {
var self = this;
this.$el.empty();
_.each(this.views, function(view) {
self.$el.append(view.render().el);
});
return this;
}
});
})(NS.module('MyModule'));
Aside from the collectionView
there isn’t much to say. Models and views vary significantly from one application to the next. Their implementation is an exercise left up to the reader. Collections, on the other hand, don’t take much imagination. Beyond adding handlers for add
and remove
events, I find that collections tend to change very little. That means that it’s possible to cover many common usage cases with a similarly unimaginative view. The collectionView
given here won’t be perfect for all applications. It’s a shim. Notice that render()
is being called whenever a collection changes? If a large collection is updated frequently, it will be much more efficient to save a few DOM insertions by append
ing and remove
-ing elements on demand. But for generic applications, this view is a pretty good place to start.
Accessing modules
When the anonymous function wrapping the module is called, its lone parameter is the return value of an as-of-yet-undefined method—NS.module
. This isn’t any more complicated than a pass-by-reference strategy for grabbing the module’s exports, but the operation is a bit subtle. Check out the method definition:
// in application.js
var NS = new function() {
var _modules = [];
// retrieve or initialize a module
this.module = function(key) {
if (!_modules[key]) {
_modules[key] = {};
}
return _modules[key];
}
};
The first time that NS.module
is called for a particular key
, it won’t find a corresponding module. Instead, it will tie a new object to the _modules
array and return it. That’s the module
argument that turns up in the module outlined above. On subsequent calls, the module’s exports will be returned instead. If that seems like massive overkill, consider this: by lazily defining the module, other pieces of the application can refer to a module before it’s been instantiated. It can’t use any of the modules methods, of course, but neither will it throw an error message. Handy? It is.
Now that NS.module
has been defined, each module’s exports will be available in other parts of the application:
// somewhere else
var myCollection = NS.module('MyModule').collection;
Conclusion
Backbone isn’t designed to enforce the rules, and different applications will require different approaches to application development. But as I’ve found myself repeating the same patterns over and over, it made sense to write them down. Got comments? Suggestions? Better ways to do things? All lines are open.