With the advent of the Internet era, front-end technologies are being updated at a faster and faster pace. Initially, a few lines of code embedded in script tags were enough to implement some basic user interaction, but now with the development of Ajax, jQuery, MVC and MVVM, the amount of Javascript code has become increasingly large and complex.

Web pages are becoming more and more like desktop programs, requiring a team to divide and collaborate, progress management, unit testing, etc. Developers have to use software engineering methods to manage the business logic of web pages, and Javascript modular development has become an urgent need. Ideally, developers would only need to implement the core business logic, and the rest could be loaded with modules already written by others.

However, Javascript is not a modular programming language, and it does not support classes, let alone modules. It was not until ES6 was finalized that Javascript began to officially support classes and modules, but it will be a long time before they are fully operational.

What is modularity

Modules are an integral part of any large application architecture. A module is a block of code or file that implements a specific function. Modules allow us to clearly separate and organize the units of code in a project. In project development, by removing dependencies and loosely coupling the application can be made more maintainable. With modules, developers can more easily use other people’s code and load whatever functionality they want. Module development needs to follow certain specifications, otherwise it will be chaotic.

The Javascript community has made a lot of efforts to implement “modules” into existing runtime environments. This article summarizes current best practices for modular programming in Javascript and shows how to put them to use.

Javascript Modularization Basic Writing Method

In the first part, modular writing based on traditional Javascript syntax will be discussed.

Primitive Writing

A module is a set of methods that implement a specific function. Simply putting different functions (and variables that record state) together is considered a module.

1
2
3
4
5
6
7
function func1(){
    //...
}

function func2(){
    //...
}

The above functions func1() and func2() make up a module. When you use them, you can just call them directly. The disadvantages of this approach are obvious: it “pollutes” the global variables, it does not guarantee that there are no variable name conflicts with other modules, and there is no direct relationship between module members.

Object writing method

To solve the above drawback, you can write the module as an object, and all the module members are put inside this object.

1
2
3
4
5
6
7
8
9
var moduleA = new Object({
    _count : 0,
    func1 : function (){
        //...
    },
    func2 : function (){
        //...
    }
});

The above functions func1() and func2() are encapsulated in the moduleA object, and when used, they call the properties of this object.

1
moduleA.func1();

However, this way of writing exposes that all module members, internal state can be rewritten externally. For example, external code can directly change the value of the internal counter.

1
moduleA._count = 3;

Immediately-Invoked Function Expression

Using Immediately-Invoked Function Expression (IIFE), you can achieve the goal of not exposing private members.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var moduleA =  (function(){
    var _count = 0;
    var func1 = function(){
        //...
    };
    var func2 = function(){
        //...
    };
    return {
        func1 : func1,
        func2 : func2
    };
})();

With the above writeup, external code cannot read the internal _count variable.

1
console.log(moduleA._count);    //undefined

moduleA is the basic way to write a Javascript module. Next, this writing style is reworked.

Augmentation mode

If a module is large and must be divided into several parts, or if a module needs to inherit from another module, it is necessary to use the “augmentation mode” (augmentation).

1
2
3
4
5
6
var moduleA = (function (mod){
    mod.func3 = function () {
        //...
    };
    return mod;
})(moduleA);

