Build Backbone Apps Using RequireJS
Backbone gives us a very powerful set of tools. It gives us models, views, and routes - all event driven, consistent and beautifully embrace underscore.js functionality. Being a library rather than a framework, Backbone doesn’t give us application structure. That “glue” we need too initialize the app and glue all the pieces together. Since coding is by far easier than explaining, I programmed a Todos app in order to demonstrate how this can be done with Asynchronous Module Definition (AMD) using RequireJS.
AMD
This is what the RequireJS guys say about AMD:
The AMD format comes from wanting a module format that was better than today’s “write a bunch of script tags with implicit dependencies that you have to manually order” and something that was easy to use directly in the browser. Something with good debugging characteristics that did not require server-specific tooling to get started. It grew out of Dojo’s real world experience with using XHR+eval and and wanting to avoid its weaknesses for the future. It is an improvement over the web’s current “globals and script tags” because:
- Uses the CommonJS practice of string IDs for dependencies. Clear declaration of dependencies and avoids the use of globals.
- IDs can be mapped to different paths. This allows swapping out implementation. This is great for creating mocks for unit testing. For the above code sample, the code just expects something that implements the jQuery API and behavior. It does not have to be jQuery.
- Encapsulates the module definition. Gives you the tools to avoid polluting the global namespace.
- Clear path to defining the module value. Either use “return value;” or the CommonJS “exports” idiom, which can be useful for circular dependencies.
It is an improvement over CommonJS modules because:
- It works better in the browser, it has the least amount of gotchas. Other approaches have problems with debugging, cross-domain/CDN usage, file:// usage and the need for server-specific tooling.
- Defines a way to include multiple modules in one file. In CommonJS terms, the term for this is a “transport format”, and that group has not agreed on a transport format.
- Allows setting a function as the return value. This is really useful for constructor functions. In CommonJS this is more awkward, always having to set a property on the exports object. Node supports module.exports = function () {}, but that is not part of a CommonJS spec.
In general, the AMD pattern allows us to keep a file for each module in our app (a module could be a model, a view or any encapsulated part of the application), manage dependencies, and maintain a well organized codebase.
If you are anything like me, you would think that while this is great for development, loading modules or assets over http in run time is extremely wasteful and slow. Luckily the guys at requireJS had that in mind, and came out with an optimization process, that alongside with almond.js combine and minify the whole codebase to one minified javascript file for deployment. We will cover that later.
Initializing the Application
We start our app with a very simple index.html file. The only important part of the file is this line:
<script src="require-2.0.5.js" data-main="app.js"></script>
It tells the browser to load RequireJS, and it tells requireJS where to find the bootstrap javascript file. As you can probably guess, app.js is responsible for initializing the application.
app.js
First we define paths to our external libraries, so in the code you require jquery
and not lib/jquery-1.8.0
etc. This way, if you want to update libraries versions, or do stuff like replacing jQuery with zepto, or underscore with lodash, all you need to do is modify the relevant line of code, in the ‘paths’ section.
Then we want to shim our dependencies. The first challenge is neither underscore or backbone are AMD compatible. Luckily, the requireJS guys thought about this and introduced a new way to ”shim” dependencies in the 2.0x release.
require.config({
baseUrl: '/js/',
paths: {
jquery: 'lib/jquery-1.8.0',
underscore: 'lib/underscore-1.3.3',
backbone: 'lib/backbone-0.9.2',
'backbone.localStorage': 'lib/backbone.localStorage'
},
shim: {
underscore: { exports: '_' },
backbone: { deps: ['underscore', 'jquery'], exports: 'Backbone' },
'backbone.localStorage': { deps: ['backbone'], exports: 'Backbone' }
}
})
Next, lets got to the main app:
require(['jquery', 'backbone', 'models/Todo', 'views/MasterView']
, function($, Backbone, Todo, MasterView) {
var Router = Backbone.Router.extend({
routes: { "": "main" },
main: function() {
var tasks = new Todo.Collection();
var view = new MasterView({collection: tasks});
tasks.fetch({ success: function(tasks){
$("#container").html(view.render().el).show();
}
});
}
});
This is pretty straight forward. We declare what are the direct dependencies for this module, and initialize the router.
Models and Views
As it seems, the files I am loading (other than libraries) are my one model and the master view.
/js/models/Todo.js
define(['underscore', 'backbone.localStorage'], function (_, Backbone) {
var Todo = Backbone.Model.extend({
defaults: { title: '', timestamp: 0, completed: false },
validate: function (attrs) {
if (_.isEmpty(attrs.title)) {
return 'Missing Title'
}
}
})
var Todos = Backbone.Collection.extend({
localStorage: new Backbone.LocalStorage(window.store || 'Todos'),
// for testing purposes
model: Todo,
completed: function () {
return this.where({ completed: true })
},
remaining: function () {
return this.where({ completed: false })
},
comparator: function (model) {
return model.get('timestamp')
}
})
return { Model: Todo, Collection: Todos }
})
One thing you should notice about AMD modules is the return signature. Inside the module we have a sandbox we can use for private methods and variables. the only parts that will be accessible are what we are returning. In this case we are returning an object literal that contains both a model and a collection “classes” representing todos. I found this pattern useful, since models and collections are tightly coupled by definition.
The same apply for views:
/js/views/MasterView.js
define([
'backbone',
'views/TaskList',
'views/NewTask',
'views/MarkAll',
'views/FooterView'
], function (Backbone, TaskList, NewTask, MarkAll, FooterView) {
var View = Backbone.View.extend({
className: 'masterView',
initialize: function () {
this.children = {
taskList: new TaskList({ collection: this.collection }),
newTask: new NewTask({ collection: this.collection }),
markAll: new MarkAll({ collection: this.collection }),
footerView: new FooterView({ collection: this.collection })
}
this.$el.hide()
this.$el.append(this.children.newTask.render().el)
this.$el.append(this.children.markAll.render().el)
this.$el.append(this.children.taskList.render().el)
this.$el.append(this.children.footerView.render().el)
},
render: function () {
this.$el.show()
return this
}
})
return View
})
This goes on an on. You can browse the code at GitHub to review the rest of it.
Optimization
Once we are done with our basic app, we have 17 http requests for javascript libraries or assets. The next thing we want to do is optimize and combine the codebase into one file. We will do it using r.js which is a part of the RequireJS release. The dependencies for that are node.js and npm (we will load the other dependancies using npm install
). We will create a build.js file with the following syntax:
;({
baseUrl: '.',
mainConfigFile: 'app.js',
name: 'app',
out: 'app.min.js',
logLevel: 0,
preserveLicenseComments: false
})
Or if we chose to use almond.js: (Note that our main app moved from main
to include
, and almond gets the main
slot)
;({
baseUrl: '.',
mainConfigFile: 'app.js',
name: '../node_modules/almond/almond',
include: 'app',
out: 'app.min.js',
logLevel: 0,
preserveLicenseComments: false
})
What we did here, is to tell the optimizer what is our app URL, what file contains require.config, in what module to start, and where to output it (and a couple of optional semantic configuration bits such as keeping copyright comments and the log level)
The benefit of using almond.js is it compiles all to one file and eliminates the need to asynchronously load anything. You get for pretty much free leaner code - because you don’t use require.js anymore, and less http request - because you load one file other than two. The tradeoff is it eliminates the option to have a hybrid app where you load some modules asynchronously (a good example is remote apis such as google maps, etc.)
There’s a nice build.js example on the r.js repository with all the details you can dream of.
The last thing we do is run the build command in our root folder:
npm install # to install dependencies
node_modules/.bin/r.js -o js/build.js
Now, we have an optimized file. The last thing we want to do is replace the script tag that loads RequireJS with:
<script src="require-2.0.5.js" data-main="app.min.js"></script>
<!-- Or if we used almond.js: -->
<script src="app.min.js"></script>
Conclusion
Using AMD to create a modular backbone app is easy. it helps to:
- Manage dependencies
- Keep the code organized
- Have separate files for each module
And with all that, when optimized does not impact performance.