Unlike most languages, Javascript lacks a built-in mechanism for loading libraries. There are a number of competing solutions, but require.js offers perhaps the most complete.
As we just saw in Chapter 3, Namespacing, organizing Backbone code is a significant challenge to the Backbone developer. As difficult as it may be to keep code well organized within a namespace, it may be even more of a challenge to keep code organized on the file system. The require.js library offers just such a solution—doing so by exposing two keywords (require and define) that give Javascript a very familiar feel.
Consider again our poor Calendar application. To draw the month view of the calendar itself, we might use a series of Backbone views that start at the top-level CalendarMonth and work all the way down to CalendarMonthDay. In our namespacing solution, this would look something like:
window.Cal = function(root_el) {
var Models = (function() { /* ... */ })();
var Collections = (function() { /* ... */ })();
var Appointments = Backbone.Collection.extend({ /* ... */ })();
var Views = (function() {
var CalendarMonth = Backbone.View.extend({ /* ... */ });
var CalendarMonthHeader = Backbone.View.extend({ /* ... */ });
var CalendarMonthBody = Backbone.View.extend({ /* ... */ });
var CalendarMonthWeek = Backbone.View.extend({ /* ... */ });
var CalendarMonthDay = Backbone.View.extend({ /* ... */ });
// ...
})();
// Routers, helpers, initialization...
};In a small Backbone application, that is not too bad. There are some definite advantages to having everything in a single editor buffer—especially if everything is fairly small.
But, if the application grows significantly, this can quickly become unwieldy. Searching through a single file for the Appointment model can easily become a tedium of by-passing places in which the model is instantiated rather than defined. Or, worse still, where the Appointment view is defined instead of the model.
In the past, client-side Javascript developers have been reduced to a series of <script> tags, each of which populate a global namespace:
<script>
var Calendar = {
Models: {},
Collections: {},
Views: {}
};
</script>
<script src="Calendar/Views/CalendarMonth.js" />
<script src="Calendar/Views/CalendarMonthHeader.js" />
<script src="Calendar/Views/CalendarMonthBody.js" />
<script src="Calendar/Views/CalendarMonthWeek.js" />
<script src="Calendar/Views/CalendarMonthDay.js" />
<!-- ... -->Without help, such a solution is very much at the mercy of networking woes. If CalendarMonth creates an instance of CalendarMonthHeader, but CalendarMonthHeader arrives in the browser later than the requiring context, trouble can ensue. Regardless of load order, require.js ensures that no code is evaluated until all requirements have finished loading.
If there is significant network latency, then the round-trip time for the browser to fetch each of these files can significantly degrade application startup. Network issues can be mitigated by packaging all Javascript files into a single bundle. Although that introduces some complexity in the deployment process, it is a fairly well-established practice—with or without require.js.
Another, more subtle problem with this approach is that it encourages inadvertent coupling between the classes. Seemingly innocent references to higher order objects from a lower order object can quickly grow out of hand (see Chapter 18, Object References in Backbone for examples). With require.js (and similar mechanisms in server-side languages), dependencies must be explicitly declared. Coupling concerns become that much more readily identified and eliminated.
Let’s see how a require.js Backbone application looks in HTML:
<script data-main="scripts/main"
src="scripts/require.js"></script>That’s it! All of the <script> tags from our traditional approach have been replaced with a single <script> tag. The src of that script tag is the require.js library itself.
How, then, does the application code get loaded? The answer is the data-main HTML5 attribute, which points to the "main" entry point of the application. The ".js" suffix is optional, so, in our case, we are loading from the public/scripts/main.js file.
The entry point for a require.js application is responsible for any configuration that needs to be done as well as initializing objects. For our calendar application, it might look something like:
require.config({
paths: {
'jquery': 'jquery.min',
'jquery-ui': 'jquery-ui.min'
}
});
require(['Calendar'], function(Calendar){
var calendar = new Calendar($('#calendar'));
});In the configuration section, we are telling require.js where to find libraries that are referenced. For the most part, require.js can guess the library needed. In this case, we tell require.js that, when we require('jquery'), that it should use the minified jquery.min.js (again the ".js" suffix is not needed). This can be especially handy if libraries include version numbers or other information in the filename (e.g. jquery-ui-1.8.16.custom.min.js). There are many config options [12], but paths suffices 80% of the time.
As for loading and initializing our Backbone application, it requires three lines of Javascript:
require(['Calendar'], function(Calendar){
new Calendar($('#calendar'));
});The first argument to require() is a list of dependent libraries. In this case, we only want public/scripts/Calendar.js. Surprisingly, we do not need to pull in jQuery, Backbone or anything else—those dependencies are resolved lower in the Backbone application. The calendar class is supplied to the anonymous function, to which we bind the Calendar variable. At this point, all that is left is to instantiate the application.
For experienced Javascript coders—especially front end developers—this is pretty amazing. It is almost as if our beloved Javascript has become a "real" server-side language like Ruby, Python or Perl—complete with require / import statements. This, of course, is the entire point of require.js. It allows us to define and require modules, classes, JSON, and even functions.
To see how we might define a require.js module, let’s have a look at the Calendar.js class that is being required above:
// public/scripts/Calendar.js
define(function(require) {
var $ = require('jquery')
, _ = require('underscore')
, Backbone = require('backbone')
, Router = require('Calendar/Router')
, Appointments = require('Calendar/Collections.Appointments')
, Application = require('Calendar/Views.Application')
, to_iso8601 = require('Calendar/Helpers.to_iso8601');
return function(root_el) {
// Instantiate collections, views, routes here
};
});Require.js modules are built with the define() method. The define() method is roughly analogous to the module keyword in other languages—it encapsulates a code module. By convention, the first thing done inside a require.js module is requiring other libraries. This is where jQuery and Backbone dependencies finally start to be seen.
At the time of this writing, this will only work with a minor fork of Backbone maintained by James Burke [13], the require.js maintainer. Jeremy Ashkenas has publicly stated his intention to merge some form of this into Backbone by the next release, so we are not going too far out on a limb here. |
Also of note, is the naming convention that we use for the individual Backbone classes on the server. Instead of grouping them in Models, Views and Collections sub-directories, we put everything inside the Calendar top-level application directory. In there, we embed the type of class into the filename. This makes it easy to tell the difference between Models.Appointment.js and Views.Appointment.js in our editors (otherwise we would just have two files named Appointment.js).
Require.js modules must return a value—this is what gets assigned by the require() function. In our Calendar class, we use a function constructor to instantiate three things: a Backbone collection, a top-level view and the router. Then, we return an object for the new Calendar($('#calendar')) call:
define(function(require) {
// require things
return function(root_el) {
var appointments = new Appointments()
, application = new Application({
collection: appointments,
el: root_el
});
new Router({application: application});
Backbone.history.start();
return {
application: application,
appointments: appointments
};
};
});Taking a quick peek at a how a Backbone view is defined, we again see the define() statement at the top, followed by the various require() statements. Last up comes the return value, an anonymous view class definition:
// scripts/Calendar/Views.Application.js
define(function(require) {
var Backbone = require('backbone')
, $ = require('jquery')
, _ = require('underscore')
, TitleView = require('Calendar/Views.TitleView')
, CalendarMonth = require('Calendar/Views.CalendarMonth')
, Appointment = require('Calendar/Views.Appointment');
return Backbone.View.extend({
// ...
});
});Things like assigning the jQuery function to the dollar sign are much more explicit in require.js: |
At first, returning an anonymous view class might seem a little foreign, but this allows us the flexibility of assigning the class name however we see fit in the requiring context:
var Application = require('Calendar/Views.Application');
// or
var Calendar = {
Views: {
Application: require('Calendar/Views.Application');
}
}Require.js is very good about loading modules only once regardless of how many times in the dependency tree a particular module is |
At the risk of being redundant, a model class might be defined as:
// scripts/Calendar/Models.Appointment.js
define(function(require) {
var Backbone = require('backbone')
, _ = require('underscore');
return Backbone.Model.extend({
// Normal model attributes
});
});The collection that uses this model could then be defined as:
// scripts/Calendar/Collections.Appointments.js
define(function(require) {
var Backbone = require('backbone')
, Appointment = require('Calendar/Models.Appointment');
return Backbone.Collection.extend({
model: Appointment,
url: '/appointments',
// Other collection attributes here
});
});With require.js, the list of individual library files needed to run a Backbone application is no longer the responsibility of the web page that happens to include the application. Now, it is the dependent libraries who are tasked with this job—a much saner, more maintainable solution.
Require.js is a browser hack rather than a language hack. That is, once it analyzes dependencies, it adds new libraries by appending new <script> tags to the body of the hosting web page. Since it is already appending things to the page, there is nothing preventing require.js from appending other things—like CSS and HTML templates.
HTML templates, in particular, can further aid in the maintainability of Backbone applications. Consider, for instance, an appointment template (using the mustache-style from Chapter 5, View Templates with Underscore.js) that displays the title and a delete widget:
// public/javascripts/calendar/Views.Appointment.html
<span class="appointment" title="{{ description }}">
<span class="title">{{title}}</span>
<span class="delete">X</span>
</span>There are some advantages to keeping such HTML templates in our views, especially if they are small. Still, there are times when the views themselves get long or the syntax highlighting in our editors would be handy. In such cases, we can install the require.js text plugin [14]. The defined sections of our views can then require the HTML template:
define(function(require) {
var Backbone = require('backbone')
, _ = require('underscore')
, html_template = require('text!calendar/views/Appointment.html')
, template = _.template(html_template)
// ...
return Backbone.View.extend({
template: template,
// ...
});
});With that, we are now maintaining templates separately from the views without any significant changes to the overall structure of the code.
In the end, even a very small Backbone application organized with require.js is going to be comprised of a large number of individual files. By way of example, a limited calendar application might look like:
scripts +-- backbone.js +-- Calendar | +-- Collections.Appointments.js | +-- Helpers.template.js | +-- Helpers.to_iso8601.js | +-- Models.Appointment.js | +-- Router.js | +-- Views.Application.js | +-- Views.AppointmentAdd.js | +-- Views.AppointmentEdit.js | +-- Views.Appointment.js | +-- Views.Appointment.html | +-- Views.CalendarMonthBody.js | +-- Views.CalendarMonthDay.js | +-- Views.CalendarMonthHeader.js | +-- Views.CalendarMonth.js | +-- Views.CalendarMonthWeek.js | +-- Views.CalendarNavigation.js | +-- Views.TitleView.js +-- Calendar.js +-- jquery.min.js +-- jquery-ui.min.js +-- main.js +-- require.js +-- underscore.js
That is 24 round trips (request / response) that the browser would need to make before it is even capable of booting the application. Even if the client is connected to the server over a fast, low latency connection, there is way too much overhead in that setup [15]. To get around that, of course, modern websites use asset packaging and CDNs.
Most asset packages are ignorant of require.js so we might be given to despair. Happily, require.js includes its own asset packager, r.js. There are at least two ways to install r.js [16]. Which installation method is best depends on individual development environments and preferences.
To run the optimization tool, it is easiest to create an app.build.js file in your application’s root directory. This file contains a number of options, some of which will be nearly duplicate of the require.config options in the data-main file:
({
// Where HTML and JS is stored:
appDir: "public",
// Sub-directory of appDir containing JS:
baseUrl: "scripts",
// Where to build the optimized project:
dir: "public.optimized",
// Modules to be optimized:
modules: [
{
name: "main"
}
],
// Resolve any 'jquery' dependencies to the versioned jquery file:
paths: {
'jquery': 'jquery-1.7.1'
}
})The paths option has exactly the same meaning in the build configuration that is has in data-main—it tells require.js to map named dependencies to non-inferable filenames. Here, we are telling require.js to require references to jquery from the jquery-1.7.1.js resource. At some point, the optimization tool may be able to extract this information directly from data-main. At the time of this writing, it is separate to allow maximum flexibility when optimizing.
Most of the other configuration options are self-explanatory. To slurp in the entire dependency tree, all we need to do is specify the main.js module via the modules attribute—r.js will take care of the rest for us.
With that, it is a simple matter of building the optimized version of our public directory:
$ r.js -o app.build.js Tracing dependencies for: main scripts/main.js ---------------- scripts/jquery-1.7.1.js scripts/underscore.js scripts/backbone.js scripts/Calendar/Views.Paginator.js ... scripts/Calendar.js scripts/main.js
That’s it! The optimized version of the site is now available in the directory specified by dir (we used public.optimized). We can then point our web server at that directory and serve up super fast, packaged code.
Web developers have lived with the lack of a mechanism to require Javascript files for so long that we are almost numb to the pain. We would belittle a server-side programming language that lacked something so basic—how can we possibly produce quality code without a reliable way to organize it? We could almost excuse the lack of this ability in the past with Javascript—after all it is only recently that we have witnessed the explosion of client-side coding.
But in 2012 and beyond, it behooves us to use a module loader like require.js. It makes our code much more readable, and easier to maintain. It’s almost like programming in a real language.
[15] Unless you are using something like SPDY. By the way, you should totally buy Chris’s "The SPDY Book" if you have not already :D
[16] Download and installation instructions are available from the require.js site: http://requirejs.org/docs/optimization.html