? The mind-boggling universe of JavaScript Module strategies (archive)

Source originale du contenu

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:

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:

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:

Cons:

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:

Cons:

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.