Recently, I was asked to do some research on Backbone bundled with RequireJs in order to fulfill some client requirements. I spent a couple of days playing with it and I would now love to share my discoveries with you.
Ok, first things first.
We all know Backbone.js is a great client-side MV* JavaScript framework that helps us to have a better structure by providing View, Model, Collection and Router classes; it also provides a publish/subscribe mechanism allowing each of its objects to trigger and bind to events.
On the other hand, RequireJs, is an AMD script loader that asynchronously loads your javascripts to improve page load performance, while also providing you the ability to organize the javascripts into self-contained modules. Each module is enclosed in a define tag that lists the module’s file dependencies and keeps the global namespace free (like a closure) and, since none of the modules are global, you need to declare their dependencies and pass them on to it.
So… what does this mean for us? Well, this provides a solution for limiting global variables and dependency management, being much better than having many script tags in a single page encouraging us to decouple the javascript logic.
Considerations:
-
A background on BackboneJs.
-
Knowledge of Handlebars templating. I'll be using hbs plugin for my templates.
Also, I'll be using a basic rack server, because loading the plain index.html and rendering the template could cause cross-origin request.
config.ru
map ‘/' do
use Rack::Static, urls: [‘’],
root: File.expand_path('./'),
index: 'index.html'
run lambda {}
end
Let's get our hands dirty
Let’s create our index.html which will load our application in RequireJs. It could be as simple as:
index.html
<html>
<head>
<title>Simple Backbone-Requirejs</title>
<meta charset='utf-8'>
<script data-main='src/main' src='lib/require.js'></script>
</head>
<body>
<header>
<h1>Simple Backbone RequireJs</h1>
<nav></nav>
</header>
<main>
</main>
<footer>
<small>All right reserved</small>
</footer>
</body>
</html>
As you may have noticed, we are loading RequireJs and, using the main data attribute, we are telling it to handle main.js as the application to load. It also contains the configuration. So, we need to create the file inside the src directory.
src/main.js
require.config({
baseUrl: './src',
paths: {
jquery: '../lib/jquery',
underscore: '../lib/underscore',
backbone: '../lib/backbone',
Handlebars: '../lib/Handlebars',
hbs: '../lib/hbs'
},
shim: {
'underscore': {
exports: '_'
},
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
},
hbs: {
disableI18n: true
}
});
Let's analyze this configuration file:
-
With the paths options we are requiring those libraries to be loaded from the given path.
-
With RequireJs 2.1.0+, we have the shim option which lets us configure non-AMD scripts. It wraps the script inside a
define()
function. In other words, it lets us modularize those libraries. Just make sure your are not using shim with AMD scripts, because the export and init config will not be triggered. -
Also, we're configuring the hbs plugin to disable I18n.
-
At the end, we're requiring the application and calling its
initialize
function.
Let's take a deeper look at the application file:
src/application.js
define([
'jquery',
'underscore',
'backbone',
'routers/base_router'
],
function($, _, Backbone, BaseRouter){
var initialize = function(){
BaseRouter.initialize();
}
return { initialize: initialize };
});
Here, we can clearly see a module, it contains a define()
function, an array with the dependencies of the module and the proper module.
In this case, I'm just requiring my base router and calling its initialize
function. I'm doing it this way because, in another project, I decided to split the application into multiple routers, because it sounded like fun :p.
And the base_router would look like this:
src/routers/base_router.js
define([
'jquery',
'underscore',
'backbone',
'views/index_view'],
function($, _, Backbone, IndexView){
var BaseRouter = Backbone.Router.extend({
routes: {
'(/)' : 'index'
},
index: function() {
indexView = new IndexView({ el: $('main') });
indexView.render()
}
});
var initialize = function(){
var baseRouter = new BaseRouter();
Backbone.history.start()
};
return { initialize: initialize };
});
Again, we are defining our backbone router inside a define
function, this will be the same for all of the Backbone elements in our application; just note the dependencies we are requiring, for this one it is the index view.
src/views/base_view.js
define(['jquery', 'underscore', 'backbone'],
function($, _, Backbone){
var BaseView = Backbone.View.extend({
initialize: function(options){
this.options = options;
if (this.model) { this.listenTo(this.model, 'change', this.render); }
},
render: function(){
if (typeof(this.beforeRender) === 'function') { this.beforeRender(); }
this.$el.html(this.tmpl(this.data));
return this;
}
});
return BaseView;
});
src/views/index_view.js
define(['jquery', 'underscore', 'backbone', 'views/base_view', 'hbs!templates/index'],
function($, _, Backbone, BaseView, tmplIndex){
var IndexView = BaseView.extend({
tmpl: tmplIndex
});
return IndexView;
});
Ok, looking at those views, you could be asking, why do you have a base_view? what's the purpose of it? Well, it has been an easy way to inherit some basic functions and properties to its children.
As you can see, I have a basic binding to a model if it is defined, this helps me to just call in my router like this:
myModel = new MyModel();
myView = new MyView({ model: myModel });
myModel.fetch()
This way, it will be rendered until the call to the server has returned. We could put a loader and then, before rendering, we could remove it using some fancy effects providing a better user experience.
The base view also helps me define a method for rendering the template. In this case it is not needed but, when working with Rails and its structure, I usually do something like this:
template: function(params = {}){
JST['my/mega/path/to/templates' + this.template_path]( _.extend(params, this.helpers, this.base_helpers))
};
As you can see, I use eco/jst templates, but I have a very long path for the templates. By using the base view I could insert helpers defined on it or helpers defined on each view, helping me to define a property simply:
template_path: '/products/show' #Instead the whole template function.
Note, it also helps me to have a simpler index_view.
I could go on like this to exemplify the whole application, but everything would be pretty much the same:
-
Wrap your backbone class within a define() function.
-
Declare your dependencies as an array in the define() function.
-
Pass those dependencies to the function so you could make use of them.
So, here I wanted to share the configuration and the way I used it. Still, you could visit the repo at github and dive into the code.
I will be updating it with more features, meanwhile, you could contact me and we can discuss what we saw here.
Conclusion:
If you’re a Rails developer and got used to Sprockets, you may be wondering why should you use RequireJs? or what are the differences? First off, Sprockets and the Rails' pipeline allow for simple structuring of Javascript code, but don't provide any module support. For example, they don't provide namespace control by its own.
Or you could be thinking about the advantages of using AMD, like:
-
Allowing to package other types of resources within the module.
-
It is unnecessary to think about dependencies or load order for modules.
-
Load modules on demand.
-
You don’t need to worry about naming conflicts as you don't have to touch the
window
object. -
It is easier to maintain or update since modules are independent. You could do incremental updates so QA testing will be done in the same way.
I know, I know, as I said before, being a Rails developer and having the assets pipeline helps us have a better structure, which lets us have several small files doing the maintenance easier. So, up until this point, both alternatives score equally high. But now take AMD's strongest advantage into consideration: the module definition (not the asynchronous loads, as you might have thought!)
Also, as with every framework, library, or programing language, the correct use will depend on the target application. So, is RequireJs suitable for every Backbone application? Only your skill and experience as a developer will tell.
Are you still in doubt? go and check the Why Web Modules? and Why AMD? sections of the RequireJs documentation. There you can find practical examples to verify if RequireJs is suitable for your Backbone app.
I will be posting again on how to use RequireJs in a Rails app and testing it with Karma, but till then, give it a try ^^.
Thanks for reading!