Chapter 8. Build Automation

We have seen how to create an application from the ground up using tests with Jasmine. But as the application grows and the numbers of files starts to increase, managing the dependencies between them can became a little difficult.

For instance, we have a dependency between the Investment and the Stock models, and they must be loaded in a proper order to work. So we do what we can; we order the loading of the scripts so that the Stock is available once the Investment is loaded:

<script type="text/javascript" src="src/Stock.js"></script>
<script type="text/javascript" src="src/Investment.js"></script>

But that soon can become cumbersome and unmanageable.

Another problem is the number of requests the application uses to load all of its files. Up until now it is 13 different files, so 13 requests.

So we have here a paradox; although it is good for code maintainability to break it in small modules, it is bad for the client performance, where a single file is much more desirable.

A perfect world would be to match the following two requirements at the same time:

  • In development we have a bunch of small files containing different modules
  • In production we have a single file with the content of all those files

Clearly what we need is some sort of build process. There are many different ways to achieve these goals with JavaScript, but we are going to focus on RequireJS.

RequireJS

RequireJS is an AMD implementation. AMD (Asynchronous Module Definition) is a standard on how to write modules in JavaScript.

There are other specifications such as CommonsJS that is used by Node.js, and they all work. But AMD is different from the others, in that it works in browsers seamlessly between development and production.

It was created based on the specifics of the browser environment, where things cannot always be synchronous, and loading a different module might require a request that will be completed at a later time.

Before we can go any further and start the project setup on RequireJS, we first need to understand the structure of an AMD module.

Module definition

In Chapter 3, Testing Frontend Code, we have seen how to use the module pattern with IIFE to organize our code. An AMD module is built on the same principles: a file and a function. However, instead of using an IIFE, we invoke the AMD define function passing a callback function as an argument. At a later time, RequireJS will invoke this function argument once it is needed by another module.

Here is a simple module definition without any dependency:

define(function () {
  function MyModule() {};
  return MyModule;
});

That is very similar to what we have done so far. The following example shows how that code would be if written using the conventions presented in Chapter 3, Testing Frontend Code:

(function () {
  function MyModule() {};
  return MyModule;
})();

And what about the dependencies? Up until now everything was globally available, so we passed the dependencies to the module as parameters to the IIFE as follows:

(function ($) {
  function MyModule() {};
  return MyModule;
})(jQuery);

But as you start using RequireJS on the project, there will be no more global variables. So how do we get those dependencies into our module?

If you looked closer at our simple module definition; it was returning the module value as its last statement:

define(function () {
  function MyModule() {};
  return MyModule;
});

So if RequireJS knows a module value, all we have to do is ask RequireJS. Let's refer again to the dependency example, but this time as an AMD module:

define(['jquery'], function ($) {
  function MyModule() {};
  return MyModule;
});

We select what module we need, so RequireJS loads it for us, and once the loading is complete, it calls the module definition function with the jQuery value. Pretty cool huh?

You can pass as many dependencies as you need to the dependencies array, and their values will be passed as arguments, in the same order, to the function, once they became available.

Project setup

Setting up a RequireJS is very simple. The library was created with ease of use in mind; it doesn't require an HTTP server or a compilation step to work (as with other solutions). All you need to get started is download a single JavaScript file and perform some small configuration.

For this example we are using version 2.1.6, so go ahead and download it from http://requirejs.org/docs/release/2.1.6/minified/require.js, and place it under the project's lib folder.

Next, we need to change our SpecRunner.html file to start using RequireJS. The first thing you are going to notice is that we no longer have any JavaScript dependency on the HTML file. Instead we refer to the RequireJS source and specify a special HTML attribute that tells RequireJS which is our main JavaScript file. From there all dependencies are declared on each module:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <title>Jasmine Spec Runner</title>
  <link rel="shortcut icon" type="image/png" href="lib/jasmine-1.3.1/jasmine_favicon.png">
  <link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">
  <script src="src/RequireConfig.js"></script>
  <script data-main="spec/SpecRunner" src="lib/require.js"></script>
</head>
<body>
</body>
</html>

It is kinda cool to see this very clean HTML file. All that is left are the CSS and the RequireJS references.

The new SpecRunner.JS file

Since we have removed all the JavaScript code from within the SpecRunner.html file, it needs to be somewhere.

