Modularity is a mechanism for breaking JavaScript programs into separate modules that can be imported on demand. As JavaScript scripts become larger and more complex, JavaScript modularization is becoming more and more important. Almost all of the latest browsers now support JavaScript native modularization.

What is the point of modularity?

The JS modularization mechanism splits the JS code into different small files, which has the following advantages:

  • Each file has a private namespace to avoid global pollution and variable collisions.
  • Logical separation, you can put different logic code in different JS files.
  • Improve code reusability, maintainability, and readability

Object-based, closure modularity

Object-based modularity

Before CommonJS and ES6Module, a common way to avoid global variable pollution was to put a class of variables in an object, so that each object’s properties (variables) were private to that object, avoiding the problem of variable collisions.

let a = {
    sayHello: 'hello1'
}
let b = {
    sayHello: 'hello2'
}

In this way, even if the same variable name appears, there will be no conflict. The variables related to each logical point are put into one object to minimize variable pollution. The JavaScript built-in object Math is also implemented in this way.

Closure based modularity

IIFE (call function expression immediately)

IIFE is a function that is called when it is defined. Defining a IIFE is simple by writing two parentheses. The first parentheses declare an anonymous function and the second parentheses pass in an argument.

// IIFE (function (arg) {console.log(arg)})(1); // IIFE () {console.log(arg)}); // 1 (function (arg) {console.log(arg)}(2)); // Iife must be followed by a semicolon to indicate the end // 2

Immediate call function expressions (IIFE) have the following advantages:

  • Variables within a function do not cause global pollution.
  • The function is destroyed immediately after execution without wasting resources.

Imagine a tool that parses code files, wraps the contents of each file into an immediately callable function expression, and keeps track of the return value of each function to assemble everything into one large file.

Some code packaging tools are implemented with this in mind.

The disadvantages of both approaches

While both approaches can implement private namespaces and avoid variable pollution problems, they still have some significant drawbacks.

For object-based modularity:

  • Declaring a variable becomes a property of a declared object. Failure to use some of the useful mechanisms for declaring variables can lead to property overwriting problems caused by repeatedly naming properties.
  • Overrides may occur between objects
  • The code is still in a file, which causes the file code to become larger and larger
Let o = {b: 1} o.a = 1; let o = {b: 1} o.a = 1; o.b = 2; Let a = 1; let a = 1; let a = 1; let b = 2; let b = 3; // You can't repeat the declaration

For closures based modularity:

  • Variables and functions in IIFE are not reusable
  • Inconvenient to use
  • Hard to test, hard to maintain

In addition, neither approach is really ideal modularity, and both have problems such as the inability to split code into different files, difficulty in maintenance, and a flawed implementation of private namespaces.

Modularity of Node.js (CommonJS)

Ideally, modularity would be the ability to split code into different files for maintainability and readability, import code files into each other for reusability, and have a private namespace for each code file to avoid variable collisions and pollution.

This is achieved by the CommonJS module mechanism, which is NodeJS’s built-in modularization mechanism. It can split a complex program into any number of code files, each file is an independent module with a private namespace, you can choose to export one or all of the variables and functions, another code file can be imported into their own file, to achieve the reuse of variables and functions.

Derivation of the node

Node exports in one of two ways, module.exports and exports. Both of these objects are global built-in and can be used directly. Their usage is as follows

// You can export the variables individually. A = "a"; exports.b = 123; Export.fn = function() {console.log(' I am a function and I am anonymous ')} // Export.fn = function() {console.log(' I am a function and I am anonymous ')} // Exports = {a, b} // let c = true; exports = {a, b} // let c = true; Let fn2 = function(){console.log(' I am a function, and I am anonymous ')} module.exports = {c, fn2}

You might think that module.exports is similar to exports, but in fact they are. Exports and module.exports refer to the same object, so export. a equals module.exports.

Exports is an object that Node exposes its properties and methods for other modules to import. Exports is a variable that points to module.exports, which is just module.export.xx.

