what are modules ?
Why use modules?
There are a lot of benefits to using modules in favour of a sprawling, interdependent codebase. The most important ones, in my opinion, are:
1) Maintainability: By definition, a module is self-contained. A well-designed module aims to lessen the dependencies on parts of the codebase as much as possible, so that it can grow and improve independently. Updating a single module is much easier when the module is decoupled from other pieces of code.
Going back to our book example, if you wanted to update a chapter in your book, it would be a nightmare if a small change to one chapter required you to tweak every other chapter as well. Instead, you’d want to write each chapter in such a way that improvements could be made without affecting other chapters.
Sharing global variables between unrelated code is a big no-no in development.
As we’ll see later in this post, modules allow us to avoid namespace pollution by creating a private space for our variables.
3) Reusability: Let’s be honest here: we’ve all copied code we previously wrote into new projects at one point or another. For example, let’s imagine you copied some utility methods you wrote from a previous project to your current project.
That’s all well and good, but if you find a better way to write some part of that code you’d have to go back and remember to update it everywhere else you wrote it.
This is obviously a huge waste of time. Wouldn’t it be much easier if there was — wait for it — a module that we can reuse over and over again?
How can you incorporate modules?
There are many ways to incorporate modules into your programs. Let’s walk through a few of them:
Example 1: Anonymous closure
With this construct, our anonymous function has its own evaluation environment or “closure”, and then we immediately evaluate it. This lets us hide variables from the parent (global) namespace.
What’s nice about this approach is that is that you can use local variables inside this function without accidentally overwriting existing global variables, yet still access the global variables, like so:
Example 2: Global import
Another popular approach used by libraries like jQuery is global import. It’s similar to the anonymous closure we just saw, except now we pass in globals as parameters:
In this example, globalVariable is the only variable that’s global. The benefit of this approach over anonymous closures is that you declare the global variables upfront, making it crystal clear to people reading your code.
Example 3: Object interface
Yet another approach is to create modules using a self-contained object interface, like so:
As you can see, this approach lets us decide what variables/methods we want to keep private (e.g. myGrades) and what variables/methods we want to expose by putting them in the return statement (e.g. average & failing).
Example 4: Revealing module pattern
This is very similar to the above approach, except that it ensures all methods and variables are kept private until explicitly exposed:
That may seem like a lot to take in, but it’s just the tip of the iceberg when it comes to module patterns. Here are a few of the resources I found useful in my own explorations:
CommonJS and AMD
The approaches above all have one thing in common: the use of a single global variable to wrap its code in a function, thereby creating a private namespace for itself using a closure scope.
While each approach is effective in its own way, they have their downsides.
For one, as a developer, you need to know the right dependency order to load your files in. For instance, let’s say you’re using Backbone in your project, so you include the script tag for Backbone’s source code in your file.
However, since Backbone has a hard dependency on Underscore.js, the script tag for the Backbone file can’t be placed before the Underscore.js file.
As a developer, managing dependencies and getting these things right can sometimes be a headache.
Another downside is that they can still lead to namespace collisions. For example, what if two of your modules have the same name? Or what if you have two versions of a module, and you need both?
So you’re probably wondering: can we design a way to ask for a module’s interface without going through the global scope?
Fortunately, the answer is yes.
There are two popular and well-implemented approaches: CommonJS and AMD.
When you’re defining a CommonJS module, it might look something like this:
We use the special object module and place a reference of our function intomodule.exports. This lets the CommonJS module system know what we want to expose so that other files can consume it.
Then when someone wants to use myModule, they can require it in their file, like so:
There are two obvious benefits to this approach over the module patterns we discussed before:
1. Avoiding global namespace pollution
2. Making our dependencies explicit
Moreover, the syntax is very compact, which I personally love.
Another thing to note is that CommonJS takes a server-first approach and synchronously loads modules. This matters because if we have three other modules we need to require, it’ll load them one by one.
CommonJS is all well and good, but what if we want to load modules asynchronously? The answer is called Asynchronous Module Definition, or AMD for short.
Loading modules using AMD looks something like this:
What’s happening here is that the define function takes as its first argument an array of each of the module’s dependencies. These dependencies are loaded in the background (in a non-blocking manner), and once loaded define calls the callback function it was given.
Next, the callback function takes, as arguments, the dependencies that were loaded — in our case, myModule and myOtherModule — allowing the function to use these dependencies. Finally, the dependencies themselves must also be defined using the define keyword.
For example, myModule might look like this:
So again, unlike CommonJS, AMD takes a browser-first approach alongside asynchronous behavior to get the job done. (Note, there are a lot of people who strongly believe that dynamically loading files piecemeal as you start to run code isn’t favorable, which we’ll explore more when in the next section on module-building).
Aside from asynchronicity, another benefit of AMD is that your modules can be objects, functions, constructors, strings, JSON and many other types, while CommonJS only supports objects as modules.
That being said, AMD isn’t compatible with io, filesystem, and other server-oriented features available via CommonJS, and the function wrapping syntax is a bit more verbose compared to a simple require statement.
For projects that require you to support both AMD and CommonJS features, there’s yet another format: Universal Module Definition (UMD).
UMD essentially creates a way to use either of the two, while also supporting the global variable definition. As a result, UMD modules are capable of working on both client and server.
Here’s a quick taste of how UMD goes about its business:
For more examples of UMD formats, check out this enlightening repo on GitHub.
Fortunately, the smart folks at TC39 (the standards body that defines the syntax and semantics of ECMAScript) have introduced built-in modules with ECMAScript 6 (ES6).
ES6 offers up a variety of possibilities for importing and exporting modules which others have done a great job explaining — here are a few of those resources:
What’s great about ES6 modules relative to CommonJS or AMD is how it manages to offer the best of both worlds: compact and declarative syntaxand asynchronous loading, plus added benefits like better support for cyclic dependencies.
Probably my favorite feature of ES6 modules is that imports are live read-only views of the exports. (Compare this to CommonJS, where imports are copies of exports and consequently not alive).
Here’s an example of how that works:
In this example, we basically make two copies of the module: one when we export it, and one when we require it.
Moreover, the copy in main.js is now disconnected from the original module. That’s why even when we increment our counter it still returns 1 — because the counter variable that we imported is a disconnected copy of the counter variable from the module.
So, incrementing the counter will increment it in the module, but won’t increment your copied version. The only way to modify the copied version of the counter variable is to do so manually:
On the other hand, ES6 creates a live read-only view of the modules we import:
Cool stuff, huh? What I find really compelling about live read-only views is how they allow you to split your modules into smaller pieces without losing functionality.
Then you can turn around and merge them again, no problem. It just “works.”
Looking forward: bundling modules
In the next section I’ll walk through module bundling, covering core topics including:
- Why we bundle modules
- Different approaches to bundling
- ECMAScript’s module loader API