As you can see by the RequireJS script tag; it sets its main file as a spec/SpecRunner.js file. This is where it all begins:

require([
  'jquery',
  'jasmine',
  'jasmine-html'
],
function($, jasmine) {
  var jasmineEnv = jasmine.getEnv();
  jasmineEnv.updateInterval = 1000;

  var htmlReporter = new jasmine.HtmlReporter();
  jasmineEnv.addReporter(htmlReporter);
  jasmineEnv.specFilter = function(spec) {
    return htmlReporter.specFilter(spec);
  };

  $(function () { jasmine.getEnv().execute(); });
});

We use the require function, since we don't need this file to be available as a module, and pass all of its dependencies (jquery, jasmine, and jasmine-html).

After RequireJS has finished loading them, it will call the function passing all of its dependencies as arguments. This is where we set up the Jasmine reporter and execute the specs; a familiar code that was once inside the SpecRunner.html file.

The RequireJS configuration

For this new runner to work, we need to tell RequireJS where to look for modules. So we create a new JavaScript source file called src/RequireConfig.js. Here we declare a global object named require:

var require = {
  baseUrl: 'src',

  paths: {
    'spec': '../spec',

    'jquery': '../lib/jquery',
    'backbone': '../lib/backbone',
    'underscore': '../lib/underscore',

    'sinon': '../lib/sinon',
    'jasmine': '../lib/jasmine-1.3.1/jasmine',
    'jasmine-html': '../lib/jasmine-1.3.1/jasmine-html',
    'jasmine-jquery': '../lib/jasmine-jquery'
  }
};

We load this file before RequireJS:

<script src="src/RequireConfig.js"></script>
<script data-main="spec/SpecRunner" src="lib/require.js"></script>

By the time RequireJS loads, it can read this configuration, and set up itself.

So far, we are setting up two parameters:

  • The folder (baseUrl) where RequireJS will look for modules by default, which we set to the src folder
  • A few path translations (paths), to allow us to refer to different library dependencies and specs, without using relative paths

This allows us to use the jQuery module like:

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

Instead of:

define(['../lib/jquery'], function ($) {} );

But there is still one piece of configuration left; to use any module as a dependency, it needs to be a valid AMD module. And this is not the case for all of our external dependencies. Luckily, RequireJS comes with a solution to support non AMD modules, as we will see next.

Using non AMD dependencies with Shim

The only external dependency that we have which supports AMD natively is jQuery, so we don't need any extra parameters for it to work. But for all of the remainder (Backbone.js, Underscore.js, Jasmine, and Sinon.js), we need to know:

  • What global variable it creates that needs to be exported to any module that requires it as a dependency.
  • Any dependencies that this module has. A known example would be the Underscore.js dependency for Backbone.js.

To fix these requirements, we need to add other configuration parameters to the RequireConfig.js file:

var require = {
  // other parameters...

  shim: {
    'backbone': {
      deps: ['underscore', 'jquery'],
      exports: 'Backbone'
    },
    'underscore': {
      exports: '_'
    },
    'jasmine': {
      exports: 'jasmine'
    },
    'sinon': {
      exports: 'sinon'
    },
    'jasmine-html': ['jasmine'],
    'jasmine-jquery': ['jasmine']
  }
};

We have to add the shim property; let's take the backbone example to understand how it works:

  • First, it tells that it depends on both underscore and jquery modules
  • And later, that it should export the global Backbone variable

To any module requesting for backbone, RequireJS will make sure that both underscore and jquery modules were already loaded, and it will also pass the correct value for the Backbone.js dependency on any define/require function.

Testing a module

Now that we have finished setting up RequireJS and our test runner, it is time to adapt our specs and code to RequireJS. And, as you will see, it is going to be pretty easy.

Let's take the Investment model as an example. First we need to wrap all the spec code inside the module definition:

define(function () {
  describe("Investment", function() {
    var stock;
    var investment;

    beforeEach(function() {
      stock = new Stock();
      investment = new Investment({ stock: stock });
    });

    it("should be a Backbone.Model", function() {
      expect(investment).toEqual(jasmine.any(Backbone.Model));
    });
  });
});

Then, we need to specify what are the spec dependencies by adding an array with the names of the depending modules:

define([
  'spec/SpecHelper',
  'backbone',
  'models/Investment',
  'models/Stock'
],
function () {
  describe("Investment", function() {
  });
});

