A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.md 18KB

title: The mind-boggling universe of JavaScript Module strategies url: https://www.airpair.com/javascript/posts/the-mind-boggling-universe-of-javascript-modules hash_url: 85169542c2

If you feel like, no matter what you do, there is always something funky in your JavaScript code, I would bet that your Module strategy is not working out so well.

The importance of adopting a proper JavaScript Module strategy is often underestimated as a preference contest, so it is important to truly understand your needs. This article exposes the foundations of different JavaScript Module strategies such as ad hoc, CommonJS, AMD and ES6 modules, and how to get started with ES6 modules right now.

JavaScript Module 101

In brief terms, JavaScript Modules were created in order to apply some classic Object Orientation ideas when building components, once the current JavaScript language support for those ideas isn't as explicit as in other languages as C++, Java and Ruby.

In order to build a module as a special type of object, which strongly needs to leverage encapsulation, we need to add support for declaring private/public attributes and methods inside a single object. Enter the Module pattern, where such encapsulation is achieved through function closures, taking advantage of the function scope to publicly disclose only what is necessary through the return of the function.

Look at the following example for an ad hoc module implementation:

zoo.js:

var Zoo = (function() { 
  var getBarkStyle = function(isHowler) {
    return isHowler? 'woooooow!': 'woof, woof!';
  }; 
  var Dog = function(name, breed) {
    this.bark = function() {
      return name + ': ' + getBarkStyle(breed === 'husky');
    };
  };
  var Wolf = function(name) {
    this.bark = function() {
      return name + ': ' + getBarkStyle(true);
    };
  };
  return {
    Dog: Dog,
    Wolf: Wolf
  };
})();

In zoo.js, we have built the module Zoo which only publicly exposes the functions Dog and Wolf, keeping the function getBarkStyle as private to the module. Precisely, this implementation is a module variation known as the Revealing Module pattern.

Now, let's see how to consume this module:

main.js:

var myDog = new Zoo.Dog('Sherlock', 'beagle');
console.log(myDog.bark()); // Sherlock: woof, woof!

var myWolf = new Zoo.Wolf('Werewolf');
console.log(myWolf.bark()); // Werewolf: woooooow!

In main.js, we are reading the global variable Zoo and instantiating Dog and Wolf from it. Note here that Zoo is also serving as a namespace for its functions, which is encouraged over root-level functions in order to avoid conflicts with other modules defining functions with similar names.

I have put together a full live example of the ad-hoc module. You can also check the source code.

Ok, modules are cool, but why should I use them?

Just to name a few reasons why every JavaScript developer should use modules as much as possible:

  • Writing scattered global JavaScript code is bad for performance, terrible for reusability, causes awkward readability, is painful for side-effects, and horrible for code organization.
  • In JavaScript, literal objects' attributes and methods are all public, making it impossible to conceal internal details of objects, which is a real demand for Components, Features, Subsystems and Façades.
  • A module can be delivered as a dependency for other modules, leveraging a composite architecture of reusable components, when properly implemented.
  • Modules can also be packaged and deployed separately from each other, allowing changes on a particular module to be properly isolated from everything else, therefore mitigating those dreaded side-effects known as the "butterfly effect" (yes, just like the movie).
  • Splitting your global code into modules is the first step on bringing cohesion up and coupling down.

To be or not to be, CommonJS or AMD?

Have another look on the ad hoc module example above. Since we are defining 2 files, we are still writing and reading the variable Zoo into the global JavaScript context. This is definitely not recommended, because it is:

  • fragile (as it is possible for any posterior code to modify/redefine your module),
  • not scalable (if you need to define 100 modules, all of them will be loaded on the global JavaScript context, even if you end up consuming just 1 out of those 100 modules, making it really bad for performance),
  • counter-productive (you have to manually resolve your dependencies, and you would need to remember to bring them altogether if you were to use your module in another application).

A good solution for such setbacks is adopting a Module Loader. As a matter of fact, it would be entirely possible for you to write your own module loader! A simple homemade loader would just need to register modules under aliases, resolve dependencies through Dependency Injection and instantiate the modules through a Factory.

Over the course of time, many developers started to elaborate around modules and module loaders, striving for defining a multi-purpose Module Standard. After some coming and going, two module standards have gained some momentum:

