JavaScript modules: an opinionated intro

March 3, 2014

It’s come to my attention that what I now consider common sense about JavaScript modules isn’t obvious, or even apparent at all, to many programmers. In fact, a year ago I myself barely knew what I was doing.

Hence this post. I’ll provide some background, steer you toward the good stuff and keep you from going into the weeds.

This is less a how-to (there’re plenty of those) and more an overview of the landscape.

The (old, outdated) module pattern

JavaScript was invented in 1995 to add neat little things to web pages, and it wasn’t foreseen that we would eventually write whole apps in it. As a result, it lacks modules. Every script you include in a page just gets plopped together. Worse, all variables are global unless you use var and wrap them in a function:

a = 3;         // global variable
var b = 4;     // still a global variable!
function doStuff() {
    c = 5;     // global variable
    var d = 6; // local variable
}

In browsers, the global object is window, so these variables are essentially attributes attached to window: window.a, window.b, window.doStuff and window.c. Only d is local and not available on the window object.

To avoid unnecessary pollution of window, someone came up with the idea to wrap entire modules (and libraries) in anonymous functions:

(function() {
    var exports = {};
    var a = 1;
    exports.foo = function foo(x) {
        a += x;
        return a;
    };
    exports.bar = function bar() { return a; }
    window.MyModule = exports;
})();

In this example, the variable a isn’t visible to the outside world, but you can still access the module’s public methods via MyModule.foo and MyModule.bar. This is often called “the module pattern”, and the approach of having an anonymous function called right after its definition is called an immediately invoked function expression or IIFE.

Why the module pattern is not enough

Major libraries such as jQuery, Underscore and Backbone use the module pattern, treating the whole library as one module from your app’s perspective. This reduces pollution, but the libraries are still exposing their functionality by attaching it to the global object (window). As a result, sequencing matters. This works:

<script src="jquery.js"></script>
<script> $('body').text('Ponies are cool'); </script>

This doesn’t:

<script> $('body').text('uh...?'); </script>
<script src="jquery.js"></script>

Backbone depends on Underscore and jQuery. So this works:

<script src="jquery.js"></script>     <!-- supplies $ -->
<script src="underscore.js"></script> <!-- supplies _ -->
<script src="backbone.js"></script>   <!-- uses $ and _ -->
<script src="my-app.js"></script>

But this doesn’t:

<script src="backbone.js"></script>   <!-- where's $ and _? -->
<script src="jquery.js"></script>     <!-- too late -->
<script src="underscore.js"></script>
<script src="my-app.js"></script>

The module pattern forces developers to manually list out the order in which libraries get loaded. That’s tedious and error-prone work the computer should do for us. Also, the module pattern still pollutes the global namespace, just not as much.

Use Browserify

To solve this problem, I recommend Browserify. Built on Node.js, it allows you to use Node’s module system in browsers.

Full introductions, again, can be found elsewhere, but here’s the basic idea. Browserify bundles your source files together, wrapping each file in a function, so it gets the benefits of being in an IIFE without you writing one yourself. It supplies globals called module and exports to let you define a module, and a global called require with which you can require other modules (and libraries). Finally, it includes a tiny bit of support code in the bundle to make require work.

Here’s an example module for Browserify:

var a = 1;
exports.foo = function foo(x) {
    a += x;
    return a;
};
exports.bar = function bar() { return a; }

Assuming this code sits in myModule.js, another module in the same directory would use it like so:

var myModule = require('./myModule');
console.log(myModule.foo());

The ./ indicates a relative filename. Arguments to require beginning with ./ or ../ are treated as relative filenames (the .js can be omitted); all other arguments are treated as library names:

var $ = require('jquery');
$('body').text('Ponies are cool');

Yes, the latest version of jQuery supports this! (I believe it was added in 2.x, so if you need jQuery 1.x for IE8 support, you may need a wrapper around jQuery to make this work.)

To bring in libraries, you have to list dependencies in a package.json file. Node has excellent documentation on writing package.json files. A minimal example:

{
    "name": "ponies-are-cool",
    "version": "0.0.1",
    "private": true,
    "dependencies": {
        "jquery": "2.1.0"
    }
}

Then run npm install to fetch dependencies from npm, and browserify -o bundle.js main.js to build your code. In this example, main.js is your main file (Browserify will recursively search for require statements and bundle all dependencies too), and bundle.js is the resulting bundle you’ll include in a script tag. You only need the one script tag.

exports versus module.exports

Node’s modules, and hence Browserify, use a slightly extended version of an older module spec called CommonJS. The one thing Node and Browserify add is the ability to export a single function or constructor.

With exports alone, you can only export an object, containing functions or whatever:

exports.func1 = function() { ... }
exports.func2 = function() { ... }

But Browserify also allows you to export a constructor, by assigning to module.exports:

function MyClass(foo, bar) {
    // ... class constructor ...
}
MyClass.prototype.method1 = function() { ... }
MyClass.prototype.method2 = function() { ... }
module.exports = MyClass;

“But I don’t like X about Browserify!”

The two most common complaints about Browserify seem to be:

There’s a compile step

Set up a watch task to rebuild automatically when source files change. I blogged about a simple way to do this, the author of Browserify has his own way, and there are fancier options using pure-JS build tools like Grunt, Gulp and Broccoli.

