Module module in ES6

Posted by Ixplodestuff8 on Wed, 09 Feb 2022 12:34:26 +0100

Before ES6, JavaScript had no module system, so it was impossible to split a large program into interdependent small files and assemble them in a simple way. The community has developed some Module loading scheme , the CommonJS specification is very good, but it is not applicable to the browser environment, so there are two schemes: AMD and CMD.
At the level of language standard, ES6 realizes the module function, and the implementation is quite simple. It can completely replace CommonJS and AMD specifications and become a general module solution for browsers and servers.

The design idea of ES6 module is to be static as much as possible, so that the dependencies of the module and the input and output variables can be determined at compile time. CommonJS and AMD modules can only determine these things at run time. For example, the CommonJS module is an object. When inputting, you must find the object attribute.

// CommonJS module
let { stat, exists, readfile } = require('fs');

// Equivalent to
//Load the FS module as a whole (that is, all methods of loading FS), generate an object (_fs), and then read three methods from this object
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

This loading is called "runtime loading", because only the runtime can get this object, so there is no way to do "static optimization" at compile time.

ES6 module is not an object, but the output code is explicitly specified through the export command, and then input through the import command.

// ES6 module
//Three methods are loaded from fs module, and other methods are not loaded
import { stat, exists, readFile } from 'fs';

This loading is called "loading at compile time" or static loading, that is, ES6 can complete module loading at compile time, which is more efficient than the loading method of CommonJS module. Of course, this also makes it impossible to reference the ES6 module itself because it is not an object.

Strict mode
The module of ES6 automatically adopts strict mode, whether you add "use strict" to the module header or not;.

export command

// profile.js
export let firstName = 'Michael';
//Another
// profile.js
let firstName = 'Michael';
let lastName = 'Jackson';
export { firstName, lastName };
//Output function or class
export function multiply(x, y) {
  return x * y;
};
//Rename using the as keyword.
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2
};
// report errors
export 1;

// report errors
let m = 1;
export m;

No external interface is provided. The first method outputs 1 directly, and the second method outputs 1 directly through variable m. 1 is just a value, not an interface. The correct way to write it is as follows.

//Other scripts can get the value 1 through the interface. Their essence is to establish a one-to-one correspondence between the interface name and the internal variables of the module.
// Writing method I
export let m = 1;

// Writing method 2
let m = 1;
export {m};

// Writing method III
let n = 1;
export {n as m};

The output of function and class must also follow the above writing method
In addition, the interface output by the export statement has a dynamic binding relationship with its corresponding value, that is, the real-time value inside the module can be obtained through this interface.

export let foo = 'bar';
setTimeout(() => foo = 'baz', 500);

The above code outputs the variable foo with the value bar, which becomes baz after 500 milliseconds.

This is completely different from the CommonJS specification. The output of CommonJS module is the cache of values, and there is no dynamic update.

Finally, the export command can appear anywhere in the module, as long as it is at the top of the module. If it is within the scope of block level, an error will be reported, and so will the import command. This is because in the conditional code block, it is impossible to do static optimization, which is contrary to the original design intention of ES6 module.

//The export statement is placed in the function, and an error is reported in the result.
function foo() {
  export default 'bar' // SyntaxError
}
foo()

import command

The import command accepts a pair of braces that specify the name of the variable to be imported from other modules.

// main.js
import { firstName } from './profile.js';
import { lastName as surname } from './profile.js';
function setName(element) {
  element.textContent = firstName + ' ' + surname ;
}

The variables entered by the import command are read-only, because its essence is the input interface. In other words, it is not allowed to rewrite the interface in the script that loads the module.

import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;

In the above code, the script loads variable a, and an error will be reported if it is re assigned, because a is a read-only interface. However, if a is an object, it is allowed to override the properties of A.

import {a} from './xxx.js'

a.foo = 'hello'; // Legal operation

This way of writing is difficult to check errors. It is suggested that all input variables should be treated as completely read-only, and its properties should not be easily changed.

The from after import specifies the location of the module file, which can be a relative path or an absolute path. If there is no path but a module name, there must be a configuration file to tell the JavaScript engine the location of the module.

import { myMethod } from 'util';

Note that the import command has the effect of promotion. It will be promoted to the head of the whole module and executed first.

foo();
//The import command is executed during the compilation phase before the code runs. 
import { foo } from 'my_module';

Because import is executed statically, expressions and variables cannot be used. These syntax structures can only be obtained at run time.

Overall loading of modules
The object where the module is loaded as a whole (circle in the following example) should be statically analyzed, so it is not allowed to change at run time. The following expressions are not allowed.

import * as circle from './circle';

// The following two lines are not allowed
circle.foo = 'hello';
circle.area = function () {};

export default command
The export default command specifies the default output for the module.

// export-default.js
export default function () {
  console.log('foo');
}

The default output of the above code is a function.

When other modules load the module, the import command can specify any name for the anonymous function.

// import-default.js
import customName from './export-default';
customName(); // 'foo'

The import command of the above code can point to export default with any name JS output method. At this time, you don't need to know the function name output by the original module. It should be noted that curly braces are not used after the import command.

The export default command can also be used before non anonymous functions.

// export-default.js
export default function foo() {
  console.log('foo');
}

// Or write

function foo() {
  console.log('foo');
}

export default foo;

In the above code, the function name foo of foo function is invalid outside the module. When loading, it is regarded as anonymous function loading.

Let's compare the default output with the normal output.

// first group
export default function crc32() { 
  // ...
}
// Braces are not required for input
import crc32 from 'crc32'; 
// Group 2 
export function crc32() { 
  // ...
};
// Braces are required for input
import {crc32} from 'crc32'; 