1) CommonJS

CommonJS is a standard for synchronous modules.

Pros:

  • It was adopted as the official module format for Node.js and NPM components. This means that any module defined in CommonJS will have access to the whole NPM ecosystem.
  • It has a simple and convenient syntax.
  • It is possible to guarantee the order of execution of modules.

Cons:

  • It doesn't naturally work on the browser, however there are great solutions for this constraint as Browserify and Webpack.
  • Since it is synchronous, the modules have to be loaded sequentially, which might take more time than if they were to be loaded asynchronously.
  • Usually, NPM modules are composed by many other modules, which means that you might end up depending on a high number of modules, and your bundles can easily get big.

When to use: CommonJS is already a mature standard for the server-side, and a good option for the client-side when the Javascript bundles executed during the page load time are not too big.

How to use: Your module file will publicly expose whatever is assigned to module.exports while everything else is private. In order to use your module, the client code needs to use the require(dependency) function, referencing your module per file location or alias.

Check out the following example:

zoo.js:

var getBarkStyle = function(isHowler) {
  return isHowler? 'woooooow!': 'woof, woof!';
}; 
var Dog = function(name, breed) {
  this.bark = function() {
    return name + ': ' + getBarkStyle(breed === 'husky');
  };
};
var Wolf = function(name) {
  this.bark = function() {
    return name + ': ' + getBarkStyle(true);
  };
};

module.exports = {
  Dog: Dog,
  Wolf: Wolf
};

Note that the public content is returned at once through module.exports.

main.js:

var Zoo = require('./zoo');

var myDog = new Zoo.Dog('Sherlock', 'beagle');
console.log(myDog.bark()); // Sherlock: woof, woof!

var myWolf = new Zoo.Wolf('Werewolf');
console.log(myWolf.bark()); // Werewolf: woooooow!

This code is also available as a live example of a Common.js module. Make sure to check the source code. In this example I am using Browserify to transform the source code on a browser bundle.

2) AMD

AMD (Asynchronous Module Definition) is a standard for asynchronous modules.

Pros:

  • Multiple modules can be loaded in parallel.
  • It naturally works on the browser.
  • It is very convenient to defer the loading of modules that are not necessary on page load.

Cons:

  • Asynchronous loading is a complex subject and it can easily create race conditions if not properly designed.
  • It isn't possible to guarantee the order of execution of asynchronous modules.
  • Its syntax can get hard to understand, specially when the dependencies array is large.

When to use: AMD is specially interesting for client-side applications that can benefit from the lazy loading of modules, when this can be properly leveraged.

How to use: Your module will publicly expose whatever is being returned on the callback function, similarly to our first ad hoc example. In order to use your module, the client code needs to refer to it (per file location or alias) on its dependencies array, which will map to an argument on its own callback function.

The following example uses the quintessential AMD implementation, Require.js, where it declares the zoo module using define(alias, dependenciesArray, callbackFunction) and consumes it later using require(dependenciesArray, callbackFunction):

zoo.js:

define('zoo', [], function() {
  var getBarkStyle = function (isHowler) {
    return isHowler? 'woooooow!': 'woof, woof!';
  }; 
  var Dog = function (name, breed) {
    this.bark = function() {
      return name + ': ' + getBarkStyle(breed === 'husky');
    };
  };
  var Wolf = function (name) {
    this.bark = function() {
      return name + ': ' + getBarkStyle(true);
    };
  };
  return {
    Dog: Dog,
    Wolf: Wolf
  };
});

Note that the public content is returned at once through the function return, just like the ad hoc implementation.

main.js:

require(['zoo'], function(Zoo) {
  var myDog = new Zoo.Dog('Sherlock', 'beagle');
  console.log(myDog.bark()); // Sherlock: woof, woof!

  var myWolf = new Zoo.Wolf('Werewolf');
  console.log(myWolf.bark()); // Werewolf: woooooow!
});

This code is also available as a live example of an AMD module. Make sure to check the source code.

Is there any interop?

As you might guess, you will run into both standards quite frequently, and there will be times you might want to use a CommonJS module with an AMD component and vice-versa. Please allow me to spoil your surprise: these standards are not naturally compatible!