At the same time, this also explains why it is not possible to direct exports = XX, because this would change the original direction of exports.

The introduction of the node

Since there are exports, there are imports, and the NodeJS module imports data from other modules by calling require().

You may have seen the following two types of imports

const fs = require('fs');

const user = require('./user.js')

The first way is to import NodeJS’s built-in FS module without writing the path, while the second way is to import the module written by the user, so the path is written (the path can be either relative or absolute).

Classification of modules

In fact, the built-in modules in Node are also called core modules, which are compiled into binary files when NodeJS source code is compiled. When NodeJS is started, these core modules are directly loaded into memory, so when a core module is loaded, compared to a file module, Core module reference does not need to carry out file positioning and dynamic compilation, speed has the advantage, the import directly write the module name, do not fill in the path, such as HTTP, FS, PATH and other common modules are core modules.

The code modules written by users themselves are called file modules, and the file modules are also divided into path introduced module and custom module according to the different ways of import.

const express = require('express'); // Custom Module const UsersRouter = require('./routes/users'); // The module is introduced as a path

For modules imported in the form of a path, since the exact path is provided, the require method converts the specified path to the actual path on the hard disk and caches the compiled result using this path as an index. By specifying the path, this form of file module saves a lot of time in path analysis and is faster than custom modules, but slower than core modules

  • The core module
  • File module

    • The module imported by path
    • Custom module

Path analysis

For custom modules, custom modules follow the following strategy for path analysis, which can be time consuming.

  1. Find the node_modules directory in the current directory to see if there is a match
  2. Find the node_modules directory in the parent directory to see if there is a match
  3. Follow this rule and search through the parent directory until you reach node_modules in the root directory

File location

When the path is analyzed and the imported path has no file extension, node will analyze the file extension and try it in the order of.js,.node, and.json.

If the path points to a directory instead of a file, then:

  • The first step is to look for the package.json file in the directory where it was hit and parse it with json.parse. The main attribute in the JSON file is retrieved as the hit file
  • If package.json or the corresponding main property is not found, then the index file under this directory will be used as the hit file. Again, try.js,.node, and.json one by one
  • If the index is still not found, the file location fails, and the search will continue one level up according to the path traversal rules mentioned above

Because of the layer by layer search, the path analysis of the custom module needs to consume a lot of events, which will lead to low search efficiency, so the loading performance of the custom module is slower than the way of loading in the form of path.

The cache

In addition, Node will cache the imported modules. The next reference will check whether there is a corresponding file in the cache and load it from the cache first to reduce unnecessary consumption.

conclusion

Therefore, Node can export using module.exports and exports, where module.exports points to the object to be exported and exports points to module.exports; Node imports via require(), either pathless or pathless (core module, custom module).

Node modules fall into the following categories:

  • The core module
  • File module

    • The module imported by path
    • Custom module

Load speed: cache > core module > with path import module > custom module

Modularity of ES6

ES6 has added import and export keywords to JavaScript, supporting modularity as a core language feature. ES6 Module is conceptually the same as CommonJS in that it splits code into different code files. Each code file is a Module that can be imported and exported from one Module to another.

ES6 Module basic use

// a.js export a = 'a'; Export fn1(){console.log("hello ES6")} // Like CommonJS, the ES6 Module can be exported with let b = 1; Let fn2 = function(){console.log("hello ES6 module ")} export {b,fn2}
// b.js
import a from "./a.js"

console.log(a.a);

a.fn1()  // hello ES6

Note: export {b,fn2} appears to declare an object literal, but the curly braces do not define the object literal. The export syntax simply requires a comma-separated list within a pair of curly braces.

In addition, ES6 modules automatically adopt strict mode, whether or not you put “use strict” in the module header; .

extension

In addition to the basic usage above, import and export also have some extended usage methods.

Use of deconstruction

Import {a} from "./a.js"console.log(a) // equal to import a from "./a.js"console.log(a.a)

Rename by the as keyword

