Modularizing and Packaging JavaScript for Scalable, High Performance Web Apps

Modules in JavaScript

With the advance of MV* frameworks you can observe as JavaScript evolves ahead of official specs towards large scale development. It requires even higher level of maintainability and one of the first focuses here is encapsulation and information hiding. We need the code-base to be organized into independent, interchangeable components so that each of them contained code implementing only one aspect of desired functionality. Here comes to help the Module pattern. Module is a portion of code enclosed with a scope where all the members have private access by default and do not pollute global namespace. To get access to a member of module we have to export it within module scope and import it outside of module.

Prior to Ecma-262 Edition 6, which is still a working draft, JavaScript had no build-in facilities to define a module. However the concept is easy achievable by moving the intended code into a closure. That is usually self-calling lambda-function that imports passed-in parameters and exports an object in return statement. As you can see, for instance, in Ben Cherry's and Todd Motto's studies, here can be a number of implementations. However there are also standardized approaches: CommonJS Modules/1.1 and AMD. The first one was initially designed for server-side JavaScript and synchronous by nature while the second describes the way to deal with asynchronously loaded modules and their dependencies. AMD is meant to improve performance of in-browser web applications by bypassing module loading along with the rest of the page content. Yet, in real-world applications the core experience can require plenty of modules what produce quite a number of HTTP requests during page loading. One of the best practices for speeding up a web site implies that this number must be minimized. Developers of AMD loaders are aware of this and provide module file combining utilities (e.g. r,js). Such tools usually concatenate and compress module files. So you still need to load in you production code usually quite weighty AMD library and, now, synchronous modules collected in a single file, still designed for asynchronous loading. It doesn't sound as an optimal way, does it?

Fortunately in like manner of AMD optimization we can compile CJS modules in a single file suitable for in-browser use. It takes surprisingly little extra code to resolve synchronous dependencies as it is described in CJS specs. Let's see see how it works.

Getting started with CommonJS compiler

First of all wee need a compiler. CommonJS Compiler can be easily installed by using nodejs package manager:

$sudo npm i cjsc -g

on Windows, of course, you don't need to prefix the command line with sudo.

Now we create a module file bar.js that defines name variable and obj object literal. The last one is being publicly exposed y using module.exports.

var name = "bar",
    obj = {
      name: name
    };
// Exporting an object
module.exports = obj;

We also put beside foo.js module file where we load bar.js and attempt accessing its members:

// Importing an object from bar module
var bar = require( "./bar" );
console.log( "Accessing private member `name` of bar.js: ", typeof name );
console.log( "Accessing private member `objs` of bar.js: ", typeof objs );
console.log( "Accessing object exposed in bar.js: ", bar.name );

Compiling foo.js into build.js:

$cjsc foo.js build.js

When running build.js we get the following output in JavaScript console:

Accessing private member `name` of bar.js:  undefined
Accessing private member `objs` of bar.js:  undefined
Accessing object exposed in bar.js:  bar

What have we just achieved? We loaded one module in another and accessed an exported object. We also made certain that private state isn't available outside the module.

Module loading

You may wonder of what exactly the loading of module does. The ES6 spec proposal for modules implies: “Loading a module URL multiple times results in a single cached instance”. Let's check it.

bar.js:

console.log( "bar.js: constructing" );
// Exporting an object
module.exports = {
	name: "bar"
};

foo.js:

// Calling `bar` module multiple times 
var bar = require( "./bar" );
console.log( "Assert both instances imported from `bar.js` strict equal: ", bar === require( "./bar" ) );

Output:

bar.js: constructing
Assert both instances imported from `bar.js` strict equal:  true

This time we checked that regardless of multiple calls the code preceding or constructing module exports is executed only once, the host module always receives the same instance of exported object.

Following this logic if we load a module that doesn't export anything its code will be simple executed and only once:

bar.js:

window.document.querySelector( "body" ).classList.add( "affected" );

foo.js:

var body;
require( "./bar" );
body = window.document.querySelector( "body" );
console.log( body.classList.contains( "affected" ) );

Loading non-JavaScript content

But what if we supply to require a path to a non-JavaScript file?

bar.tpl:

Lorem ipsum dolor sit amet, 
Lorem ipsum dolor sit amet, 
Lorem ipsum dolor sit amet

foo.js:

var tpl = require( "./bar.tpl" );
console.log( tpl );

Output:

Lorem ipsum dolor sit amet,
Lorem ipsum dolor sit amet,
Lorem ipsum dolor sit amet

