19 November 2012

Backbone.js: harnessing the clutter

Last few months I've been doing a lot of client-side work using Backbone.js as my main building block. It sure helps a developer to remain sane after long hours in this quasi-civilized world of JavaScript and DOM. I've read its annotated source (probably more than ten times to this date), as well as many tips and tutorials. Although we collectively moved light-years ahead of former jQuery-spaghetti madness, I dare to say many things still have some rough edges. This post introduces some of my insights on development wtih Backbone.js.

First of all I have to admit that routers (formerly controllers) are scarcely used in my applications. That is because I didn't really need to write a single-page app yet. Usually my use case is static page enrichment with some progressive JavaScript widgets (or sets of multiple widgets). This doesn't mean Backbone is useless for my work, to the contrary.

Code arrangement and namespacing

Many Backbone.js examples and introductory tutorials declare building objects (models, views, etc) as a global scope variables:
This is really bad as no namespacing is applied at all. I believe any JavaScript developer could provide huge list of reasons why it's so wrong -- anyway, just don't do this. 
Some more advanced examples use a closure (like $() or $(document).ready() call) to encapsulate whole application -- Backbone objects and glue code as well. This is somewhat better because global namespace is not polluted with our own variables. Still, other issues arise. Going further we'd end up with the entire application written in a single huge JavaScript file. It's because we cannot split our work into modules in a simple way -- one closure hides all application objects and prevents any access from outside.
There is a simple and popularly recognized solution for this problem, an explicit namespace for our project:

A single empty object is defined in global scope as a namespace for our application objects. It's secured with  or clause to prevent shadowing existing object. Then follow specialized namespaces for views and models. Finally we define some view prototype and attach it to our new namespace hierarchy. This way we can split our code into separate "modules" for bootstrapping code, views, models, etc. Each module can use objects from other files and define its own prototypes.
Unfortunately bad things can still happen. Any external library (or even our own misbehaving code) is free to shadow or modify these namespaces. Even easier way to break our application is an accidental reordering of <script> tags in a HTML file. For example the views module could be loaded before the initialization code or models file -- we end up we a ReferenceError.
The aforementioned issues are well known in JavaScript world and pushed many people to create their own substitutes of module systems. I would recommend any JS developer to take a look at asynchronous module definition. Using Require.js (or some other AMD tool) we can organize our application like this:
This is just a small example. Make sure to read Require's documentation before you design your project. Here I introduced many new concepts. First of all a single JS file is used for one atomic, meaningful object (like a model or a view). Then every module is loaded with all its dependencies. Each dependent module is loaded asynchronously and evaluated once and only once. This approach also prevents leaking variables into other scopes as every module runs inside it's own closure. It seems we've tackled all our issues.

View placement and DOM modification

In case of laying out views frequently you can see code similar to my last example. Whole application is bound to <body> tag, then sub-views are rendered inside parent view's specific elements. Sometimes the first part is omitted and each view is just initialized with it's own el attribute being some DOM element:

I believe this approach is too static and not really scalable. Too great focus is put onto each view instead of  thinking of application layout as a whole. Suppose your layout changes dynamically, based on user interaction, and you have to introduce another view(s) between the ones already rendered on page...
I like to think about views as reusable components similar to the concept of widgets in native GUI libraries. Hence they should know how to render themselves and nothing more. Their placement should be the concern of higher level entity, for example some stripped-down layout view. This view would behave like a bag for widgets (think responsive design), where newly added views line up one after another, occupying space in direct neighborhood of the previously rendered view. Let's see:
Naturally these widgets (or sub-views) can be instantiated in your application glue code and only then passed to the layout view for placement on a page. You could as well split the layout into multiple regions (rows, columns, etc) and maintain multiple widget lists, each of them rendered into separate region.

Similar idea is implemented in a more robust and consistent fashion inside Backbone.Marionette library. I'm yet to play with this tool, though it looks very interesting. You should read the docs and consider using it for your app if it's view-heavy.

Conclusion

If you care about tidiness of source code and your sanity use some structuring library like Backbone.js to provide proper skeleton for your application. Sure, you'd have to invest some time upfront to get around but it will make your life easier in the long run, especially when the refactor time comes.
Use AMD (e.g. with Require.js) to produce cleaner code with automatically maintained dependencies. Some other side effects may include better performance -- through asynchronous loading and single evaluation of your modules -- and more streamlined development process due to smaller units of self-contained code. If you really can't use an AMD library at least try to introduce well-defined namespace hierarchy.
Design your views as independent components that can be easily put in place regardless of the page layout. Avoid anchoring all views to specific DOM elements. Try out Backbone.Marionette (or some other library) for more opinionated approach to application layout.

No comments:

Post a Comment