March 30, 2018 · Javascript ES6

ES6 modules and managing dependencies

ES6 modules! What's there to them? They're nothing more than throw-away one liners using built in keywords right? Why should I read an entire blog post about them?

Well, au contraire my friend. The new ES6 module syntax may look very simple, but looks can be deceiving. There's actually incredible depth behind the new ES6 module syntax.

I'll warn you upfront though, the rabbit hole gets pretty deep. But I think you'll be rewarded along the way with a greater appreciation of what it means to write loosely coupled, modular code. Are you down?

What the heck is a module?

At its most basic level, a module is simply a self-contained piece of code. Not too incredibly complicated, right? But if we go beyond the mere surface level, a module does a few more things. Namely, a module defines what other pieces of code it depends on. A module also specifies what particular functionality it provides to other modules. In other words, a module specifies its own dependencies and interface.

Programmers author modules in the same way that novelists compose chapters of a book. The chapter analogy isn't perfect though, since it doesn't quite bring out the reusable nature of modules. A better analogy for that is Lego blocks. Modules are the building blocks of large programs in the same way that single Legos are the building blocks of entire Lego structures.

Why modules are important

Modules are super useful. They allow us to divide a codebase up into logical chunks. This provides several benefits:

  • makes the codebase more readable
  • keeps the codebase in a maintainable state
  • keeps the codebase DRY, since modules can be reused
  • makes for way easier unit testing
  • serves as a convenient tool for namespacing

Have I sold you on the awesomeness of modules yet?

The Problem

As useful as modules are, there used to be no support at all for modules in Javascript. You could try to separate your code out into separate files, but each file would not have its own private scope. In other words, the variables in the topmost scope of each file would still pollute the global namespace. So in the old days, an individual file would not represent a truly self-contained block of code.

Moreover, there was no built-in mechanism for dependency management. If a piece of code depended upon another piece of code, it was up to you to figure out how to orchestrate everything so that it would work.

<body>
    ...
    
    <script src="../path/to/underscore-min.js"></script>
    <script src="../path/to/backbone-min.js"></script>
</body>

For instance, have you ever used a library that depended upon another library in your frontend code? The code above shows what you would have to do to include Backbone.js in your code, which depends upon Underscore.js. The documentation for Backbone.js would tell you that it requires Underscore.js as a dependency. That basically meant that you should include the script for Underscore.js first, and then include the script for Backbone.js after that.

The problem with this is that you're forced to manually resolve dependencies yourself. Furthermore, there's nothing in the code that tells you that Backbone.js depends upon Underscore.js. That information was kept as implicit knowledge in the heads of the developers who wrote the code.

How We Dealed in ES5

Needless to say, the lack of any built-in mechanism for dependency management led to unnecessary mental overhead as well as code that was error prone.

So how did we deal in ES5?

Javascript developers came up with a myriad of different ways to solve this problem. We'll cover a few of the most common solutions in the sections below.

A vanilla Javascript solution

In vanilla Javascript, developers utilized the Revealing Module Pattern in order to improvise their own modules. There are two variations of the Revealing Module Pattern: a singleton variation, and a constructor variation.

Variation 1: Singleton pattern

Revealing Module Patterns of the singleton variety make use of IIFEs, or immediately invoked function execution. This means that all the code is encapsulated within the scope of the immediately invoked function. This creates a sort of ad-hoc scope that's private to the module only.

const module = function() {
  let message = `private message`;
  
  function printMessage() {
    console.log(message);
  }
  
  function updateMessage(newMessage) {
    message = newMessage;
  }
  
  return {
    printMessage, // ES6 shorthand for `printMessage: printMessage`
    updateMessage // ES6 shorthand for `updateMessage: updateMessage`
  };
}();

module.printMessage(); // prints "private message"
module.updateMessage(`private.. but changeable`);
module.printMessage(); // prints "private.. but changeable"

Moreover, when the code is executed, it immediately returns an object literal that serves as the module's interface. In the code above, module's interface is comprised of the methods printMessage and updateMessage. Notice that the implementation details of those methods are encapsulated (abstracted away) within module, which is exactly what we want.

Variation 2: Constructor pattern

The constructor variety of the Revealing Module Pattern is almost exactly the same as the singleton variety. The only difference is that now the function expression isn't immediately invoked. In the constructor variation, the function expression needs to be invoked somewhere else in the code.

const Module = function() {
  let message = `private message`;
  
  function printMessage() {
    console.log(message);
  }
  
  function updateMessage(newMessage) {
    message = newMessage;
  }
  
  return {
    printMessage, // ES6 shorthand for `printMessage: printMessage`
    updateMessage // ES6 shorthand for `updateMessage: updateMessage`
  };
};

const module = new Module(); // this code can actually be anywhere, like in another file
module.printMessage(); // prints "private message"
module.updateMessage(`private.. but changeable`);
module.printMessage(); // prints "private.. but changeable"
Summary

No matter what form it takes, the Revealing Module Pattern achieves true self-containment of code, since each module has its very own scope. Furthermore, the pattern allows modules to specify a public interface that other modules can depend upon.

