preface

Since the birth of JavaScript language, module standardization road twists and turns. A variety of solutions emerged in the community, including AMD, CMD, CommonJS, etc., and then ECMA added module functionality (as it was introduced in ES2015, it will be called ES6 Module below) at the level of the JavaScript language standard. Today we are going to talk about why these different module specifications come into being and what problems they solve at historical points.

What is modularity?

Or according to the function, or according to the data, or according to the business, a large program divided into interdependent small files, and then put together in a simple way.

The global variable

The demo project

To better understand each module specification, add a simple project for demonstration.

# Project contents:├ ─ js# js folder│ ├ ─ main. Js# entry│ ├ ─ config. Js# Project configuration│ └ ─ utils. Js# tools└ ─ index. HTML# HTML page
Copy the code

Window

In the primitive society of slash-and-burn, communication between JS files depended almost entirely on Window objects (except with HTML, CSS, or backend).

// config.js
var api = 'https://github.com/ronffy';
var config = {
  api: api,
}
Copy the code
// utils.js
var utils = {
  request() {
    console.log(window.config.api); }}Copy the code
// main.js
window.utils.request();
Copy the code
<! -- index.html -->
<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Mr. Petty thief: JS module specification evolution</title>
</head>
<body>

  <! -- All script tags must be in the correct order, otherwise an error will be reported -->
  <script src="./js/config.js"></script>
  <script src="./js/utils.js"></script>
  <script src="./js/main.js"></script>
</body>
</html>
Copy the code

IIFE

In the browser environment, variables declared in the global scope are global variables. Global variables have many problems such as naming conflicts, memory usage that cannot be reclaimed, and low code readability.

At this point, IIFE (execute function now anonymously) appears:

; (function () {
  ...
}());
Copy the code

Reconstructing config.js with IIFE:

; (function (root) {
  var api = 'https://github.com/ronffy';
  var config = {
    api: api, }; root.config = config; } (window));
Copy the code

With the advent of IIFE, the number of declarations of global variables has been effectively controlled.

The namespace

API. If window.config does not exist, window.config. API will report an error. To avoid this error, Var API = window.config && window.config. API; Code like this.

Here’s where namespace comes in, with a simplified version of the namespace function implementation (for demonstration only, not production) :

function namespace(tpl, value) {
  return tpl.split('. ').reduce((pre, curr, i) = > {
    return (pre[curr] = i === tpl.split('. ').length - 1
      ? (value || pre[curr])
      : (pre[curr] || {}))
  }, window);
}
Copy the code

Set window.app.a.b with namespace:

namespace('app.a.b'.3); // window.app.a.b 值为 3
Copy the code

Get window.app.a.b from namespace:

var b = namespace('app.a.b');  // the value of b is 3
 
var d = namespace('app.a.c.d'); // the value of d is undefined
Copy the code

App.a.c is undefined, but because namespace is used, app.a.c.d does not report an error, and variable d is undefined.

AMD/CMD

With the front-end business weight, the code is more and more complex, relying on the global variable communication way began to be inadequate, the front-end is in urgent need of a clearer, simpler way to deal with code dependence, JS modular implementation and specifications have emerged, which has been widely used module specifications AMD and CMD.

Faced with a modular solution, we first need to understand: 1. How to export the interface; 2. How to import interfaces?

AMD

The Asynchronous Module Definition Specification (AMD) lays down rules for defining modules so that modules and their dependencies can be loaded asynchronously. This is appropriate for an environment where browsers load modules asynchronously (which can cause performance, availability, debugging, and cross-domain access issues).

This specification defines only one function, define, which is a global variable.

/ * * *@param {string} Id Module name *@param {string[]} Array * of modules that the dependencies module depends on@param {function} The factory module initializes the function or object to be executed *@return {any} Module export interface */
function define(id? , dependencies? , factory) :any
Copy the code

RequireJS

AMD is an asynchronous module specification and RequireJS is an implementation of the AMD specification.

Next, we refactor the above project with RequireJS.

Add require.js to the original project js folder:

# Project contents:├ ─ js# js folder│ ├ ─... │ └ ─ the require. Js# RequireJS JS library
└─  ...
Copy the code
// config.js
define(function() {
  var api = 'https://github.com/ronffy';
  var config = {
    api: api,
  };
  return config;
});
Copy the code
// utils.js
define(['./config'].function(config) {
  var utils = {
    request() {
      console.log(config.api); }};return utils;
});
Copy the code
// main.js
require(['./utils'].function(utils) {
  utils.request();
});
Copy the code
<! -- index.html -->
<! -... Omit the others -->
<body>

  <script data-main="./js/main" src="./js/require.js"></script>
