Eventual Consistency

Converting an existing Backbone.js project to Require.js

Recently, I've undertaken the task of converting a rather large Backbone.js app to using Require.js. Since the process can get tricky at times, here's a quick recap of what I had to do to get it done.

0. Why use Require.js in the first place?

Most projects start small. A couple of script tags that include jQuery (or Zepto, or whatever else you are using), a couple of plug-ins, and your own code. As the project matures, more code is added, and to keep things sane, we divide them into gradually more modular components. We usually end up with a lot of files, each one containing a specific portion of functionality. While this seems easy enough at first, it makes reusing them really hard since they usually depend of each other in all sorts of weird ways.

Require.js helps us define exactly what our component depends on, almost like "regular" imports in other development environments. It means we can lazily load dependencies as needed, as well as reuse components easily, as dependencies are pulled automatically. In addition, with r.js we can easily combine and minify our app to reduce the amount of script tags along with the superfluous HTTP requests they incur.

1. Adding Require.js to your project

OK so the first part is easy enough. Download the require.js distribution, or if using Bower, just issue the following command:

$ bower install requirejs

(As a side-note, Bower and require.js work really well together).

Once we have the library, we can add it to our project by adding a single <script> tag:

<script data-main="app/js/main" src="static/js/vendor/require.js"></script>

that data-main attribute will tell require.js where to start. It should point to a javascript file which require.js will load, containing configuration and instructions on how to fire up our application. be sure to remove the .js from the file name, since we now treat it as a module. This is a convention that require.js adheres to.

Also, this would be a good time to remove all other <script> tags you have lying around there. We won't be needing them anymore.

2. Setting up the required configuration

So we've loaded require.js, and pointed it at a currently empty file which should somehow load our app. If you haven't yet, create that main.js file. Inside our main module we'll start by describing our project's layout to require.js:

requirejs.config({
  'baseUrl': '/static',
  'paths': {
    'app': 'app/js',
    // define vendor paths
    'jquery': 'js/vendor/jquery-2.0.0.min',
    'underscore': 'js/vendor/underscore-min-1.5.1',
    'backbone': 'js/vendor/backbone-min-1.0.0',
    'handlebars': 'js/vendor/handlebars',
  }
});

This assumes the following structure:

static/
    app/
        js/
            base.js
            views.js
            models.js
            collections.js
            app.js
            main.js
    js/
        vendor/
            jquery-2.0.0.min.js
            underscore-min-1.5.1.js
            backbone-min-1.0.0.js
            handlebars.js

So what have we here? We told require.js that /static is the root URL for our application. This means that all paths are from now on relative to that location.

Then, we go on to explain where our current dependencies are located. The paths object is actually a mapping between module name and a real location in our project from which to load that module. You'll notice that we don't specify the .js extension here as well.

3. Handling non AMD compliant modules and plug-ins

In a perfect world, our setup would be essentially over. However, require.js uses AMD style loading, which means that modules need to declare what they depend on, and more importantly, what they export.

Backbone.js and Underscore, however, are currently not AMD compliant. for that purpose, require.js introduces the concept of "shims", which let require.js load non AMD compliant libraries. Let's add them to our config object:

requirejs.config({
  'baseUrl': '/static',
  'paths': {
    'app': 'app/js',
    // define vendor paths
    'jquery': 'js/vendor/jquery-2.0.0.min',
    'underscore': 'js/vendor/underscore-min-1.5.1',
    'backbone': 'js/vendor/backbone-min-1.0.0',
    'bootstrap': 'js/vendor/bootstrap.min',
    'handlebars': 'js/vendor/handlebars',
  },
  // Shim declaration
  'shim': {
    'underscore': {
      'exports': '_'
    },
    'backbone': {
      'deps': ['jquery', 'underscore'],
      'exports': 'Backbone'
    },
    'handlebars': {
      'exports': 'Handlebars'
    }
  }
});

Notice how jQuery does not need such a shim. This is because jQuery >=1.7 is AMD compliant, so it should just work.

So now require.js knows what our app basically looks like. Now it's time to actually load our application and convert our own code to be AMD compliant.

