When building a large-scale Backbone.js Single-Page Application (SPA), it's easy to make a mess.
If you don't take pains to avoid some common pitfalls, you'll likely end up with a codebase that's buggy, poorly structured, and hard to maintain.
Don't do that. :-)
Here are a handful of tips I've used when working on large-scale Backbone applications. They're simple principles that I use as basic architectural rules, and they've served me well on the Insight team at NetApp and elsewhere.
There are other important principles, too, but these are the ones that I use the most when figuring out new parts of an application, talking to team members, or reviewing code.
If you remember just one thing...
The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application. -- Justin Meyer (via Addy Osmani)
If I could only remember one rule for my architecture, that would be it. Everything else comes from that starting point.
The rest of this post just lists each principle with a brief explanation. I'll follow up on a few of these items with separate blog posts.
1. Get the server-side REST API model right
This may seem odd because it's not about the client app per se, but it's a critical point: if you get the REST API right, your job on the client side will go smoothly. If you have a bad API, life will be miserable.
A bad API forces you to spend a lot of time writing client-side code to make up for the API's shortcomings.
Spend the time up front to get the REST API right. Take the time to refactor & clean it up when you find rough spots. You'll reap the rewards tenfold in your backbone app.
2. Decouple ruthlessly
Code coupling is enemy #1. You must fight it constantly. You must defeat it.
Keep your code loosely coupled. Unnecessary coupling between components will turn your code into an interdependent, brittle mess. Use events, a registry-type system, dependency injection, or some other layer of indirection to decouple components.
3. Aim for high cohesion
Loose coupling, high cohesion -- that's the goal.
Cohesion means that the data & functionality in a component should all be tightly related. It's similar to the Single Responsibility Principle: each object & method should do exactly one thing.
It's common to see people turning a single component into a hodgepodge of functionality. Instead, use the Extract Function & Extract Class refactoring patterns to create smaller components with high cohesion.
Achieving proper cohesion & coupling in your system provides the basic organization that allows you to make deliberate architectural choices. Refactor frequently, particularly early in the project, until you achieve the right balance for your system.
4. Make views as simple as possible
In an MVC-style client application, the view classes are the #1 place where people want to jam as much data & functionality as possible. This is an instance of the cohesion problem, above, but I'm calling it out as a separate issue because it's so common.
Typical "bad smells" for this item include views that end up acting as controllers for the model, make raw ajax calls to the server, or do data processing.
Push hard to get as much code as possible out of each view. This effort leads to two other principles listed below: use controllers (#5), and decompose complex UI into smaller pieces (#6).
5. Use controllers to manage views & models
As mentioned above, I try to ruthlessly keep my views focused on presentation, and models on their data. I use a separate class called a Controller to manage & coordinate between each view & its model(s).
The Controller is usually where I put the code that gets extracted from a view. Often that code involves manipulating the model, or signalling some other component in response to a user event. "Controller" can be a loaded term, and some people prefer "mediator" or something else. Fine. Whatever you call it, the controller has a key role in keeping the view & model code highly cohesive & loosely coupled.
6. Decompose complex UI into small, simple pieces
Sometimes I find a view that has too much going on, even after I've pushed non-presentation code out to its controller. I can see that the view lacks cohesion -- it does multiple jobs, even if they're all view-type jobs. What to do?
The answer is to split that single view up into multiple, smaller ones. Make each sub-view do its own, single job, and do it well.
But how do you sew the whole thing together again? Why, you just...
7. Use layouts
I use Marionette's LayoutView to give me a single high-level component whose sole job is to contain the subviews. The LayoutView is nice because it contains the UI that's the container for the subviews, and also manages the subview rendering & create/destroy lifecycle issues for me.
You can write your own LayoutView if you're not using Marionette, but you need something like it in your toolkit -- it's an essential piece of the puzzle in many real-world applications.
8. Use a message bus
A message bus provides components with a loosely-coupled way to communicate. Marionette uses the backbone.wreqr / backbone.radio mechanism as a system-wide event bus, and I've used it to good effect many times. I've also added a similar mechanism at the module & layer levels when needed.
Messaging is a good, proven architectural pattern that can be a great aid in the battle to reduce coupling. Use it.
9. Write unit tests
Writing tests has the obvious benefits of finding bugs early, and giving you an easy way to check that code is still running properly when you need to refactor it.
But writing good tests also helps you write better code. Low-quality code is hard to test. By refactoring so you can test more easily, you'll improve the quality of your code.
10. Use lodash or underscore
This is another item that may seem like an oddball, like the "get the REST API right" item.
But a large-scale backbone application has a significant amount of code, and reducing unnecessary code is a good thing.
I use lodash a lot, and it has greatly reduced the code I have to write for everyday coding tasks like data manipulation and iteration.
Other benefits include things like eliminating common bugs (if I use
_.each instead of
for loops, I'll never have off-by-one bugs in those loops); making my code more readable because I'm programming with higher-level functions; and exposing me to excellent examples of functional programming.
I've been in many code reviews where someone has written a lot of code that could be replaced by a few lodash functions.
Lodash gives you leverage. Learn it and use it.
There is a lot more to building a large-scale web app than these points, of course. There's nothing in this post about how to solve code design problems, what key pieces of the system to get in place first, or what trade-offs are OK & which will lead to disaster.
But these principles are a good foundation. Keep these points in mind as you build your own applications, and you'll be ahead of the curve.