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.
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.
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; };
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:
script
tags matters, since otherwise the browser will try to call the non-existent require()
function and throw an Error.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.
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 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.