The export default command specifies the default output of the module. Obviously, a module can only have one default output, so the export default command can only be used once. Therefore, there is no need to use parentheses after the import command, because it can only correspond to the export default command.

In essence, export default is to output a variable or method called default, and then the system allows you to give it any name. Therefore, the following writing is effective.

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// Equivalent to
// export default add;

// app.js
import { default as foo } from 'modules';
// Equivalent to
// import foo from 'modules';

Because the export default command only outputs a variable called default, it cannot be followed by a variable declaration statement.

// correct
export let a = 1;

// correct
let a = 1;
export default a;

// error
export default let a = 1;

In the above code, export default a means to assign the value of variable a to variable default. Therefore, the last way of writing will report an error.

Similarly, because the essence of the export default command is to assign the following value to the default variable, you can write a value directly after export default.

// correct
export default 42;

// report errors
export 42;

In the above code, the error in the latter sentence is because the external interface is not specified, while the previous sentence specifies the external interface as default.

With the export default command, it is very intuitive to enter the module. Take the lodash module as an example.

The export statement is as follows.

export default function (obj) {
  // ···
}

export function each(obj, iterator, context) {
  // ···
}

export { each as forEach };//, the forEach interface is exposed. By default, it points to each interface, that is, forEach and each point to the same method.

If you want to input the default method and other interfaces in an import statement, you can write it as follows.

import _, { each, forEach } from 'lodash';

export default output class.

// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass';
let o = new MyClass();

Compound writing of export and import
If the same module is input first and then output in a module, the import statement can be written together with the export statement.

export { foo, bar } from 'my_module';

// It can be simply understood as
import { foo, bar } from 'my_module';
export { foo, bar };

It should be noted that after being written in one line, foo and bar are not actually imported into the current module, but they are equivalent to forwarding these two interfaces externally, so that the current module cannot directly use foo and bar.

The interface name of the module and the overall output can also be written in this way.

// Interface renaming
export { foo as myFoo } from 'my_module';

// Overall output
export * from 'my_module';

The default interface is written as follows.

export { default } from 'foo';

The writing method of changing the named interface to the default interface is as follows.

export { es6 as default } from './someModule';

// Equivalent to
import { es6 } from './someModule';
export default es6;

Similarly, the default interface can also be renamed a named interface.

export { default as es6 } from './someModule';

Before ES2020, there was an import statement without corresponding compound writing.

import * as someIdentifier from "someModule";

ES2020 adds this wording.

export * as ns from "mod";

// Equivalent to
import * as ns from "mod";
export {ns};

Module inheritance

Suppose there is a circle plus module that inherits the circle module.

// circleplus.js

export * from 'circle'; //The export * command ignores the default method of the circle module
export let e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}

// circleplus.js
//Output only the area method of the circle module and rename it to circleArea

export { area as circleArea } from 'circle';

The writing method of loading the above module is as follows.

// main.js

import * as math from 'circleplus';
import exp from 'circleplus';//Load the default method of the circleplus module as the exp method
console.log(exp(math.e));

Cross module constant

const declared constants are valid only in the current code block. If you want to set a constant across modules (that is, across multiple files), or a value should be shared by multiple modules, you can use the following writing method.

// constants.js module
export const A = 1;
export const B = 3;

// test1.js module
import * as constants from './constants';
console.log(constants.A); // 1

// test2.js module
import {A, B} from './constants';
console.log(A); // 1

If there are many constants to use, you can create a special constants directory, write various constants in different files and save them in this directory.

// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

Then, the constants output from these files are merged into index JS inside. When using, directly load index JS is OK.

// constants/index.js
export {db} from './db';
export {users} from './users';
// script.js
//use
import {db, users} from './constants/index';
import()

The ES2020 proposal introduces the import() function to support dynamic loading of modules.

import(specifier)

In the above code, the parameter specifier of the import function specifies the location of the module to be loaded. The import() function can accept whatever parameters the Import command can accept. The main difference between the two is that the latter is dynamic loading.

import() returns a Promise object. Here is an example.

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });

The import() function can be used anywhere, not just in modules, but also in non module scripts. It is executed at runtime, that is, when it runs to this sentence, the specified module will be loaded. In addition, the import() function has no static connection with the loaded module, which is also different from the import statement. import() is similar to the require method of Node. The main difference is that the former is asynchronous loading and the latter is synchronous loading.

Applicable occasions

(1) Load on demand.

button.addEventListener('click', event => {
  import('./dialogBox.js')
  .then(dialogBox => {
    dialogBox.open();
  })
  .catch(error => {
    /* Error handling */
  })
});

(2) Conditional loading

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

(3) Dynamic module path

//import() allows module paths to be generated dynamically. Load different modules according to the return result of function f.

import(f())
.then(...);

Attention
After import() loads the module successfully, the module will be used as an object and as a parameter of the then method. Therefore, the syntax of object deconstruction assignment can be used to obtain the output interface.

import('./myModule.js')
.then(({export1, export2}) => {
  // ...·
});

If the module has a default output interface, it can be obtained directly with parameters.

import('./myModule.js')
.then(myModule => {
  console.log(myModule.default);
});

The above code can also be in the form of named input.

import('./myModule.js')
.then(({default: theDefault}) => {
  console.log(theDefault);
});

If you want to load multiple modules at the same time, you can use the following writing method.

Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

import() can also be used in async functions.

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();

reference resources: https://es6.ruanyifeng.com/#docs/module

Topics: Javascript Front-end