ES6 — Modularity (CommonJS, AMD, ES Module)

Module mode

Splitting the code into independent chunks and then connecting these chunks together can be achieved through the module pattern. The idea behind this model is simple: divide the logic into blocks, encapsulate each one, and make it independent of each other. Each block decides on its own what to expose to the outside world, and at the same time it decides on its own what external code to introduce and execute.

Module identifier

The module system is essentially a key/value entity, where each module has an identifier that can be used to reference it. This identifier may be a string in a system that emulates a module, or it may be the actual path to the module file in a natively implemented module system.

Module dependencies

Each module is associated with a unique identifier that can be used to retrieve the module. This identifier is usually the path to a JavaScript file, but in some module systems this identifier can also be a namespace path string declared within the module itself.

Module loading

  1. The concept of loading modules is derived from dependency contracts. When an external module is specified as a dependency, the local module expects the dependency to be ready and initialized when it is executed.
  2. In a browser, loading a module involves several steps. Loading a module involves executing the code within it, but only after all dependencies have been loaded and executed. If the browser does not receive the code for the dependent module, it must send the request and wait for the network to return. After receiving the module code, the browser must determine whether the module just received also has dependencies. Then recursively evaluate and load all dependencies until all dependent modules are loaded. Only when the entire dependency graph is loaded can the entry module be executed.

Entrance

Modules that depend on each other must specify one module as the entry point, which is also the starting point for code execution.

Dynamic dependencies

if(loadCondition) {
    require('./moduleA');
}

In this module, whether moduleA is loaded is determined at runtime.
Dynamic dependencies can support more complex dependencies, but at the cost of increasing the difficulty of static analysis of the module.

Static analysis

Analysis tools examine code structure and infer its behavior without actually executing the code.

Circular dependencies

The loader will perform depth-first dependency loading:

Screenshot 2023-10-03 15.07.35.png

Module loader before using ES6

Before ES6 natively supported modules, JavaScript code that used modules essentially expected to use language features that were not available by default. Therefore, code must be written according to a module syntax that conforms to a certain specification, and separate module tools are needed to connect these module syntaxes to the JavaScript runtime. The module syntax and connection methods here have different manifestations, usually requiring additional libraries to be loaded in the browser or preprocessing to be completed at build time.

CommonJS

The CommonJS specification outlines module definitions for declaring dependencies synchronously. This specification is primarily used to implement modular code organization on the server side, but can also be used to define module dependencies for use in browsers. CommonJS module syntax does not work directly in the browser

Note: It is generally believed that the module system of Node.js uses the CommonJS specification, which is actually not completely correct. Node.js uses a slightly modified version of CommonJS. Because Node.js is mainly used in a server environment, there is no need to consider network latency issues. For consistency, this section uses Node.js-style module definition syntax.

CommonJS module definition needs to use require() to specify dependencies, and use the exports object to define its own public API.

var moduleB = require('./moduleB');
module.exports = {
    stuff: moduleB.doStuff();
}

No matter how many times a module is referenced in require(), the module is always a singleton

console.log('moduleA');
var a1 = require('./moduleA');
var a2 = require('./moduleA');

console.log(a1===a2); // true

The module will be cached after it is loaded for the first time, and subsequent loads will obtain the cached module. The loading order of modules is determined by the dependency graph.
In CommonJS, the synchronous operation performed by the module system when the module is loaded. Therefore require() can be programmatically embedded in a module like this:

console.log('moduleA');
if (loadCondition) {
    require('./moduleA');
}

If moduleA has been loaded somewhere before, this condition require() means that only the moduleA namespace is exposed.

Asynchronous module definition

CommonJS takes the server as the target environment and can load all modules into memory at one time, while the module definition system of AMD Asynchronous Module Definition takes the browser as the target execution environment, which requires consideration of network latency. AMD’s general strategy is to let modules declare their own dependencies, and the module system running in the browser will obtain the dependencies on demand and execute the modules that depend on them as soon as the dependencies are loaded.

The core of AMD module implementation is to wrap module definitions with functions. This prevents global variables from being declared and allows the loader library to control when modules are loaded. Wrapper functions also facilitate portability of module code, because all module code inside the wrapper function uses native JavaScript structures. The function that wraps the module is a parameter of the global define, which is defined by the implementation of the AMD loader library

AMD modules can specify their own dependencies using string identifiers, and AMD supports optionally specifying string identifiers for modules.

//Module definition with ID 'moduleA'. moduleA depends on moduleB,
// moduleB will be loaded asynchronously
define('moduleA', ['moduleB'], function(moduleB) {
  return {
    stuff: moduleB.duStuff();
  }
})

AMD also supports require and exports objects, through which CommonJS-style modules can be defined inside AMD module factory functions. This allows them to be requested just like modules, but the AMD loader will recognize them as native AMD structures rather than module definitions

define('moduleA', ['require'], function(require){
  if(condition) {
    var moduleB = require('moduleB');
  }
})

General module definition

In order to unify the CommonJS and AMD ecosystems, the Universal Module Definition (UMD) specification came into being. UMD can be used to create module code that can be used by both systems. Essentially, a UMD-defined module detects at startup which module system to use, configures it appropriately, and wraps all the logic in a function expression that is called immediately.

