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.