</body>
</html>
Copy the code

It can be seen that with RequireJS, each file can be managed as a module, and the communication mode is also in the form of modules, which can not only clearly manage module dependencies, but also avoid declaring global variables.

For more information about AMD, see the documentation. For more on RequireJS, see the documentation.

Special note: there was RequireJS first, then AMD specification, with the promotion and popularity of RequireJS, AMD specification was created.

CMD and AMD

CMD, like AMD, is a modular specification of JS, and is mainly applied to the browser side. AMD was created in the process of popularizing RequireJS. CMD was created during the promotion and popularity of SeaJS.

The main difference between the two is that CMD advocates proximal dependency while AMD advocates predependency:

// AMD
Dependencies must be written in the beginning
define(['./utils'].function(utils) {
  utils.request();
});

// CMD
define(function(require) {
  // Dependencies can be written nearby
  var utils = require('./utils');
  utils.request();
});
Copy the code

AMD also supports nearby dependencies, but the RequireJS authors and official documentation recommend dependency precursors in preference.

Considering that AMD and CMD are being used less and less in mainstream projects, it is good to have a general understanding of AMD and CMD and not to go into too much detail here.

For more CMD specifications, see the documentation. For more SeaJS documentation, see the documentation.

With the advent of the ES6 module specification, AMD/CMD will eventually become a thing of the past, but there is no doubt that AMD/CMD is an important step in the process of front-end modularization.

Mr. Thief. – The original address of the article

CommonJS

As mentioned earlier, AMD and CMD are mainly used on the browser side. With the birth of Node, the server side module specification CommonJS was created.

CommonJS: config.js, utils.js, main.js

// config.js
var api = 'https://github.com/ronffy';
var config = {
  api: api,
};
module.exports = config;
Copy the code
// utils.js
var config = require('./config');
var utils = {
  request() {
    console.log(config.api); }};module.exports = utils;
Copy the code
// main.js
var utils = require('./utils');
utils.request();
console.log(global.api)
Copy the code

Perform the node main js, https://github.com/ronffy be printed out. Print global. API in main.js with undefined. Node uses global to manage global variables, similar to a browser’s Window. In browsers, the top-level scope is a global scope. Variables declared in the top-level scope are global variables. In Node, the top-level scope is not a global scope, so variables declared in the top-level scope are not global variables.

The module exports and exports

Module. exports is used in one place and exports is used in another. What is the difference?

The CommonJS specification only defined exports, but there were some problems with exports (more on that below), so module.exports was created and it was called CommonJS2. Each file is a module, and each module has a Module object whose module property exports interfaces. External modules import the current module using the Module object, which node does based on the CommonJS2 specification.

// a.js
var s = 'i am ronffy'
module.exports = s;
console.log(module);
Copy the code

Execute node A.js and look at the printed Module object:

{
  exports: 'i am ronffy'.id: '. './ / module id
  filename: '/Users/apple/Desktop/a.js'.// File path name
  loaded: false.// Check whether the module is loaded
  parent: null.// Parent module
  children: [].// Submodule
  paths: [ / *... * /].// The path to the node search module after the node a.js command is executed
}
Copy the code

When other modules import this module:

// b.js
var a = require('./a.js'); // a --> i am ronffy
Copy the code

When writing in A. js:

// a.js
var s = 'i am ronffy'
exports = s;
Copy the code

The module.exports of the A. js module is an empty object.

// b.js
var a = require('./a.js'); // a --> {}
Copy the code

Module. exports and exports

var module = {
  exports: {}}var exports = module.exports;
console.log(module.exports === exports); // true

var s = 'i am ronffy'
exports = s; Module.exports is not affected
console.log(module.exports === exports); // false
Copy the code

Exports and module.exports refer to the same memory when the module is initialized. Exports is reassigned to the same memory address and is disconnected from the original memory address.

So, exports are used like this:

// a.js
exports.s = 'i am ronffy';

// b.js
var a = require('./a.js');
console.log(a.s); // i am ronffy
Copy the code

CommonJS and CommonJS2 are often confused. CommonJS and CommonJS2 are often referred to as CommonJS2, as is the case in this article, but it is good to know the difference and how to use them.

CommonJS with AMD

Both CommonJS and AMD are run time loaded, in other words: dependencies between modules are determined at run time.

