A front end developer's guide to CommonJS modules
CommonJS modules are the standard way of writing modules for node so if you're coming from that background they won't need explaining. However, when you are coming from a front end developer's background where you are used to writing code that runs in the browser I found they need a bit more explanation.
Background
For some time I have been using the basic JavaScript module pattern to organise my code:
window.DECADE_CITY = (function(module) {
// Module code
return module;
}(window.DECADE_CITY || {}));
Using an Immediately Invoked Function Expression (IFFE) provides a safe wrapper round code and only introduces one variable to the global scope. This pattern, and variations of it, have been in use by front end JavaScript developers for many years so most will be familiar with it.
Whilst this is fine for small to medium projects it has limits on how far it will scale, particularly when you want to re-use code between projects and need to resolve dependencies.
Browserify has a lot of potential for larger projects with its ability to use npm and Git repositories to manage dependences but I found that, coming from a front end perspective, the documentation suffered from the "how to draw an owl" problem.
One of the best introductory guides I found to Browserify is &yet's npm, Browserify & Modules which gives a good run down of what you need to know to get started. However, there was one sentence in particular that made me realise what I had been missing in the "how to draw an owl" explanations:
Npm and commonjs modules are how almost all backend node.js developers write and manage JavaScript code.
Whilst I'm familiar with npm, not being a back end node developer I'm not familiar with CommonJS modules. I can only assume that, as they are how back end developers work, none of the explanations I had encountered had explained CommonJS in a way that I could easily relate to as a front end developer used to the traditional module pattern.
Having familiarised myself with CommonJS modules they aren't inherently difficult but they are conceptually different to the traditional module pattern that I'm used to.
CommonJS modules
The most important thing to grasp about CommonJS modules is that the code you are writing is not what will turn up in the browser. They will go through an additional build step before they are ready for the browser.
The next thing that you need to know is that a CommonJS module is a single file that contains JavaScript. With systems based on the traditional module pattern you can have more than one module contained within a single file (even though you probably wouldn't). However, with CommonJS a file on the file system is a module.
Module scope
CommonJS introduces a new scope: module scope. Each module (and thus file) has its own scope. Coming from a front end perspective it can look like each module is putting code into the global scope but remember there is a build step before the code gets to the browser. If we're using Browserify to do the compilation it will wrap each module (file) in a closure which will fully encapsulate the module.
Whilst we don't have a global scope per se, there are two items that are implicitly present and these are what makes the module system work: module
and require
1.
Whilst these implicit items might seem like magic it's not too different from a browser environment. Even though we don't normally think about it, in the browser we are used to items being implicitly present, window
and document
being the most obvious.
module
As the code in the module will be wrapped in a closure we need a way to expose code to make our module useful. This is achieved by the module
object. It contains a number of properties about the module but the only one you need to worry about for now is the exports
property. Whatever you assign to module.exports
is what will be exposed when your module is included.
Shown below is an example module, this is the entire contents of the file:
Filename: my-module.js
function plus(foo, bar) {
return foo + bar;
}
module.exports = plus;
In this case we are attaching a function but you could attach variables, objects or constructors - whatever you want to expose as the external interface to this module. We'll see how these are accessed next.
require
Remember that each file is a module and it has its own scope. As a result we need a method of introducing code from other modules into that scope. This is where the built in require
function comes in, it's the counterpart to module.exports
.
require()
takes one argument - a string representing the ID of a module2 - and it returns the module.exports
of that module. By assigning it to a variable you can now access the external interface of that module.
Filename: my-other-module.js
var baz = require('my-module');
// The `module.exports` of 'my-module' is now assigned to `baz`:
baz(1, 2); // => 3.
// Nothing from the 'my-module' scope is available:
plus(1, 2); // => ReferenceError: plus is not defined
In this case we've assigned plus
to the export of my-module
and then used require()
to reassign it to the variable baz
within the module scope of my-other-module
.
In reality you would assign it to a better named variable than baz
but for the purposes of this illustration I wanted to use distinct names show how module.exports
and require()
interact to contain the module scope of each file.
Summary
The key points to remember when dealing with CommonJS modules are:
- Each file is a module, each module is a file.
- Each module has its own scope.
- A module exposes its code to other modules via
module.exports
. - Code exposed by other modules is introduced into the module's scope with
require()
.
Whilst this is an oversimplification, I found it helped me to think of modules as being wrapped with the following IIFE which is much closer to the module pattern that is traditionally used in front end development:
(function(require, module) {
// Module code
return module;
}(require, { 'exports': {} }));