How to Write Highly Scalable and Maintainable JavaScript: Modules

Originally published at innoarchitech.com here on September 16, 2014.

Articles in This Series

  1. The “Wild West” Syndrome
  2. Namespacing
  3. Modules
  4. Coupling

Introduction

So far we’ve covered the so-called “wild west” syndrome and some reasons for needing better organized JavaScript code bases, as well as namespacing as one strategy to achieve this goal. We ended the last chapter by mentioning native and non-native JavaScript module formats and specifications. This chapter is a deep dive into JavaScript modules.

If a method or function is a distinct unit of functionality, then modules should be thought of as a distinct grouping of units of similar functionality. JavaScript modules are somewhat synonymous to components in many server-side frameworks.

Using a classical analogy, a class should abide by the single responsibility principle from SOLID. Likewise, a module should also have a single responsibility, but In this case the single responsibility is that a given module is a one-stop-shop for handling related tasks.

Returning to our error handling module discussion from a previous chapter, this module may have many distinct units of functionality (i.e., functions), but they should all be related to handling errors. In addition, the module should not contain any functionality not required for handling errors, and it should implement any and all error handling requirements for a given application.

This error handling module should be consumable by your application, or other modules as required given its cross-cutting nature. Even better, it could be loosely coupled from the rest of your application through an event-driven pattern like Pub/Sub, which we will cover in a later chapter.

For now, let’s take a closer look at actual JavaScript module implementations.

Vanilla JavaScript Modules

Modules are relatively easy to implement in plain JavaScript, and there are a variety of patterns at your disposal. Each pattern arguably has its pluses and minuses. We’ll cover some of these patterns here, but leave out the opinions on which is most suitable for a given implementation.

Most of the time it boils down to your preference and what works best for your particular situation. There is no shortage of very strong opinions online about what module implementations are best, so feel free to search around a bit if you’re still unsure.

Let’s begin.

A JavaScript module can be as simple as an plain JavaScript object, where the properties (i.e., functions) of the object are the individual units of functionality. Note that without a closure, this module example would wind up polluting the global namespace.

Example: POJO module pattern

var myModule = {
propertyEx: “This is a property on myModule”,
functionEx: function(){
// insert functionality here
}
};

In order to create a closure and ensure all variables and functions are local to the module’s scope, practitioners will often wrap the module with an IIFE (immediately-invoked function expression).

Example: Scoped module pattern

var myModule = (function () {
var module;
    module.varProperty = “This is a property on myModule”;
module.funcProperty = function(){
// insert code here
};
    return module;
})();

Another pattern is the module pattern itself, along with the so-called revealing module pattern. The revealing module pattern is a special case of the module pattern that implements a form of hidden private members, and thus exposes a public interface. Here is an example of the revealing module pattern.

Example: Revealing module pattern