What are the differences between the two:

  1. CommonJS is the server side module specification, AMD is the browser side module specification.
  2. CommonJS modules are loaded synchronously, i.e. executedvar a = require('./a.js');After the loading of the A.js file is complete, the following code is executed. AMD loads modules asynchronously, executing code in the form of callback functions after all dependencies are loaded.
  3. [Code below]fsandchalkIt’s all modules. The difference is,fsIs the Node built-in module,chalkIs an NPM package. These two conditions are only available in CommonJS, AMD does not support them.
var fs = require('fs');
var chalk = require('chalk');
Copy the code

UMD

Universal Module Definition.

There are so many module specifications, if you produce a module for others to use, want to support the form of global variables, also in accordance with the AMD specification, can also comply with the CommonJS specification, can so omnipotent? Yes, it can be so versatile. UMD shines.

UMD is a generic module definition specification that looks something like this (assuming our module name is myLibName):

!function (root, factory) {
  if (typeof exports= = ='object' && typeof module= = ='object') {
    // CommonJS2
    module.exports = factory()
    // define. Amd is used to determine whether the project requires.js.
    / / more define. Amd, please [see document] (https://github.com/amdjs/amdjs-api/wiki/AMD#defineamd-property-)
  } else if (typeof define === 'function' && define.amd) {
    // AMD
    define([], factory)
  } else if (typeof exports= = ='object') {
    // CommonJS
    exports.myLibName = factory()
  } else {
    // Global variables
    root.myLibName = factory()
  }
}(window.function () {
  // The module initializes the code to execute
});
Copy the code

UMD is a good solution to the problem of JS modules being used across module specifications and platforms.

Mr. Thief. – The original address of the article

ES6 module

AMD, CMD and so on are on the basis of the original JS grammar on the second encapsulation of some methods to solve the modular scheme, ES6 Module (in many places is abbreviated as ESM) is the language level of the specification, ES6 Module aims to provide universal module solutions for browsers and servers. In the long run, ES6 Module will be used uniformly in module specifications in the future, whether it is jS-BASED WEB side, server side or desktop application based on Node.

compatibility

Currently, ES6 Modules are not fully supported natively, either on the browser side or in Node. If you use ES6 Modules, you can use compilers such as Babel. This article deals only with ES6 Module syntax, so it won’t discuss ways to compile ES6, such as Babel or typescript.

Export interface

In CommonJS, the top-level scope is not global. Similarly, in ES6 Module, a file is a module, and the top-level scope of a file is not global. The export keyword is used for the export interface, and the import keyword is used for the import interface.

Export Export interface can be exported in the following ways:

Method 1

export const prefix = 'https://github.com';
export const api = `${prefix}/ronffy`;
Copy the code

Way 2

const prefix = 'https://github.com';
const api = `${prefix}/ronffy`;
export {
  prefix,
  api,
}
Copy the code

Method 1 and method 2 are written differently and the result is the same, exporting prefix and API separately.

Option 3 (Export by default)

// foo.js
export default function foo() {}

// Equivalent to:
function foo() {}
export {
  foo as default
}
Copy the code

Export default is used to export the default interface of the module, which is equivalent to exporting an interface named default. The AS keyword used with export is used to rename the interface when exporting the interface.

Mode 4 (Import and export abbreviation)

export { api } from './config.js';

// Equivalent to:
import { api } from './config.js';
export {
  api
}
Copy the code

To import an interface from a module and then export it, you can use export… From ‘module’.

Import Module interface

ES6 Modules use import to import module interfaces.

Export interface module code 1:

// config.js
const prefix = 'https://github.com';
const api = `${prefix}/ronffy`;
export {
  prefix,
  api,
}
Copy the code

Interface has been exported, how to import:

Method 1

import { api } from './config.js';

// or
// The 'as' keyword used with' import 'is used to rename the imported interface.
import { api as myApi } from './config.js';
Copy the code

Mode 2 (Whole import)

import * as config from './config.js';
const api = config.api;
Copy the code

All interfaces that export the config.js module are mounted on the Config object.

Option 3 (import by default export)

// foo.js
export const conut = 0;
export default function myFoo() {}
Copy the code
// index.js
// The default imported interface is deliberately named cusFoo to show that the naming is fully customizable.
import cusFoo, { count } from './foo.js';

// Equivalent to:
import { default as cusFoo, count } from './foo.js';
Copy the code

Export default Export interface, you can use import name from ‘module’ to import. This way, it is easy to import the default interface.

Mode 4 (Whole loading)

import './config.js';
Copy the code

This loads the entire config.js module, but does not import any of its interfaces.

Mode 5 (Dynamically loading modules)

The various ways to import interfaces to the ES6 Module have been described above, but one scenario has not been covered: dynamically loading modules. For example, the popup window will pop up after the user clicks a button, and the modules involved in the function of the popup window have a heavy amount of code. Therefore, it is a waste of resources if these related modules are loaded when the page is initialized. Import () can solve this problem and realize the module code loading on demand from the language level.

The ES6 Module handles all the above methods of importing module interfaces at compile time, so the import and export commands can only be used at the top level of the module.

/ / an error
if (/ *... * /) {
  import { api } from './config.js'; 
}

/ / an error
function foo() {
  import { api } from './config.js'; 
}

/ / an error
const modulePath = './utils' + '/api.js';
import modulePath;
Copy the code

Load on demand using import() :

function foo() {
  import('./config.js')
    .then(({ api }) = >{}); }const modulePath = './utils' + '/api.js';
import(modulePath);
Copy the code

Note: The proposal for this feature is currently in phase 4 of the TC39 process. For more information, see TC39/proposal-dynamic-import.

CommonJS and ES6 module

CommonJS and AMD are run time loads where module dependencies are determined at run time. The ES6 module handles module dependencies at compile time (import() is loaded at run time).

CommonJS

CommonJS loads modules when they are imported. “CommonJS is runtime loaded” because the code generates module.exports after it is run. Of course, CommonJS caches modules so that a module is only loaded once, even if it is imported multiple times.

// o.js
let num = 0;
function getNum() {
  return num;
}
function setNum(n) {
  num = n;
}
console.log('o init');
module.exports = {
  num,
  getNum,
  setNum,
}
Copy the code
// a.js
const o = require('./o.js');
o.setNum(1);
Copy the code
// b.js
const o = require('./o.js');
// Note: this is just a demo, do not modify modules like this in the project
o.num = 2;
Copy the code
// main.js
const o = require('./o.js');

require('./a.js');
console.log('a o.num:', o.num);

require('./b.js');
console.log('b o.num:', o.num);
console.log('b o.getNum:', o.getNum());
Copy the code

Run the node main.js command. The following output is displayed:

  1. o init

Modules are loaded only once, even if they are imported by multiple other modules, and the interface is assigned to the module.exports property after the code is run. Module 0 is not reflected in module.exports when the module is loaded. B. Num: 2 Direct modifications to imported modules are reflected in the module.exports of that module. 4. B. Getnum: 1 module forms a closure after loading.

ES6 module

// o.js
let num = 0;
function getNum() {
  return num;
}
function setNum(n) {
  num = n;
}
console.log('o init');
export {
  num,
  getNum,
  setNum,
}
Copy the code
// main.js
import { num, getNum, setNum } from './o.js';

console.log('o.num:', num);
setNum(1);

console.log('o.num:', num);
console.log('o.getNum:', getNum());
Copy the code

We added an index.js to support ES6 Module on the Node side:

// index.js
require("@babel/register") ({presets: ["@babel/preset-env"]});module.exports = require('./main.js')
Copy the code

Run the NPM install @babel/ core@babel/register@babel /preset-env -d command to install the ES6-related NPM package.

Run the node index.js command. The following output is displayed:

  1. o init

Modules are loaded only once, even if they are imported by multiple other modules. 2. O.num: 0 3. O.num: 1 Determine the ES6 module that the module depends on when compiling. The interface imported through import is only a reference to the value, so num will print two different results. 4. o.getNum: 1

For print result 3, be aware of the result and keep that in mind in your project. This involves many concepts and principles such as “Module Records”, “Module instance” and “linking”. You can check ES modules: A Cartoon deep-dive, no more in this article.

The ES6 Module is loaded at compile time (or “statically loaded”), which allows you to make many optimizations to your code that were not possible before:

  1. Code checks related to import and export modules can be done at the development stage.
  2. In combination with tools such as Webpack and Babel, dead-code that is not referenced in context can be removed at the packaging stage. This technique is called “Tree shaking”, which can greatly reduce code size, shorten application runtime, and improve application performance.

Afterword.

We are using CommonJS and ES6 Module in daily development, but many people only know it and do not know why, and even many people are relatively strange to AMD, CMD, IIFE and other overview, I hope that through this article, we can have a clear and complete understanding of the JS modular road. JS modular road tends to be stable at present, but certainly will not stop here, let us learn together, progress together, witness together, also hope to have the opportunity for the future of modular specification to contribute their own little strength. My ability is limited, the article may unavoidably have some fallacies, welcome everyone to help improve, article github address, I am Mr. Thief.

reference

AMD official documentation ruan Yifeng: Module load implementation