Thursday, July 18, 2013

Client side modules with require.js


In this post I will show you how to use Require.js to split a project into simple and manageable modules.

UMD and AMD

I really didn't want to dive into differences of these 2 module systems but I think it's very important to realize of what module system we are talking about.

UMD is the module system used by node.js. You define modules doing this (foobar.js):

module.exports = {
    foo: 'bar'
};

And load a module doing this:

var foobar = require('./foobar');
console.log(foobar.foo); // prints bar

You can use UMD in the browser using browserify.
If you usually work with Javascript in the browser you will notice two issues:
  • the exports object would be overwritten every time by differents modules
  • it uses a synchronous approach
As a matter of fact it cannot work in the browser without a build step (and this is the browserify's task).

AMD instead is designed from the ground up to work in the browser.

Saying this I am not advocating one of these systems. They are both very useful even though they use different approaches.

I started by explaining UMD because, unfortunately, both systems use a function called "require".
Now that you can't be fooled anymore by this let's go on.

What is an AMD module

An AMD module must be contained in a single file and is encapsulated by the global function define.
"define" takes two parameters: an array of dependencies and the actual module code.

//module3.js
define(['module1', 'module2'], function(module1, module2) {
    'use strict';

    var namespace = {};
    
    return namespace;
});

In this example I have defined a module called "module3". This module needs module1 and module2 to run.
The return value of define will be returned if another module requires module3.
module1 and module2 are resolved loading with AJAX module1.js and module2.js (both AMD modules).
The job of require.js is basically to resolve the dependency tree and to make sure that every module would be run just once.

A module has a pair of interesting properties:

  • It is loaded the first time is required by another module.
  • not a single variable is added to the global namespace. For this reason you can even use different versions of the same library if you need to

Bootstrap and configuration

After defining modules you will need to bootstrap your application (configure and load the first module). For doing this you will define in your page a script tag with require.js and the url of the bootstrap script (main.js).

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

After loading require.js it will load the "main.js" bootstrap using ajax. From this point every script will be loaded asynchronously and the DOMContentLoaded event (the jquery ready event) will be fired independently from the script loading.

Main.js is made of 2 part. The first one is require.js configuration:

require.config({
  baseUrl: "js/",
  paths: {
    jquery: 'vendor/jquery-1.9.1',
    underscore: 'vendor/underscore',
    backbone: 'vendor/backbone',
  },
  shim: {
      underscore: {
          exports: "_"
      },
      backbone: {
          deps: ['underscore', 'jquery'],
          exports: 'Backbone'
      },
      'jquery.bootstrap': {
          deps: ['jquery'],
          exports: '$'
      }
    }
});

Here are the most important parameters:
baseUrl: this is the path used to resolve modules.. So if you require "module1" this will be loaded from "js/module1.js".

paths is very useful to define script that are in some other paths. When you require "jquery" will be loaded the script "js/vendor/jquery-1.9.1.js".

Shims

Until now I assumed that every script is an AMD module but this is not always true.
require.js shim option allows to wrap automatically a non AMD script into an AMD define.

"deps" are dependencies to be injected and "exports" is the value to be returned:

For example your backbone.js will become:

define(['underscore', 'jquery'], function (_, $){

..actual backbone.js source ...

return Backbone;

});

The trick works even when you have to add attributes to an existing object (like defining a jquery plugin):

define(['jquery'], function ($){

$.fn.myJqueryPlugin = function (){
};

return $;

});

This is the case of Twitter bootstrap.

require

The second part of main.js is the actual bootstrap. It loads and execute the first module.

require([
  // Load our app module and pass it to our definition function
  'app',
], function(App){
  // The "app" dependency is passed in as "App"
  App.start();
});
The app module will start a chain of loading that will load the entire application's scripts.

The useful text plugin

The text plugin allows you to add text files as dependencies and this is very useful for client side templating.
Just add "text" in the path config:

require.config({
  ...
  paths: {
     ...
    text: 'vendor/text'

and you can load your templates:

define(['underscore', "text!templates/template.html"], function (_, template_html){
    var template = _.template(template_html);
    ...
});

You can also write your own plugin as described here.

Script optimization

Using a lot of modules have an obvious drawback: the time spent to load these modules in the browser.
Require.js comes with a useful tool for optimizing scripts: r.js. It analizes, concatenates and minifies all the dependencies in one single script.

I warmly recommend to use a build step to make this operation automatically.

I use grunt and a grunt plugin to automate everything.

Installing grunt and the require.js optimizer plugin

Grunt is structured in 2 different modules: "grunt-cli" and "grunt" and a series of plugins. "grunt-cli" can be installed globally:

npm install -g grunt-cli
grunt and the plugins should be installed locally pinning a release version. This system allows to use different versions and plugins for each project.
npm install grunt --save-dev

npm install grunt-contrib-requirejs --save-dev

The save-dev option adds the modules in the package.json under the "devDependencies" key and using the latest release.
...
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-requirejs": "~0.4.1"
  },
...

You can also do this manually and launch "npm install".

Grunt configuration

Grunt needs a configuration file called Gruntfile.js this is an example from a past project of mine:

module.exports = function(grunt) {

  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    requirejs: {
      compile: {
        options: {
          mainConfigFile: "static/js/main.js",
          baseUrl: "static/js",
          name: "main",
          paths: {
            'socketio': 'empty:',
            'backboneio': 'empty:'
          },
          out: "static/js/main-built.js"
        }
      }
    }
  });

  // Load the plugin that provides the "uglify" task.
  grunt.loadNpmTasks('grunt-contrib-requirejs');
  // Default task(s).
  grunt.registerTask('default', ['requirejs']);

};

In the "requirejs" configuration I have:
mainConfigFile: the path of the bootstrap script
baseUrl: the same as configured in the main.js configuration
name: the name of the main script
paths: in this section I added a pair of script to not be included in the optimized script. These 2 files are served directly by my node module. You can also do the same for script served through a CDN or an external service.
out: the output file

"registerTask" allows me to launch the whole optimization step with the "grunt" command (using no options).

At the end of the task I can load my new optimized script using:

<script data-main="/js/main-built" src="js/vendor/require.js"></script>
I think this is all. Stay tuned for the next!

Edit: If you liked this you'll probably be interested in this other blog post on require.js edge cases.