Browserify is fast, so once you start a watch-and-rebuild task, you won’t notice there’s a compile step at all.

You don’t see original file/line number when debugging

Source maps make this a non-issue in Chrome, and, once this small bug is fixed, Firefox too. Pass the -d flag to Browserify to enable source maps.

I started using Browserify before it had source maps, and even then, debugging wasn’t a big deal. Browserify doesn’t minify your code, so you can easily scan it and see what function you’re looking at.

If the lack of source maps in IE8 is truly a showstopper, you could even write a polyfill. Trap errors, look up the line number in the source map, and rethrow the error.

RequireJS: complex, you probably don’t need it

The main alternative to Browserify is RequireJS. Unlike Browserify, RequireJS can be used in development without a build step. It can also load JavaScript asynchronously, including from external servers.

That flexibility comes at a steep price. RequireJS is complicated, confusing and makes you write a lot of configuration.

RequireJS implements a module spec called AMD, which makes you define modules in an awkward way that lists every dependency twice:

define(["./foo", "./bar"], function(foo, bar) {
    // ... do stuff with foo and bar ...
});

However, you can avoid that using the relatively little-known “CommonJS wrapper”, which lets you write modules a little more like Node:

define(function(require) {
    var foo = require('./foo');
    var bar = require('./bar');
    // ... do stuff with foo and bar ...
});

In that case, RequireJS parses your function’s source code to pull out the require statements.

I initially used RequireJS when developing Pots, fyi. The last version of the code using RequireJS doesn’t look terrible, but I remember being frustrated at every moment trying to figure out how things worked. Esa-Matti Suuronen’s blog post about switching to Browserify encouraged me to try it, and it was an absolute breath of fresh air. All the boilerplate garbage went out the window.

I concede that lazy-loading JS can be useful if you have a huge amount of code (say, over 200KB minified and gzipped), and cold-start performance is important. But unless you know for sure those are your constraints, I’d steer clear. Even then, I’d look around for another, simpler solution before going back to RequireJS.

Closure Library: a quaint half-solution

At work I’m converting some old code to Browserify from relying on the module loader in Google’s Closure Library.

Closure’s modules appear to be an earnest attempt to solve the problems I described above with the module pattern. You use goog.provide to create a Java-style, hierarchical namespace for your module:

goog.provide('myapp.example.Counter');

This does very little; according to the tutorial it’s equivalent to:

myapp = myapp || {};
myapp.example = myapp.example || {};
myapp.example.Counter = myapp.example.Counter || {};

You then assign attributes to your new namespace. var is not used. To create a “private” member, you prefix its name with an underscore, but this is only a convention and not enforced:

myapp.example.Counter = function(initialCount) {
    this._count = initialCount || 0;
}
myapp.example.Counter.prototype.add = function(n) {
    this._count += n;
}
myapp.example.Counter.prototype.get = function() {
    return this._count;
}

I’ve modified my example module to be a class since that seems more in line with the Closure way of thinking — it really is Java-inspired. However, you could combine Closure modules with an IIFE and use them in a fairly normal way, like this:

// in foo.js
goog.require('myapp.example.Counter');

(function() {
    var counter = new myapp.example.Counter();
    // ...
})();

With Closure, modules explicitly specify the other modules (including libraries) that they depend on. That fixes the problem of specifying load order manually.

There’s a catch, though. In a third file, you might forget to include the dependency:

// in bar.js
(function() {
    var counter = new myapp.example.Counter();
    // ...
})();

Because foo.js required myapp.example.Counter, you’ll get away with this! At least up until foo.js gets removed or changed. At that point, bar.js will suddenly stop working. In other words, nothing checks that you have the requires in the right file, just that you have them somewhere.

There’s more to Closure’s module loader. It appears to have support for asynchronous loading, but the support isn’t documented that I can see. There’s a callback parameter to require and no mention of what it’s called with. Anyway, Closure is bizarre.

ES6 modules

EcmaScript 6, the next standards document for JavaScript, finally adds modules to the language. ES6 is not finished yet, though you can read drafts. ES6 modules are not implemented in any browser, but you can use them with the ES6 Module Transpiler, which compiles them to Node-style or AMD (RequireJS) modules.

ES6 module syntax looks like this:

// circle.js
import {PI, pow} from 'math';

function area(radius) {
    return PI * pow(radius, 2);
}
function circumference(radius) {
    return 2 * PI * r;
}

export {area, circumference};

Which “transpiles” to the following, usable in Browserify or Node:

"use strict";
var __dependency1__ = require("math");
var PI = __dependency1__.PI;
var pow = __dependency1__.pow;

function area(radius) {
    return PI * pow(radius, 2);
}
function circumference(radius) {
    return 2 * PI * r;
}

exports.area = area;
exports.circumference = circumference;

I like the ES6 module syntax — it’s Python-esque. But I see no particular reason to bother with it when the spec is still in flux (the syntax changed as recently as November!) and tooling support for CommonJS/Browserify is currently more mature. In a few years I can imagine ES6 modules being the obvious thing to use.

Conclusion

It’s 2014. Browserify is the thing you want to use. Check it out.

You can follow me on Mastodon or this blog via RSS.

Creative Commons BY-NC-SA
Original text and images (not attributed to others) on this page are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.