4. Converting our code to be AMD compliant

We'll assume for the purpose of this post that our app is laid out as such: base.js - containing basic utilies and settings, views.js - containing our Backbone Views, models.js - containing Backbone Models, and app.js - containing our main Application.

The process is pretty much the same for all files. We start by examining the file and seeing what dependencies the file uses. for example, the views.js file will use jQuery for DOM manipulation, Handlebars for rendering templates, and Backbone for defining the View subclasses.

What we'll need to do is wrap our code with a define() function which require.js provides. This function takes 2 arguments: an array of strings describing the dependencies and a function that should return whatever we want to export from this module. This function gets whatever we decided to import as arguments.

As always, this is better explained with an example. Our converted views.js file should now look something like this:

define(['jquery', 'handlebars', 'backbone'], function($, Handlebars, Backbone) {

    var SomeView = Backbone.view.extend({
        render: function() {
            var template = Handlebars.compile( $('#template-someview').html() );
            var rendered = template(this.getContext());
            ...
        }
        // more view code
        ...
    });
    ...
    // export stuff:
    return {
        'SomeView': SomeView
    };
});

Loading something such as jquery seems a bit abstract at first. Require.js knows how to find our jQuery module using the paths declaration we made before in main.js. This means that the module loading configuration is centralized, and decoupled from our code. Our views module doesn't need to know where jQuery is located in our project, meaning that we can easily port this module to another project if we'd like, without changing or adjusting our imports. Awesome.

By the way - You don't have to return all View subclasses. as with any module, you should only export things that could be used by other modules. It's ok to keep some views or bits of functionality for internal "private" use by the module itself. It's up to you.

Repeat this process as necessary for other files in your project. This might also be a good time to break things down into smaller files with smaller scopes of functionality. Don't worry about having to load multiple scripts, we'll use r.js later to concatenate and minify all our resources into a single file later.

5. Launching our application.

Time to fire up our newly converted app. For that, we'll revisit our main.js file, and add the following beneath our requirejs.config() declaration:

require(['app/app'], function(Application) {
    var app = new Application();
    app.start(); // or whatever startup logic your app uses.
});

This will load your main application and fire it up, the same way a "main" module would probably do in several other languages. Your app is now AMD compliant and uses require.js to manage and declare modules and dependencies. Way to go!

6. Deploying to production

A great tool that require.js comes with is r.js. It lets you automatically pull all your dependencies and modules into a single file and minify it. It also lets you do other neat stuff such as concatenating CSS files.

The easiest way to build our app for production, is to create a build.js file containing our build configuration. It should look something like this:

({
  "baseUrl": "./static",
  "name": "app/main",
  "mainConfigFile": "./static/app/js/main.js",
  "out": "./static/js/app.js"
})

I generally put this file one level above my static folder, since I don't want it to be deployed as a static asset. The most important setting here to note is mainConfigFile - this should be the path to our main module from before, so we keep our configuration DRY.

To actually build the project we need to first install r.js. The easiest way to do that (provided you have node.js and npm installed) is to use npm:

$ npm install -g requirejs

Once installed, we can call r.js pointing it to our build.js file:

$ r.js -o build.js

Depending on the size of your project, it could take a few seconds to resolve all dependencies and minify them. Once done, we can change our <script> tag to include the built version:

<script src="/static/js/vendor/require.js"></script>
<script src="/static/js/app.js"></script>

Notice how we removed the data-main attribute. We added the compiled version of our code instead, containing everything we need inside it. If you want to further optimize this, you can use something like Almond.js which lets you only use a single <script> tag containing a minimal AMD loader, and your built application in a single file.


So thats basically it. I'm guessing different setups will require slight adjustments for this to work. If you have tips of your own for working with AMD and Require.js feel free to comment or to hit me up on Twitter at @ozkatz100!

comments powered by Disqus

Hi, I'm Oz Katz

I am a co-founder and CTO over at Swayy.

I usually write about software development using Python, JavaScript and other awesome, open source tools.

Feel free to reach out on Twitter, or contact me using the links at the bottom of the page.