var myRevealingModule = (function () {
    var privateVar = “Alex Castrounis”,
publicVar = “Hi!”;
    function privateFunction() {
console.log( “Name:” + privateVar );
}
    function publicSetName( strName ) {
privateVar = strName;
}
    function publicGetName() {
privateFunction();
}
    return {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
})();

The last vanilla JavaScript pattern that we’ll cover is the prototype pattern. It’s very similar to the module patterns described above, but embraces the prototypal nature of JavaScript. Here is an example of a module based on the prototype pattern with private members.

Example: Prototype pattern

var myPrototypeModule = (function (){

var privateVar = "Alex Castrounis",
count = 0;

function PrototypeModule(name){
this.name = name;
}

function privateFunction() {
console.log( "Name:" + privateVar );
count++;
}

PrototypeModule.prototype.setName = function(strName){
this.name = strName;
};

PrototypeModule.prototype.getName = function(){
privateFunction();
};

return PrototypeModule;
})();

For a more detailed view of different JavaScript module patterns and implementations, including global importing, I highly recommend reading this article by Ben Cherry. In addition, Addy Osmani’s book on JavaScript design patterns is a must read for anyone seriously interested in the topic.

Module Formats and Specifications

While there’s nothing technically wrong with implementing vanilla JavaScript modules, there are some things worth considering. The first is the inconsistency of plain JavaScript module implementations due to the lack of standardization and variety of patterns used (recall the “Wild West” syndrome). The other is that the native JavaScript approach does nothing to account for asynchronous and parallel module loading, dependency management between modules, optimization, and so on.

In order to address these concerns, a variety of module formats and specifications have surfaced that are intended to promote the following: standardization and consistency, encapsulation, asynchronous and parallel script loading, increased application performance, improved readability and code cleanliness, reduction of HTML script tags, and dependency management. The three biggest players are AMD (Asynchronous Module Definition), CommonJS, and the ECMAScript Harmony module specification.

Let’s examine each of these options individually and at a high level. Note again that this series is intended to be an overview of available choices, architectural considerations, and best practices, but not to give strong opinions on which options are best. For additional information on these topics and options, refer to the ‘Resources’ section at the end of this chapter.

Asynchronous Module Definition API (AMD)

Asynchronous Module Definition, or AMD, is a specification for defining modules and their dependencies, which can be loaded asynchronously by the browser. The Require.js file and module loader by James Burke is likely the most popular framework based on the AMD specification, but is not the only AMD implementation available.

Require.js is highly configurable, customizable, very well implemented, and widely used. It uses the AMD syntax, which specifies the define and require functions. Common complaints about Require.js and AMD in general are that it can be fairly complicated to configure, and the syntax is not exactly terse. Also, Require.js is more of a client-side (a.k.a. browser-based) module implementation framework by design.

Despite these complaints, there are certainly some upsides to embracing AMD and Require.js. The first is that Require.js is very good at what it does. Assuming that you have everything set up properly, you can be assured that your modules and their dependencies will be loaded asynchronously and in whatever order specified by your configuration.

The other advantage of using Require.js is that it has a built-in optimizer that can be used to combine and minify scripts (via UglifyJS or Google’s Closure Compiler), as well as inlining and optimization of CSS files via the @imports directive and comment removal.

CommonJS

CommonJS is another module specification that is widely used, and specifies the exports and require functions. It’s a module format intended to be used by both browsers and servers alike, although it’s most commonly implemented in server-side frameworks like Node.js.

Browserify is a very popular client-side CommonJS module framework. You can implement a full stack CommonJS module solution when combining Browserify on the client, and Node.js on the server for example.

Some common complaints of the CommonJS specification are reliance on server-side tools and build processes, being too server-driven in general, cross-domain issues, and lack of transport format. Advantages of CommonJS include being relatively easy to setup and configure, terse syntax and implementation, easy to read, diverse, and can be implemented across the entire stack. It is also able to handle cyclical dependencies very well.

ECMAScript 6 Harmony

ECMAScript is a scripting language specification upon which the JavaScript language itself is based, and is defined by the ECMA-262 standard. The maintenance and standardization of the specification is the responsibility of ECMA International and the TC39 committee.

The current edition of the ECMAScript standard is 5.1, although ECMAScript 6 (also known as Harmony and ES.next) is the next edition of the standard to be officially published.

Whenever new features and modifications are introduced to the officially published ECMAScript specification, the JavaScript language itself, and engines that interpret and execute it change accordingly (at least in theory).

The obvious benefit of a native module implementation in JavaScript is that it’s, well, native! According to Axel Rauschmayer, ES6 modules are more compact, benefit from static analysis and module structure, implement improved cyclical dependency handling, standardize on a single module specification, eliminate globals, and reduce or eliminate dependency of object-based namespaces.

ES6 modules consist of a declarative importing and exporting syntax, and programmatic loader API for module loading and dependency management. Unlike AMD and CommonJS, which implement a few very key functions (define, require, and exports), ES6 modules are keyword driven. The keywords import and export are the primary players. When an exported module member has a name declared, it becomes a named export.

In addition to named exports, ES6 defines a default export, which is meant to represent the single and most important exported value per module. The value can be whatever you want (function, object, etc.), but the default export is the key exported item for a given module. Note that a module can have a default export along with named exports.

Additional features of ES6 modules include support for both synchronous and asynchronous loading, as well as a programmatic and configurable module loader API. Note that ES6 modules are not implemented yet by all major browsers and may require a transpiler to use now. This link is a great resource for determining browser compatibility for ES6 and ES7 features.

Summary

In this chapter, we’ve examined plain JavaScript module implementations, as well explored the three dominant module formats and specifications. The upcoming ECMAScript Harmony module specification is certainly exciting, but is still in its infancy.

I highly recommend choosing a module pattern or specification that makes the most sense for you. Once you have settled on one, be sure to use it consistently throughout your application. Consistency is a key component to readability and maintainability!

In the next chapter, we will discuss patterns used to minimize coupling between modules, which can heavily contribute to an application’s scalability, reusability, reliability, and maintainability.

About the Author: Alex Castrounis founded InnoArchiTech. Sign up for the InnoArchiTech newsletter and follow InnoArchiTech on Twitter at @innoarchitech for the latest content updates.