But the Revealing Module Pattern doesn't give us everything we want though. It doesn't provide any way for a module to declare what its own dependencies are. This means that dependencies still need to be manually resolved by the developer, which is far from ideal.

Usage of 3rd party tools

Javascript developers created 3rd party tools to provide a mechanism for both modularity and dependency management. These 3rd party tools can be categorized by whether they were meant to be used on client side or server side.

But wait! First we need to define some terms. A module format is a convention that specifies special syntax for how to define modules. Module formats go hand in hand with module loaders, which are the 3rd party tools that can interpret a module format correctly. The module loader takes care of actually loading all the dependencies needed to run the code.

Client side tooling

On the client side, many developers made use of AMD & RequireJS. AMD is the name of the module format, and RequireJS is the tool that can correctly interpret AMD syntax.

AMD syntax looks like this:

define(['./d1', './d2'], function(d1, d2) {
  let message = `private message`;

  function printMessage() {
    console.log(message);
  }

  function updateMessage(newMessage) {
    message = newMessage;
  }

  return {
    printMessage,
    updateMessage
  };
});

In AMD syntax, define is a special function used for defining modules. The first parameter of define is an array of the module's dependencies. The second parameter is the function body that defines the module, and this function also takes in its own dependencies as parameters. At the end of the module, an object literal is returned which represents the module's public interface.

RequireJS would then be used to load all the specified dependencies written in AMD syntax. Moreover, RequireJS loads these dependencies asynchronously, which is great for performance in the browser environment.

Server side tooling

On the server side, many developers made use of CommonJS & SystemJS. CommonJS is the name of the module format, and SystemJS is the tool that can correctly interpret CommonJS syntax.

CommonJS syntax looks like this:

let message = `private message`;

function printMessage() {
  console.log(message);
}

function updateMessage(newMessage) {
  message = newMessage;
}

module.exports = {
  printMessage,
  updateMessage
};

We can load the module in another file:

var module = require('./module.js');
module.printMessage(); // prints "private message"

In CommonJS, we define a module's public interface by reassignment of module.exports. Then in another file, we use require to load the module. A call to require just returns the corresponding object referenced by module.exports.

We then use SystemJS to load all the specified dependencies written in CommonJS syntax. Crucially, SystemJS loads modules synchronously, which better suits performance in a server side environment.

*CommonJS syntax is supported natively in Node.js. So you can use CommonJS syntax without having to install any 3rd party module loader. Node.js will automatically be able to interpret the code.

Native ES6 Modules finally!

Phew, we've covered a lot so far! First we went over how developers improvised support for modularity in vanilla Javascript. We then went over the various 3rd party tools created to enable both modularity and dependency management in Javascript.

The good news is that the ES6 standard provides support for modularity and dependency management.

In ES6, modules are first class citizens. Each file represents a module with its own private scope. Modules can then be imported and exported using reserved keywords in ES6.

ES6 module syntax looks like this:

const Module = function() {
  let message = `private message`;
  
  function printMessage() {
    console.log(message);
  }
  
  function updateMessage(newMessage) {
    message = newMessage;
  }
  
  return {
    printMessage, // ES6 shorthand for `printMessage: printMessage`
    updateMessage // ES6 shorthand for `updateMessage: updateMessage`
  };
};

export default Module;

In another file:

import Module from './module.js';

const module = new Module();
module.printMessage(); // prints "private message"
module.updateMessage(`private.. but changeable`);
module.printMessage(); // prints "private.. but changeable"

We use the keyword export to export a module's public interface. And we use the keyword import to import the dependencies that a module depends upon.

Benefits of ES6 Modules:

  • define a cross platform standard that will work no matter what environment the code runs in
  • make it crystal clear what a module needs in order to run, since dependencies are defined at the beginning of the file
  • allow for certain performance optimizations, such as tree shaking, since modules can be statically analyzed (i.e. imports and exports are resolved at compile time rather than at runtime)
  • provide better support for cyclic dependencies than prior solutions
  • a file encapsulates its own private scope
  • compact and declarative syntax

Summary

In designing the new standard, the creators of the ES6 module system took inspiration from the AMD and CommonJS systems. They wanted to combine the best of both worlds. And by all accounts, they have. ES6 modules are the definitive standard. They're the future of Javascript.

As of the time of my writing this article however, native support for interpreting ES6 module syntax is far from complete. But you can (and arguably should!) take advantage of the new module syntax today, via various transpilers. Some top choices include Traceur Compiler, Babel, and Rollup.

Still unsure of how to get started? This article won't go into all the nitty gritty details of every syntactic variation you can utilize with ES6 modules. But such information can easily be found on MDN.

That's it!

Phew, congrats if you made it this far! When I first started doing research to write this article, I really had no idea how deep the rabbit hole could go. There is a ton of history behind the incredibly succinct and simple syntax of ES6 modules. And this article really only touched the tip of the iceberg.

I learned a ton though, and I hope you feel the same way.

I truly hope that by becoming aware of this complicated history, you'll gain a greater appreciation of the beauty and practicality of modular Javascript 🌹. Cheers!