When resolving module dependencies CommonJS Compiler exports any content of non-JavaScript or JSON syntax as a string. That comes pretty handy while working with templates:

bar.tpl:

{{title}}
 spends {{calc}}

foo.js:

var mustache = require( "./mustache" ),
		tpl = require( "./bar.tpl" ),
		view = {
			title: "Joe",
			calc: function () {
				return 2 + 4;
			}
		};

console.log( mustache.render( tpl, view ) );

Output:

Joe
 spends 6

Note that this example requires Mustache.js library

Debugging compiled code

It's usually hard to trace back the problem code in the sources files when an error occurs in the compiled code. However nowadays browsers support so-called source maps, so you can make JavaScript console point directly to the problem code:

$cjsc foo.js build.js --source-map=build.js.map

Run-time configuration

You may find among advantages of AMD configuration settings (including path aliases) to simplify path resolution and dependency listing. That is available with CommonJs compiler either.

You can compose a JSON configuration of this pattern:

{
    "<dependency-name>": {
        "path": "<dependency-path>",
        "globalProperty": "<global-property>",
        exports: [ "<variable>", "<variable>" ],
        require: [ "<dependency-name>", "<dependency-name>" ]
    }
}

And supply it with compilation options

$cjsc foo.js build.js --config=config.json

For example we want globally available jQuery object available as a CommonJS module. Besides we want a module out of jQuery plugin that is nether of CommonJS syntax nor of UMD syntax:

{
	"jQuery": {
		"globalProperty": "jQuery"
	},
	"plugin": {
		"path": "./config/vendors/jquery.plugin.js",
		"require": "jQuery",
		"exports": "jQuery"
	}
}

For the jQuery module, we instruct the compiler to make a module from global property jQuery of window global object. For the plugin, we define path alias (./config/vendors/jquery.plugin.js) and specify that plugin's module must retrieve a jQuery instance from jQuery module and export it after modification (jQuery.fn.plugin).

Build automation

Building a web project is becoming a common thing. By using an automated build tool (ANT, Grunt, Gulp) we are used to run various tasks on project including CSS pre-processing, image optimization, JavaScript linting, source compression. Grunt task configuration for CommonJs compiler can look like that:

... 
grunt.loadNpmTasks( "grunt-contrib-cjsc" );
grunt.initConfig({
	...
	pkg: grunt.file.readJSON("package.json"),
	cjsc: {
       debug: {
        options: {
          sourceMap: "./wwwroot/build/js/*.map",
          minify: false,
          config: {
            "backbone": {
              "path": "./wwwroot/vendors/backbone/backbone"
            }
          }
         },
         files: {
            "./wwwroot/build/js/app.js": "./wwwroot/js/app.js"
         }
       },
       build: {
        options: {
          minify: true,
          banner: "/*! <%= pkg.name %> - v<%= pkg.version %> - " +
                "<%= grunt.template.today(\"yyyy-mm-dd\") %> */",
          config: {
            "backbone": {
              "path": "path": "./wwwroot/vendors/backbone/backbone"
            }
          }
         },
         files: {
            "./wwwroot/build/js/app.js": "./wwwroot/js/app.js"
         }
       }
    }
	...

It gives us two options: cjsc:debug and cjsc:build. The first one we run during development; it provides source maps for debugging and doesn't compress output.

The second option we use when preparing production/stagging build.

This task in especially handy in conjunction with `watch`:

grunt.loadNpmTasks( "grunt-contrib-watch" );
... 
grunt.initConfig({
... 
	watch: {
      options: {
        livereload: false
      },
			 appJs: {
          files: [ "./wwwroot/js/**/**/**/*.js" ],
          tasks: [ "cjsc:debug" ]
      }
			... 
	}

Now we can make Grunt running compilation with every source file modification event:

$grunt watch

You can find an example of a simple single-page application built with Exoskeleton (Backbone) and CommonJS Compiler utilizing Grunt at https://github.com/dsheiko/exoskeleton-exercise

In conclusion

Comparing to AMD CommonJS modules are easy to maintain, it provides cleaner more readable syntax. Compiled CJS modules may give better end-user response time for the core experience code. However if your project requires modules loaded on-demand going without AMD loader can be a headache.

Both AMD and CJS/1.1 are put in the proposal of CommonJS Modules/2.0 specification. Some day we are going to have synchronous and asynchronous modules natively in the language. Until then we have to choose between an AMD loader and a CJS compiler depending on specific requirements of the project.