Import {a as b} from "./a.js"console.log(b) // Equals import {a} from "./a.js"console.log(a) // You can also set the alias export {a as b}

Executing a module, but not importing any values, can be written like this

import "./a.js"

If the same import statement is executed multiple times, it is executed only once, not more than once.

Block loading, in which an asterisk (*) is used to specify an object on which all output values are loaded.

import * as moduleA from './a.js'; console.log(moduleA.a) // 'a'

From the example above, the import needs to know the name of the exported function/variable before it can be used, which is not very convenient, we can set a default value.

// moduleA.jsexport default function () { console.log('foo'); }

This way, you don’t need to know the name of the function or variable exported by ModuleA when importing.

// moduleb.js // import fn from "./moduleA"fn() // foo

The import command in the above code can point to the method that modulea.js outputs with any name, without needing to know the name of the function that the original module outputs. Note that there are no curly braces after the import command.

Note: Only one export default can exist in a module

In essence, export default is simply printing a variable or method called default, and the system allows you to name it whatever you want. So, the following is also valid.

// modules.jsfunction add(x, y) { return x * y; }export {add as default};

A compound of export and import

If you input and then output the same module within a module, an import statement can be written together with an export statement.

export { foo, bar } from 'my_module'; Import {foo, bar} from 'my_module'; import {foo, bar} from 'my_module'; export { foo, bar };

For more compound notation, see Network Channel -ES6

Static and dynamic imports

The import used earlier has the following characteristics:

  1. There is a promotion behavior, will be promoted to the head of the entire module, the first to execute
  2. Imported variables and functions are read-only because they are essentially input interfaces
  3. Compile-time import

Import and export commands can only be found at the top level of a module, not in a block of code (for example, in an if block, or in a function), because the engine processes imports at compile time, importing the contents of the module before the code is executed, and does not parse or execute if statements.

If (true) {import moduleA from "./a.js"}

Because an import is a static import, the module to be imported is determined when the code is written, not when the code is executed.

// const path = './' + fileName; import a from path;

To solve this problem, the ES2020 proposal introduces the import() function to support dynamic loading of modules.

// The above example could be written as const path = './' + fileName; import(path).then(module=>{}).catch(err=>{});

The biggest difference between import() and import() is that the former is a static import, while the latter is a dynamic import, and the latter returns a Promise.

The import() function can be used anywhere, not just for modules, but also for non-module scripts. It is run at time, that is, whenever it reaches this point, the specified module is loaded. In addition, unlike import statements, the import() function does not have a static connection to the loaded module. Import () is similar to Node’s require method, except that it loads asynchronously while the latter loads synchronously.

Import () is suitable for:

  • According to the need to load
  • Conditions of loading
  • Dynamic path loading

Import () can also be used with deconstruction and asyn/await.

import('./myModule.js').then(({export1, export2}) => {  // ...·});async function main() {  const myModule = await import('./myModule.js');  const {export1, export2} = await import('./myModule.js');  // 同时加载多个模块  const [module1, module2, module3] =    await Promise.all([      import('./module1.js'),      import('./module2.js'),      import('./module3.js'),    ]);}main();

CommonJs vs Es6 Module

CommonJs Es6 Module
Support for Node applications Support for the Web and will continue to support Node in the future
CommonJS can load statements dynamically, and the code occurs at run time ES Module can be imported dynamically or statically
CommonJS exported values are copies and you can modify the exported values The ES6 Module outputs a reference to a value and is read-only
CommonJS caches imported modules Do not cache values

Wrote last

With official support for modularity as a core language feature, it is likely that the future modularity solution will be the use of ES6 modules (Node13 is starting to support ES6 modules), but since most Node applications currently use CommonJS, it is likely that both modules will be used in parallel for a long time to come.

Finally, code words are not easy. If this article has helped you, please give me a like. Thank you.

reference

Part of the content is referenced from:

Net road – ES6 Module

The Definitive Guide to JavaScript, 7th Edition