Nevertheless, a number of approaches are there to provide more compatibility between CommonJS and AMD, striving to come up with a way to write your module just once and have it working on both standards. Great interop examples are UMD, SystemJS and uRequire.

AMD aficionados might also consider Almond, a nifty lightweight AMD implementation which natively supports both synchronous and asynchronous loading. It is not really interop, however it is a single solution that benefits from both worlds.

Now, forget about that. ES6 is right around the corner!

Earlier I've affirmed that the JavaScript language support for Modules isn't much explicit on its current version (officially known as ECMAScript 5 or just ES5). However, it turns out that JavaScript Modules have just become explicit!

The upcoming version of JavaScript (ECMAScript 6 or ES6) offers native support for modules in a compact and effective way, quite a bit similar to CommonJS. See how it will look like:

zoo.js:

var getBarkStyle = function(isHowler) {
  return isHowler? 'woooooow!': 'woof, woof!';
}; 
export function Dog(name, breed) {
  this.bark = function() {
    return name + ': ' + getBarkStyle(breed === 'husky');
  };
}
export function Wolf(name) {
  this.bark = function() {
    return name + ': ' + getBarkStyle(true);
  };
}

Note that now we can have more than one export per module. This way, the client code can choose which functions it wants to import from the module:

main.js:

import { Dog, Wolf } from './zoo';

var myDog = new Dog('Sherlock', 'beagle');
console.log(myDog.bark()); // Sherlock: woof, woof!

var myWolf = new Wolf('Werewolf');
console.log(myWolf.bark()); // Werewolf: woooooow!

This code is also available as a live example of an ES6 module. Make sure to check the source code. Not only ES6 has brought modules, but it also brought a solution for the CommonJS vs AMD battle! According to this Dr. Axel Rauschmayer's article, ES6 modules will support both synchronous and asynchronous loading within the same syntax. And even better, they will work both on the browser and on the server!

One more piece of good news: this syntax is finalized, so further syntax changes aren't expected for ES6 Modules. This means that you can start learning it right now!

Babel to the rescue

Using ES6 modules sound thrilling, but since they are targeted for June 2015, what can you do before it gets released and adopted by all the major browsers and devices?

Enter Babel, my personal choice of ES6 to ES5 transpiler (a transpiler is a source-to-source compiler, i.e., it will transform ES6 source code into ES5 source code). Babel works for both client-side and server-side JavaScript.

To implement the example above, I am using the Babelify plugin over Browserify to transform the ES6 source code directly into an ES5 browser bundle. However, it would be just as simple to transpile the files individually, if you are curious to know how the ES5 source would be. You can even choose it to be transpiled to a specific format, like Common.js or AMD! All you would need to do is:

npm install -g babel
babel --modules common zoo.js -o zoo-commonjs.es6
babel --modules amd zoo.js -o zoo-amd.es6

Since in a real project you are hopefully not transpiling files manually, but instead adopting some build system to do that for you, you will be very happy to know that Babel supports most of builders and assets pipelines, and even Rails is on the list! Check out how Babel integrates with your favorite asset pipeline.

Who you gonna call?

Modules are a big deal for sure. All the most successful MV* frameworks rely on a strong module-oriented architecture, such as AngularJS, Ember.js, Marionette.js and React. This is why the JavaScript community is so concerned with helping everyone use them as seamless as possible, as you can see with the ES6 modules initiative.

However, we still have to take into consideration the plethora of existing code which is too big to be rewritten into ES6 anytime soon. Depending on the size, complexity and risk of the project, this might never happen. Still, even if all the major browsers start supporting ES6 this year, it would take a handful of years for most of the worldwide population (or at least your clients) to be effectively using those browser versions supporting ES6. Just for an example [cue the sad-trombone], IE 8 was released exactly 6 years ago.

Anyhow, the other way around is totally feasible and recommended. Writing new code on ES6 with a good transpiler is totally worth taking a deep look at. The benefits are great: more concise syntax, great support for the current Module strategies, and the best thing: you are embracing the future of JavaScript!

When in doubt, remember that Fortune favors the brave. Or just remember Ghostbusters: I ain't afraid of no ghost.