Finally, we add the callback function arguments, to receive these dependencies into the module:

define([
  'spec/SpecHelper',
  'backbone',
  'models/Investment',
  'models/Stock'
],
function (jasmine, Backbone, Investment, Stock) {
  describe("Investment", function() {
  });
});

Since all specs require Jasmine, its custom matcher and plugins to be properly configured, they will all have the SpecHelper as a dependency.

And as with everything else, we need to make this SpecHelper an AMD module:

define([
  'jasmine',
  'jasmine-jquery'
],
function (jasmine) {
  jasmine.getFixtures().fixturesPath = 'spec/fixtures';

  beforeEach(function() {
    this.addMatchers({
      toBeAGoodInvestment: function() {
        var investment = this.actual;
        var what = this.isNot ? 'bad' : 'good';
        this.message = function() {
          return 'Expected investment to be a '+ what +' investment';
        };

        return investment.get('isGood'),
      }
    });
  });

  return jasmine;
});

As you can see, it has dependencies to Jasmine and all of its plugins (in our case, just jasmine-jquery).

Since it is actually setting up Jasmine, we return jasmine as the module value.

And since all dependencies are in place, all we have to do to make this spec run, is add it as a dependency to the SpecRunner module at spec/SpecRunner.js:

require([
  'jquery',
  'jasmine',
  'jasmine-html',
  'spec/models/InvestmentSpec'
],
function($, jasmine) {
  // Spec Runner code...
});

By now, you should be able to run this spec by opening the SpecRunner.html file on your browser. But as you might have expected, it should be failing.

So let's move to the Investment implementation, to see how we can fix this. Since we were using an IIFE, the process of conversion is much simpler than of the spec.

We already have all dependencies in place, all we have to do is add the define function and the array of dependency names. And instead of assigning the Investment to the global namespace, we return it as the module value:

define([
  'backbone',
  'models/stock'
],
function (Backbone, Stock) {
  var Investment = Backbone.Model.extend();

  return Investment;
});

This was very simple to accomplish because we were already using good practices to organize our code.

Optimizing for production

After we have finished porting the entire codebase to RequireJS, we are ready to use the optimizer to pack and minify our code. Therefore, for achieving the second goal we have a single file deployed on production.

To use the optimizer you are going to need Node.js and its package manager. The installation process was explained in Chapter 4, Asynchronous Testing – AJAX.

To simplify its use, we are going to install it globally, so it is always available on your path. Using NPM from any folder on your computer, install the package:

$ npm install -g requirejs

After the installation is complete, let's create a build configuration file with our build parameters. Add a new file at the project root directory called Build.js:

({
  mainConfigFile: 'src/RequireConfig.js',
  baseUrl: "src",
  out: "build/boot.js",
  name: "Boot"
})

As you can see it imports the parameters from the former RequireConfig.js file:

mainConfigFile: 'src/RequireConfig.js',

It also sets the baseUrl parameter again (Leaving it blank was causing a problem in the build process while using version 2.1.6 of the optimizer.).

baseUrl: "src",

It sets the destination for the build artifact; a file that will contain all our packed and minified source and dependencies:

out: "build/Boot.js",

Finally, it specifies the main file. For our SpecRunner.html it was the spec/SpecRunner.js file. And here, it is the src/Boot.js file:

name: "Boot"

As for the Boot file, it requires the Application to start:

require([
  'Application'
],
function (Application) {
  Application.start();
});

We haven't covered this Boot file in the book, so be sure to check the attached source files to understand better how it works.

Everything set; we are ready to run the optimizer. On the console, in the projects folder, type the following command:

$ r.js -o build.js

You should see something like this:

Tracing dependencies for: Boot
Uglifying file: code/build/Boot.js

code/build/Boot.js
----------------
code/lib/underscore.js
code/lib/jquery.js
code/lib/backbone.js
code/src/models/stock.js
code/src/models/Investment.js
code/src/models/Stock.js
code/src/plugins/jquery-disable-input.js
code/src/views/NewInvestmentView.js
code/src/views/InvestmentView.js
code/src/views/InvestmentListView.js
code/src/views/ApplicationView.js
code/src/routers/InvestmentsRouter.js
code/src/Application.js
code/src/Boot.js

This means that build/Boot.js was created.

You can take a look at it, a packed and minified version of the application code and its dependencies, ready for deployment!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset