ES2015 was officially released in June 2015, which finally introduced native support for modules. Nowadays, modular JS development is very convenient and natural, but this new specification only lasted 3 years. Just seven years ago, JS modularity was stuck in runtime support; Thirteen years ago, module dependencies were defined through back-end template definitions and annotations. For those who lived through it, the modularized way of history lingers.

Why modularity is needed

A module is a program or subroutine required to complete a function. A module is a “single responsibility” and “replaceable” part of a system. Modularity refers to the division of system code into a series of replaceable modules with a single responsibility. Modular development refers to how to develop new modules and reuse existing modules to achieve application functions.

So why modularity? The main reasons are as follows:

  • Traditional Web development is shifting to Web Apps development
  • Code complexity is increasing, and as Web capabilities increase, more and more business logic and interactions are implemented in the Web layer
  • Developers want separate JS files/modules for easy maintenance of subsequent code
  • When you deploy, you want to optimize your code into a few HTTP requests

The evolution of modularity

Defining dependencies directly (1999)

The first attempt to introduce modularity into JS was in 1999, called “direct definition dependency”. This approach to modularity is straightforward — modules are defined and referenced through global methods. Dojo organizes modules in this way, using dojo.provide to define a module and dojo.require to call modules defined elsewhere.

Let’s change the example in Dojo 1.6:

/ / the greeting. Js file
dojo.provide("app.greeting");

app.greeting.helloInLang = {
    en: 'Hello world! '.es: '¡ Hola mundo! '.ru: 'П р и kind guide е т м и р! '
};

app.greeting.sayHello = function (lang) {
    return app.greeting.helloInLang[lang];
};

/ / hello. Js file
dojo.provide("app.hello");

dojo.require('app.greeting');

app.hello = function(x) {
    document.write(app.greeting.sayHello('es'));
};
Copy the code

The way to define dependencies directly is very similar to CommonJS, except that modules can be defined in any file and are not associated with a file. In CommonJS, each file is a module.

Namespace Mode (2002)

Originally, we wrote code like this:

function foo() {}function bar() {}Copy the code

Many variables and functions are declared directly under the global scope, which can easily cause naming conflicts. Thus, the namespace pattern was proposed. Such as:


var app = {};

app.foo = function() {

}

app.bar = function() {

}

app.foo();
Copy the code

Anonymous Closure IIFE Pattern (2003)

Although the Namespace pattern solves the problem of variable contamination on the global namespace to some extent, it does not solve the problem of code and data isolation.

The first solution to this problem is the anonymous closure IIFE pattern. At its core, it encapsulates data and code and provides external access to them. A basic example is as follows:


var greeting = (function () {
    var module = {};

    var helloInLang = {
        en: 'Hello world! '.es: '¡ Hola mundo! '.ru: 'П р и kind guide е т м и р! '
    };

    module.getHello = function (lang) {
        return helloInLang[lang];
    };

    module.writeHello = function (lang) {
        document.write(module.getHello(lang))
    };
    
    return module; } ());Copy the code

Template dependency Definitions (2006)

The template dependency definition is the next solution to the modularity problem. It needs to be used in conjunction with the template syntax on the back end, which aggregates JS files for dependency loading.

It was originally used in the Prototype 1.4 library. Back in 2005, Sam Stephenson started developing the Prototype library as part of Ruby on Rails. Since Sam uses Ruby a lot, it’s no wonder he chose to use erB templates in Ruby as dependency management for JS modules.

The template dependency definition is specified by special tag syntax in the header of a JS file that depends on other JS files. These tag syntax can be parsed by back-end templates (ERB, Jinjia, Smarty) and identified by specialized build tools such as Borshik. It should be noted that this mode only works during the precompilation phase.

Here is an example using Borshik:


/ / app. TMP. Js file

/*borschik:include:.. /lib/main.js*/

/*borschik:include:.. /lib/helloInLang.js*/

/*borschik:include:.. /lib/writeHello.js*/

/ / the main js file
var app = {};

/ / helloInLang. Js file
app.helloInLang = {
    en: 'Hello world! '.es: '¡ Hola mundo! '.ru: 'П р и kind guide е т м и р! '
};

/ / writeHello. Js file
app.writeHello = function (lang) {
    document.write(app.helloInLang[lang]);
};

Copy the code

Annotations Define dependencies (2006)

The annotation definition dependency is very similar to the direct definition dependency in 1999, except that the annotation definition dependency defines modules on a file basis. Dependencies between modules are defined through annotation syntax.

An application that wants to do this can either precompile (Mootools) or dynamically parse the downloaded code at runtime (LazyJS).

/ / helloInLang. Js file
var helloInLang = {
    en: 'Hello world! '.es: '¡ Hola mundo! '.ru: 'П р и kind guide е т м и р! '
};

/ / sayHello. Js file

/ *! lazy require scripts/app/helloInLang.js */

function sayHello(lang) {
    return helloInLang[lang];
}

/ / hello. Js file

/ *! lazy require scripts/app/sayHello.js */

document.write(sayHello('en'));
Copy the code

Dependency Injection (2009)

In 2004, Martin Fowler introduced the concept of dependency injection to describe the problem of communication between components in Java.

Five years later, former Sun and Adobe employee Miško Hevery (a JAVA programmer) began designing a JS framework for his startup, which used the idea of dependency injection to solve the problem of communication between components. As you all know, his project was acquired by Google and Angular became one of the most popular JS frameworks.

/ / the greeting. Js file
angular.module('greeter', [])
    .value('greeting', {
        helloInLang: {
            en: 'Hello world! '.es: '¡ Hola mundo! '.ru: 'П р и kind guide е т м и р! '
        },

        sayHello: function(lang) {
            return this.helloInLang[lang]; }});/ / app. Js file
angular.module('app'['greeter'])
    .controller('GreetingController'['$scope'.'greeting'.function($scope, greeting) {
        $scope.phrase = greeting.sayHello('en');
    }]);
Copy the code

CommonJS model (2009).

The CommonJS (or CJS) specification came out in 2009 and was finally implemented in Node.js.

/ / the greeting. Js file
var helloInLang = {
    en: 'Hello world! '.es: '¡ Hola mundo! '.ru: 'П р и kind guide е т м и р! '
};

var sayHello = function (lang) {
    return helloInLang[lang];
}

module.exports.sayHello = sayHello;

/ / hello. Js file
var sayHello = require('./lib/greeting').sayHello;
var phrase = sayHello('en');

console.log(phrase);
Copy the code

Here we find that in order to implement the CommonJS specification we need two new entries — require and Module, which provide the ability to load modules and expose interfaces to the outside world.

It is worth noting that both require and module are language keywords. In Node.js, we can use it because it’s accessibility. Before sending modules in Node.js to V8, they are wrapped with the following function:

(function (exports, require, module, __filename, __dirname) {
    // ...
    // The module code is here
    // ...
});
Copy the code

The CommonJS specification is by far the most common module format specification. You can use it not only in Node.js, but also in browsers with Browserfiy or Webpack.

AMD pattern (2009).

CommonJS is in full swing, and there is a growing discussion of asynchronous loading of modules. The main purpose is to solve the dynamic loading dependency of Web applications, which is smaller than CommonJS.

/ / lib/greeting. Js file
define(function() {
    var helloInLang = {
        en: 'Hello world! '.es: '¡ Hola mundo! '.ru: 'П р и kind guide е т м и р! '
    };

    return {
        sayHello: function (lang) {
            returnhelloInLang[lang]; }}; });/ / hello. Js file
define(['./lib/greeting'].function(greeting) {
    var phrase = greeting.sayHello('en');
    document.write(phrase);
});
Copy the code

While AMD’s model is well suited for browser-side development, it may be phased out as the NPM package management mechanism becomes more popular.

ES2015 Modules(2015)

ES2015 Modules (ES Modules for short) is the modularity scheme we currently use. It has been supported natively in Node.js 9 and can be used by starting with flag –experimental-modules, without relying on Babel and other tools. Currently not implemented in browsers, front-end projects can be experienced in advance using Babel or typescript.

/ / lib/greeting. Js file
const helloInLang = {
    en: 'Hello world! '.es: '¡ Hola mundo! '.ru: 'П р и kind guide е т м и р! '
};

export const greeting = {
    sayHello: function (lang) {
        returnhelloInLang[lang]; }};/ / hello. Js file
import { greeting } from "./lib/greeting";
const phrase = greeting.sayHello("en");
document.write(phrase);
Copy the code

conclusion

JS modularization evolution process let people sigh, thanks to TC 39 support for ES modules, so that JS modularization process can finally come to an end, I hope that in a few years all mainstream browsers can support ES modules natively.

On the other hand, the evolution of JS modularization also shows that the ability of Web is increasing, and Web applications are becoming more and more complex. We believe that the future JS ability to further improve, our development efficiency will be more efficient.


IVWEB Technology Weekly shocked online, pay attention to the public number: IVWEB community, weekly timing push quality articles.