The above code adds a new method func3() to the moduleA module, and then returns the new moduleA` module.

Loose augmentation mode

In a browser environment, the various parts of a module are usually fetched from the web, and sometimes it is impossible to know which part will be loaded first. If the previous section is written, the first part to be executed may load a non-existent empty object, and then “Loose augmentation mode” is used.

1
2
3
4
var moduleA = ( function (mod){
    //...
    return mod;
})(window.moduleA || {});

In contrast to the “augmentation mode”, the “Loose augmentation mode” means that the parameters of the “Immediately-Invoked Function” can be empty objects.

Enter global variables

Independence is an important feature of modules, and it is desirable that modules do not interact directly with other parts of the program internally. In order to call global variables inside a module, other variables must be explicitly entered into the module.

1
2
3
var moduleA = (function ($, YAHOO) {
    //...
})(jQuery, YAHOO);

The moduleA module above requires the use of the jQuery library and the YUI library, so the two libraries (actually two modules) are entered as parameters to moduleA. In addition to ensuring module independence, this also makes the dependencies between the modules obvious. For more discussion on this, see Ben Cherry’s famous article “JavaScript Module Pattern: In-Depth”.

Specification-Based Javascript Modularization

In order for developers to all write modules in the same way, a specification for modules must be developed. While there is no official specification for Javascript modules to date, there are two common specifications for Javascript modules: CommonJS and AMD.

CommonJS

In 2009, American programmer Ryan Dahl created the node.js project to use the Javascript language for server-side programming. This marked the birth of “Javascript modular programming”. Because in a browser environment, the lack of modules is not a particularly big problem, after all, web programs are limited in complexity; but on the server side, there must be modules that interact with the operating system and other applications, otherwise there is no way to program.

The module system of node.js is implemented in reference to the CommonJS specification. According to the CommonJS specification, a single file is a module. Each module is a separate scope, which means that variables defined inside that module cannot be read by other modules unless they are defined as properties of a global object. The best way to export module variables is to use the module.exports object.

1
2
3
4
5
6
7
8
9
var i = 1;
var max = 30;

module.exports = function () {
    for (i -= 1; i++ < max; ) {
        console.log(i);
    }
    max *= 1.1;
};

The above code defines a function that bridges the communication between the outside of the module and the inside, using the module.exports object. The module is loaded using the require method, which reads a file, executes it, and returns the module.exports object inside the file.

Once you have a server-side module, it is natural to want a client-side module. And ideally the two should be compatible, so that a module can run in both the server and the browser without modification. But here’s the problem.

1
2
var math = require('math');
math.add(2, 3);

The second line, math.add(2, 3), runs after the first line, require('math'), so it must wait for math.js to finish loading. That is, if it takes a long time to load, the whole application will just stop there and wait. This is not a problem for the server side, because all modules are stored on the local hard disk and can be loaded simultaneously, and the waiting time is the read time of the hard disk. However, for the browser, this is a big problem, because the modules are placed on the server side, the waiting time depends on the speed of the network, it may take a long time to wait, the browser is in a “false death” state. Therefore, the browser side of the module, you can not use “synchronous loading”, only “asynchronous loading”. This is the background of the birth of the AMD specification.

AMD

AMD stands for “Asynchronous Module Definition”, which means “Asynchronous Module Definition”. It loads the module asynchronously, so that the loading of the module does not affect the execution of the statements that follow it. All statements that depend on the module are defined in a callback function, which will not run until the loading is complete.

AMD also uses the require() statement to load modules, but unlike CommonJS, it requires two arguments.

1
require([module], callback);

The first parameter module is an array whose members are the modules to be loaded; the second parameter callback is the callback function after the successful loading. If you rewrite the previous code in AMD form, it would look like this.

1
2
3
require(['math'], function (math) {
    math.add(2, 3);
});

math.add() is not synchronized with the loading of the math module and no false death occurs in the browser. So it is clear that AMD is better suited to the browser environment. Currently, there are two main Javascript libraries that implement the AMD specification: require.js and curl.js.

AMD is a specification for modular development on the browser side. Because it is not natively supported by JavaScript, page development using the AMD specification requires the use of a corresponding library function, the famous requireJS, which is actually the output of the specification of module definitions during the promotion of requireJS. requireJS was created to requireJS was created to solve two problems.

  1. multiple JavaScript files may have dependencies, and the dependent file needs to be loaded into the browser before the file that depends on it
  2. the browser stops rendering the page when JavaScript is loaded, and the more files are loaded, the longer the page loses response time

Let’s start with an example.

Define the module: (moduleA.js)

1
2
3
4
5
6
7
8
9
// define moduleA.js
define(['myLib'], function(myLib) {
    function foo() {
        myLib.doSomething();
    }
    return {
        foo : foo
    };
});

Load the module and call.

1
2
3
4
// load moduleA
require(['moduleA'], function (m) {
    m.foo();
}

Syntax of requireJS.

requireJS defines a function define, which is a global variable used to define the module.

1
define(id?, dependencies?, factory);
  • id is an optional parameter that defines the module’s identity, and if it is not provided, the script filename (minus the extension)
  • dependencies is an array of module names that the current module depends on
  • factory is a factory method where the module initializes the function or object to be executed. If a function, it should be executed only once. If it is an object, this object should be the output value of the module

Load the module on the page using the require function.

1
require([dependencies], function(){});

The require() function accepts two arguments.

  • the first argument is an array indicating the dependent modules
  • The second argument is a callback function that will be called when all the previously specified modules have been loaded successfully. The loaded modules are passed into the function as arguments, so that they can be used inside the callback function

The require() function loads dependent functions asynchronously, so that the browser does not lose response. Also it specifies a callback function that will run only after all the previous modules have been loaded successfully, solving the dependency problem.

CMD

CMD (Common Module Definition) is a specification that specifies the basic writing format and basic interaction rules for modules. This specification was developed in China. AMD is dependency front-loading, CMD is on-demand loading.

The CMD specification was developed in China, just like AMD has requireJS, CMD has a browser implementation of SeaJS, which solves the same problem as requireJS, except in the way modules are defined and loaded (so to speak, run, parse). SeaJS solves the same problem as requireJS, except that it differs in the way modules are defined and the timing of module loading (running, parsing, if you will).

Sea.js promotes one module, one file, and follows a uniform writing style

Module definition.

1
define(id?, deps?, factory);

Because the CMD specification promotes a file as a module, the file name is often used as the module id. CMD advocates proximity of dependencies, so dependencies are generally not written in the define argument. factory is the constructor of the module. This constructor is executed to get the interface that the module provides to the outside. The factory method is executed with three parameters by default: require, exports and module.

1
2
3
define(function(require, exports, module) {
    // module code...
});
  1. require is the first parameter of the factory function, a method that accepts the module identifier as the only parameter to get the interfaces provided by other modules.
  2. exports is an object that is used to provide the module interface to the outside.
  3. module is an object that stores some properties and methods associated with the current module.

The following is an example of a CMD-compliant specification.

1
2
3
4
5
// define moduleA.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    $('div').addClass('active');
});
1
2
3
4
// load module
seajs.use(['moduleA.js'], function(m) {
    // ...
});

AMD vs CMD Differences

The two specifications were originally developed for different purposes.

AMD is the output of the specification of module definitions made by RequireJS during the rollout CMD is the output of the specification of module definitions made by SeaJS during the rollout

For dependent modules, AMD preloads and CMD defers loading.

AMD promotes dependency preloading, declaring the dependent modules when defining the module CMD promotes proximity dependency, loading a module only when it is used

This difference has its advantages and disadvantages, it’s just a syntax difference, and both requireJS and SeaJS support each other’s writing style.

The biggest difference between AMD and CMD is that the timing of execution of dependent modules is handled differently, note that it is not the timing or manner of loading that is different.

Many people say that requireJS loads modules asynchronously and SeaJS loads them synchronously, but this understanding is actually inaccurate. In fact, both specifications load dependent modules asynchronously, except that AMD is “dependency-front”, where the factory can easily know what dependent modules are available, and CMD is “dependency-near”. You need to parse the module into a string when you use it to know which modules depend on it. This is one of the things that many people criticize about CMD, sacrificing performance to bring convenience to development, when in fact parsing modules takes so little time that it can be ignored.

Again, the modules are loaded asynchronously. AMD will execute the module after loading the module dependencies, and after all modules are loaded and executed it will enter the require callback function and execute the main logic. The effect of this is that the order of execution of the dependencies and the order in which they are written are not necessarily the same, depending on the speed at which the modules are loaded and which one is executed first, but the main logic must be executed after all the dependencies have been loaded.

CMD does not execute a dependency module after it is loaded, it just downloads it. After all the dependency modules are loaded, it enters the main logic and executes the corresponding module only when it encounters the require statement, so that the execution order of the modules and the writing order are exactly the same.

This is the reason why many people say that AMD has a good user experience because there is no delay and the dependent modules are executed in advance, and CMD has a good performance because it is executed only when the user needs it.

Future-proofing the ES6 modularity standard

Since the call for modular development is so high, the official ECMA must do something about it. In fact, ECMA has included modularity in the draft for a long time, and finally the standard specification for modularity was included in the ES6 official release in June 2015. However, probably due to the immaturity of the technology involved, ES6 removes the content about how modules are loaded/executed and only retains the syntax for defining and introducing modules, which requires no special work to define a module, since the role of a module is to provide APIs to the public. The module can be defined with the module keyword, and the API that the module needs to provide to the public can be exported with exoprt.

1
2
3
4
// Method 1: 
export var a = 1;
export var obj = {name: 'abc', age: 20};
export function run() {....}
1
2
3
4
5
// Method 2:
var a = 1;
var obj = {name: 'abc', age: 20};
function run() {....}
export {a, obj, run}
1
2
3
4
5
6
7
// Method 3:
module math {
    export function sum(x, y) {
        return x + y;
    }
    export var pi = 3.141593;
}

Use the import keyword to load external modules.

1
2
3
// we can import in script code, not just inside a module
import {sum, pi} from math;
alert("2π = " + sum(pi, pi));
1
2
3
// import everything
import * from math;
alert("2π = " + sum(pi, pi));
1
2
3
4
5
// import part of module
import {run as go} from  'a';
import { draw: drawShape } from shape;
run();
drawShape();
1
2
3
4
5
6
7
// nested module
module widgets {
    export module button { ... }
    export module alert { ... }
    export module textarea { ... }
    ...
}

The module requested from the server.

1
2
3
4
<script type="harmony">
// loading from a URL
module JSON at 'http://json.org/modules/json2.js';
alert(JSON.stringify({'hi': 'world'}));

Dynamic loading of a module.

1
2
3
Loader.load('http://json.org/modules/json2.js', function(JSON) {
    alert(JSON.stringify([0, {a: true}]));
});

This is the basic usage of ES6 Module, which is really weak and needs a long time to be standardized, so it is a long way to go. It also has a problem that the new syntax keywords are not backward compatible (e.g. lower versions of IE browsers). Currently we can use some third-party modules to compile ES6 into working ES5 code, or AMD-compliant modules, such as the ES6 module transpiler. There is also a project that provides a way to load ES6 modules, such as es6-module-loader, but these are temporary solutions, so maybe once ES7 is released, there will be a standard for module loading, and browsers will be able to implement it, so there will be no use for these tools. The future is still worth looking forward to, from the language standard support modularity, Javascript can be more confident into large-scale enterprise-level development.