Melon Marshall Farrier's tech blog

Commentary, coding tips, libraries and utilities

Starting with a fair number of interdependent JavaScript files that I had been loading in the traditional way by adding all scripts in the proper order at the bottom of each page, I wanted to reorganize these files using AMD, which not only improves performance by allowing asynchronous loading to the extent possible but also makes it much easier to keep track of dependencies. I in fact discovered that I had comments in my old files that declared dependencies that didn't in fact exist and missed dependencies that did.

What not to do

I first tried to set everything up without changing all of my source files but instead just declaring dependencies in the shim member of the object passed to require.config() in my main.js file:

// main.js
require.config({
    // other configuration specifications 
    shim: {
        'backbone': ['underscore', 'jquery'],
        'radioGroupView': ['backbone'],
        // other dependency declarations
    }
});
This technique leads to ugliness and missed dependencies. It's ugly because you end up repeating the same dependencies over and over every time you have a new main.js file. In the above example, I can avoid having to declare the radioGroupView dependency in the shim configuration if I wrap that file in a define() block. I had been trying to avoid wrapping each file in a define() for several reasons:
  • I didn't want to have to change every single JavaScript file.
  • I didn't understand exactly how the define() function works.
  • I didn't want my library code to have a dependency on require.js.

What finally prompted me to do it correctly was when a demo of one of my JavaScript libraries failed in Chrome because a dependency wasn't loading properly. What I eventually discovered is that my massive shim object was missing a dependency. In the meantime, I had looked more closely at the recommended way to set up modules so that I had a better understanding of the define() wrapper. And I noticed that such a pervasive library as jQuery has no problem with the dependency on RequireJS, which even allows you to build all of your JavaScript into one optimized file. And once I started reorganizing my code, I realized how much better it is to do it the recommended (right) way.

Since I also use Backbone.js, I looked at a backbone tutorial on file organization and discovered that I didn't need to modify my directory structure at all. Here, I won't reiterate what I learned in various online tutorials, but I would like to point out a few things that I didn't see in the tutorials.

define() and require()

It makes for a clean structure to enter your JavaScript in 2 steps: First, you have a main.js file that gets called from your .html file with a line like:

<script src="lib/require.js" data-main="scripts/main"></script>

In main.js you want to do only generic configurations such as paths to the libaries you're using and to call your executable JavaScript, which is in a separate file app.js. main.js uses the require() function as entry point into app.js, and after that all files, including app.js is wrapped in a define() function. Here are the contents of my main.js:

require.config({
    baseUrl: '.',

    paths: {
        'jquery': 'lib/jquery.min',
        'bootstrap': 'lib/bootstrap/js/bootstrap.min',
        'underscore': 'lib/underscore.min',
        'backbone': 'lib/backbone.min',
        'prettify': 'lib/google-code-prettify/prettify'
    },

    shim: {
        'backbone': ['underscore', 'jquery']
    }
});

require([
        'scripts/app'
    ], 
    function(App) {
        App.initialize();
    }
); 

Note that I still do have a shim field for Backbone. That's because this code is using backbone-1.1.0, which needs this bridge to work properly. This problem has been addressed in the most recent release of backbone. I'll discuss below more about what it is and how to deal with it if you're using a library that isn't AMD-aware.

The require() call in the above code says to load the file scripts/app.js, then call the function passed to require() using as argument whatever it is that the function passed to define() in app.js returns. That sounds more complicated than it is. An extremely simple app.js file might look like this:

define(function() {
    var initialize = function() {
        console.log("Hello World");
    };

    return {
        initialize: initialize
    };
});

In that case the value of the App argument in line 20 of main.js is an object with a member initialize, returned as anonymous object by the function passed to define() in app.js. Its initialize field has been set there to be the function defined in lines 2-4.

Let's look at a more realistic version of app.js. Here's one I'm actually using:

define([
    'scripts/utilities/views/app-view'
], function(
    AppView) {
    var init = function() {
        var appView = new AppView({
            el: '#app'
        });
    };

    return {
        initialize: init
    };
});

Here there is actually only one dependency required to interpret the code in the function passed to define(). So, that's the only dependency I need to declare. When this code executes, the interpreter has to know what the object is that the wrapped function in scripts/utilities/views/app-view.js returns. That object is what will be called AppView in the scope of this function, and it better be a constructor given the code here.

While define() for module definition and require() for starting the execution of your JavaScript look very similar, a very important difference is that the function passed to require() doesn't return a value but simply executes. With define(), however, the function passed must actually return the module you have created, which will normally be either an object or a function. Both require() and define() specify dependencies which will be executed prior to the execution of the function passed as argument, but require() can't pass an object to dependent code.

The beauty of this setup, in contrast to the whole shim construct, is that we only need to declare immediate dependencies here. In fact, the code in app-view.js is dependent on a number of other libraries, some my own, others imported. But those dependencies only need to be declared in app-view.js itself. So, one huge advantage is that you can declare dependencies exactly once, in the file where they're used, and then you don't have to compose a comprehensive set of transitive dependencies when you use that file. Each file takes care of its own dependencies.

Here is the relevant portion of app-view.js:

define([
    'jquery',
    'backbone',
    'underscore',
    'usr/utils',
    'scripts/utilities/views/radio-group-view'
], function(
    $,
    BbRet,
    UsRet,
    Utils,
    RadioGroupView) {
    "use strict";

    var AppView = Backbone.View.extend({
        // code for building view
    });

    return AppView;
});

The nested function depends on jQuery, Backbone, Underscore, my own library code (usr/utils), and another view. And the dependent code will only executed once these dependencies are loaded.

Note, however, the strange names I have given the variables for Backbone (BbRet) and UnderscoreUsRet. This is because my code wasn't working when I used the expected names Backbone and _, and the error message I was getting was that the variables were undefined. The reason is that the versions I was using didn't provide a return value but instead just created the global variables Backbone and _. I was thus shadowing the global variable (the file was executing, so the global variable was being created) with a local variable that had the value undefined. jQuery, by contrast, is set up to return $ if used in conjunction with require.js and to define it globally so that it can also be run without require.js. So the jQuery dependency can (and should) be passed to the function with its expected name of $.The latest version of Backbone seems to do the same thing, but I haven't tried it out and thought that, before doing so, I would share this information for the benefit of others who might run into the same issue.

Marshall

Contact:
info@codemelon.com

Projects

Utilities

Downloads

Documentation