Modularity in Javascript

Modularity in code increases maintainability of the project, letting us to to break code into manageable parts, easy to read and easy to fix forthcoming issues.

What options do we have for creating modules un Javascript?

Options

  • Use home-grown library for implementing modularity

  • Use CommonJS specification

  • Use RequireJS framework as implementation of CommonJS specification

  • Wait for upcoming ECMAScript 6 implementation of Javascript

  • Use other language that support modules, e.g. Dart

Using Anonymous Closure

You can simulate modularity in Javascript with the help of anonymous closure. It creates an anonymous function and execute it immediately. All of the code inside the function lives in a closure:

(function () {
  // - all vars and functions are in this scope only
  // - still maintains access to all globals
}());

Notice the () brackets around the anonymous function. Including () creates function expression instead of function declaration. For example:

var MyModule = (function() {
  var exports = {};

  // Export foo to the outside world
  exports.foo = function() {
     return "foo";
  }

  // Keep bar private
  var bar = "bar";

  // Expose interface to outside world
  return exports;
})();

MyModule.foo(); // OK
MyModule.bar(); // error

Using JQuery.extend

You can use jquery’s extend API in order to implement module:

function ModularityLibrary() {}

ModularityLibrary.prototype.createClass = function(definitions,
  extra_definitions) {
  var klass = function() {
    this.initialize.apply(this, arguments);
  };

  jQuery.extend(klass.prototype, definitions);

  if(extra_definitions) {
    jQuery.extend(klass.prototype, extra_definitions);
  }

  return klass;
};

ModularityLibrary.prototype.extendClass = function(baseClass,
  methods) {
  var klass = function() {
    this.initialize.apply(this, arguments);
  };

  jQuery.extend(klass.prototype, baseClass.prototype);
  jQuery.extend(klass.prototype, methods);

  return klass;
};

var Modularity = new ModularityLibrary();

Now you can use it in your code:

// Create new class

var DisplayModule = Modularity.createClass({
  initialize: function () {},

  display: function(connector) {
    console.log("display");
  }
});

// Create instance of class
var displayObject = new DisplayModule();

// Call instance function
displayObject.display();

Working with CommonJS

CommonJS is the set of specifications that define how to do modules in Javascript.

Instead of running your Javascript code from a global scope, CommonJS starts out each of your Javascript files in their own unique module context.

CommonJS adds two new variables which you can use to import and export other modules:

  • module.exports object exposes variables to other libraries;

  • require function helps to import your module into another module.

For example, Javascript class and jasmine spec for it could look like this:

// app/assets/javascripts/commonjs/example.js

module.exports.hello = function() {
  return 'Hello World';
};

// spec/javascripts/commonjs/example_spec.js
var example =
 require('../../../app/assets/javascripts/commonjs/example');

describe('example', function() {
  it("tests CommonJS", function() {
    example.hello();
  });
});

If you use karma framework for your unit testing and code coverage, you need to install karma-commonjs plugin:

npm install karma-commonjs --save-dev

and modify karma.conf.coffee file in order to recognize commonjs:

# karma.conf.coffee
 module.exports = (config) ->
   config.set
     ...

     frameworks: ['jasmine', 'commonjs]

     files: [
       'app/assets/javascripts/commonjs/*.js',
       {pattern: 'spec/javascripts/commonjs/*_spec.js',
         included: true}
       {pattern: 'spec/javascripts/commonjs/*_spec.coffee',
         included: true}
     ]

     preprocessors:
       'app/assets/javascripts/commonjs/*.js': ['commonjs'],
       'spec/javascripts/commonjs/*_spec.js': ['commonjs']
       'spec/javascripts/commonjs/*_spec.coffee': ['commonjs']
     ...

You have to add commonjs as framework and mark files that use CommonJS with commonjs preprocessor.

CommonJS implementations

Because CommonJS is just specification, you cannot use it directly in the browser. Node.js has it’s own implementation and we use it within spec, but we cannot use it on client side inside the browser.

Developers have different options to have it in browser. Some of them:

  • browserify, webmake - command line tools that wraps up your CommonJS-compatible code with simple implementation of require and module.exports.

  • NodeJS - asynchronous implementation of CommonJS specification.

  • [List of other solutions] [http://wiki.commonjs.org/wiki/Implementations]

Working with RequireJS

RequireJS uses another module format: Asynchronous Module Definition (AMD), originally created as part of the Dojo web framework.

Compared to CommonJS, the main differences of AMD are:

  • Special syntax for specifying module imports - define - must be done at the top of each script.

  • No tooling required to use, works within browsers out of the box.

First, create RequireJS-compatible js code:

// app/assets/javascripts/requirejs/example.js

define('example', function() {
  var message = "Hello!";

  return {
    message: message
  };
});

Then, create jasmine spec for it:

// spec/javascripts/requirejs/example.js

require(['example'], function(example) {
  describe("Example", function() {
    it("should have a message equal to 'Hello!'", function() {
      console.log(example.message);
      expect(example.message).toBe('Hello!');
    });
  });
});

Configure karma.conf.coffee to recognize RequireJS framework:

# karma.conf.coffee
 module.exports = (config) ->
   config.set
     ...
     frameworks: ['jasmine', 'requirejs]

     files: [
       'app/assets/javascripts/requirejs/*.js',
       {pattern: 'spec/javascripts/requirejs/*_spec.js', included: true}
       {pattern: 'spec/javascripts/requirejs/*_spec.coffee', included: true}
       'spec/javascripts/requirejs/spec-main.js'
     ]
     ...

You don’t have to preprocess requirejs files - it’s already part of karma framework.

Create main RequireJS file for tests only:

// spec/javascripts/requirejs/spec-main.js

// Grabs specs
var specs = [];

for (var file in window.__karma__.files) {
  if (window.__karma__.files.hasOwnProperty(file)) {
    if (/spec\.js$/.test(file)) {
      specs.push(file);
    }
  }
}

console.log(specs);

// Configures RequireJS for tests
requirejs.config({
  // Karma serves files from '/base'
  baseUrl: '/base/app/assets/javascripts/requirejs',

  paths: {
    'jquery': process.env.GEM_HOME + '//gems/jquery-rails-3.0.4/vendor/assets/javascripts/jquery'
  },

  // ask Require.js to load these files (all our tests)
  deps: specs,

  // start test run, once Require.js is done
  callback: window.__karma__.start
});

Using RequireJS in browser

For using RequireJS in browser you have to download it and include into your html file.

Your sample haml template file:

-#index.haml
%html{:lang => "en"}
  %head
    = javascript_include_tag "requirejs-2.1.8.min"
    = javascript_include_tag  "helper"
    = javascript_include_tag  "application"

  %body
    = "Hello, Web!"

and main RequireJS file:

// application.js
require.config({
  baseUrl: 'assets/javascripts',

  paths: {
    app: '.'
  }
});

// Start the main app logic.
require(['jquery-1.10.2.min', 'helper'], function($, helper) {
  helper.do_something();
});

Now you can open it in the browser:

open index.html

Calling CommonJS module from RequireJS

If you have CommonJS module that you would like to use with RequireJS, you have to:

  • define a module
  • provide a factory function which takes three arguments: require, exports and module.

See example below:

// app/assets/javascripts/commonjs/example.js
module.exports.hello = function() {
  return 'Hello World';
};

// app/assets/javascripts/requirejs/example.js
define('rjsExampleModule', function(require, exports, module) {
  var cjsExampleModule = require('example');

  return {
    rjsHello: function() {
      return cjsExampleModule.hello();
    }
  };
});

With the require argument, you load module using CommonJS style syntax. Other two parameters are optional and can be omitted.