JavaScript modules in the browser, Node, and Closure compiler

about | archive


[ 2014-May-28 16:51 ]

One of the many sucky things about JavaScript is that it was not designed for large-scale programming, so it has no built-in support for building reusable modules. There are a few different techniques, so when I started writing a lot of JavaScript, I found it difficult to write modules that could be used in both the browser and Node without modification. Here is the approach I currently use, which works in both the browser and in Node, can be type-checked using the Closure Compiler, and automatically unit tested in both the browser and Node. Example code lives in a Github repository.

Modules in web browsers

In the browser, everything is loaded in a single global namespace. If a global name is assigned multiple times, whichever comes last is used. To avoid conflicts, reusable JavaScript modules typically create a namespace object at the global scope, and add all their exported functions as properties of this object. For example, jQuery creates the namespace $, and has functions like $.ajax(). Ideally the module is defined inside an anonymous function, to hide any internal functions and variables. Here is a simple example (complete example):

var mylib = {};
(function(){

var privateAddFive = function(a) {
  return 5 + a;
};

mylib.publicAddSix = function(b) {
  return privateAddFive(b) + 1;
};

})();

To use the module, you add the script tag to your page. After that tag, anyone can call mylib.publicAddSix() but privateAddFive() is hidden.

Modules in Node

Node needed to invent their own module system since there is no DOM or script tags. The require() function evaluates a file and returns a namespace object. In a file, you export names by adding properties to the module.exports object, or by replacing module.exports with your own object. The module above could look like (complete example):

var privateAddFive = function(a) {
  return 5 + a;
};

module.exports.publicAddSix = function(b) {
  return privateAddFive(b) + 1;
};

Making it work in both

To make the module work on both, we define namespace objects for the browser. We detect Node at runtime, by checking if module.exports exists using typeof (which doesn't throw if the variable is undefined). If it exists, we replace it with the namespace object we defined. To import other modules, we declare the variable and call require if it is not already initialized (complete example).

// new namespace object defined by this file
var mylib = {};
// import used by this file
var dependency = dependency || require('./dependency');  
(function(){

// define properties on mylib, use dependency

// export the namespace object
if (typeof module !== 'undefined' && module.exports) {
  module.exports = mylib;
}
})();

There are some important limitations:

Automated unit tests

This approach can be used to write unit tests that work in the browser and in Node. Both Jasmine and Mocha work, but I'm using Jasmine since it seems to be more widely used. To run them in the browser, you need to add the appropriate script tags to an HTML template (example). To automatically run them in the browser, I've been using Karma with PhantomJS. This requires yet another configuration file specifying the files to load, but after it is set up works very well. For running tests in Node, I use jasmine-node. Both Karma and jasmine-node can run in a mode where they automatically re-run tests when any file changes, which is useful for development.

Type-checking with the Closure compiler

I think the Closure compiler's type checks are useful, and thankfully making it work with these modules is relatively easy. We need an externs file that declares require and module, and we need to add @suppress{duplicate} annotations when importing dependencies. See the example in my repository for details. If you want to use the compiled output, you must be aware that it aggressively renames everything it can, so you'll need to follow the usual techniques for exporting symbols when using advanced mode. If you compile your unit tests, you can run the compiled output in both the browser and in Node. This seems potentially for verifying that the compiler's optimizations don't break your code.

The annoying part: dependencies

The annoying part that I left out of the discussion is how to specify the dependencies you need to load to run a given piece of code. If you want to use this file in a web page, in a unit test runner, or with the Closure compiler, you need to include all the transitive dependencies manually. Some of the more sophisticated module systems include an automatic way to collect these dependencies, like Google's Closure library ClosureBuilder. In my case, I currently use the Closure Compiler and unit tests to verify that I've included all the right pieces.