Here is an example of a UMD module definition containing only one dependency:

 (function (root, factory) {
    if(typeof define === 'function' & amp; & amp; define.amd) {
      //AMD. Register as anonymous function
      define(['moduleB'], factory);
    }else if (typeof module === 'object' & amp; & amp; module.exports) {
      //Node. Strict CommonJS is not supported
      // But it can be used in CommonJS environments such as Node that support module.exports.
      module.exports = factory(require('moduleB'));
    }else {
      // Browser global context (root is window)
      root.returnExports = factory(root.moduleB);
    }
  }(this, function (moduleB) {
    // Use moduleB in some way
    // Use the return value as an export of the module
    // This example returns an object
    // But modules can also return functions as values
    return {};
  }));

Module loaders will eventually die

As the ECMAScript 6 module specification becomes more widely supported, the model shown above will eventually decline.

Using ES6 modules

One of the biggest improvements to ES6 modules is the introduction of module specifications. This specification simplifies the previous module loader in all aspects. Native browser support means that loaders and other preprocessing are no longer necessary. In many ways, the ES6 module system is the culmination of AMD and CommonJS.

Module tags and definitions

ECMAScript 6 modules exist as a whole block of JavaScript code. With type=”module” attribute

Module loading

Browsers that fully support ECMAScript 6 modules can load the entire dependency graph from the top-level module asynchronously. The browser will parse the entry module, determine the dependencies, and send a request for the dependent module. After these files are returned over the network, the browser parses their contents, determines their dependencies, and sends more requests if these secondary dependencies have not been loaded. This asynchronous recursive loading process continues until the entire application’s dependency graph has been resolved. After parsing the dependency graph, the application can officially load the module.
The process is very similar to AMD-style module loading. Module files are loaded on demand, and subsequent module requests will be delayed synchronously due to the network delay of this dependent module.

Module Behavior

ECMAScript 6 modules borrow many excellent features from CommonJS and AMD. like:

  • Module code is only executed after loading
  • Modules can only be loaded once
  • Modules are singletons
  • Modules can define public interfaces, and other modules can observe and interact based on this public interface.
  • Modules can request to load other modules
    The ES6 module system also adds new behaviors
  • ES6 modules execute in strict mode by default
  • ES6 modules don’t share the global namespace
  • The value of this at the top level of the module is undefined (window in regular scripts)
  • var declarations in modules are not added to the window object
  • ES6 modules are loaded and executed asynchronously

Module export

ES6 modules support two types of exports: named exports and default exports.

  1. The export keyword is used to declare a value as a named export. The export statement must be at the top level of the module and cannot be nested in a block.
  2. Exported values have no direct impact on the execution of JavaScript inside the module, so there are no restrictions on the relative position of the export statement and the exported value or on the order in which the export keyword appears in the module. The export statement can even appear before the value it is exporting:
 const foo = 'foo';
    export {<!-- --> foo };
    
    export const foo = 'foo';
    
    export {<!-- --> foo };
    const foo = 'foo';

Named exports behave as if the module were a container for the exported values.

1. Inline named export
    export const foo = 'foo';
    const foo = 'foo';
    export { foo };
2. An alias can be provided when exporting, and the alias must be specified in the brace syntax of the export clause.
    const foo = 'foo';
    export { foo as myFoo };
3. Export statement grouping
    const foo = 'foo';
    const bar = 'bar';
    const baz = 'baz';
    export { foo, bar as myBar, baz};

Default exports act as if the module and the exported value were one and the same. Default export Use the default keyword to declare a value as the default export. There can only be one default export per module.

export default foo
export default function() {}
export default function foo() {}
export default function*() {}
export default class {}

Module import

Modules can use values exported by other modules by using the import keyword. Must appear at the top level of the module

  • A module identifier can be a relative path to the current module or an absolute path to a module file. It must be a pure string, not the result of a dynamic calculation.
  • Imports are read-only for modules and are actually equivalent to variables declared by const. When using * to perform a bulk import, named exports assigned to aliases appear as if they had been frozen using Object.freeze(). It is not possible to directly modify the exported value, but you can modify the properties of the exported object.
  • The difference between named exports and default exports is also reflected in their imports. Named exports can be batch-fetched using * and assigned to the alias that holds the export collection, without listing each identifier:
const foo = 'foo', bar = 'bar', baz = 'baz'
export {foo, bar, baz}

    
import * as Foo form './foo.js'
console.log(Foo.foo); // foo
  • To import by name, place the identifier in the import clause.
import { foo, bar, baz as myBaz } from './foo.js';
  • The default export is as if the entire module was the exported value. It can be imported using the default keyword and providing an alias.
import { default as foo } from './foo.js';
import foo from './foo.js'

Module transfer and export

Values from module imports can be transferred directly to exports via pipes. At this point, you can also convert the default export to a named export, or vice versa. If you want to group all named exports of a module together, you can use *exports in bar.js like this:

export * from './foo.js'

This way, all named exports in foo.js will appear in the module that imports bar.js. If foo.js has a default export, this syntax ignores it.