Quick understanding of JavaScript modules

Posted by Genux on Wed, 17 Nov 2021 04:26:38 +0100

summary

As the development of Web applications with modern JavaScript becomes complex, naming conflicts and dependencies become difficult to deal with, so modularization is needed. The introduction of modularization can avoid naming conflicts, facilitate dependency management, and improve code reusability and maintainability. Therefore, on the premise that JavaScript has no module function, modularization can only be realized through third-party specifications:

  • CommonJS: synchronization module definition, used on the server side.
  • AMD: asynchronous module definition, used for browser side.
  • CMD: asynchronous module definition, used for browser side.
  • UMD: unify the definition of COmmonJS and AMD modular scheme.

They are based on the syntax and lexical features of JavaScript to "forge" the behavior of similar modules. TC-39 adds a module specification to ECMAScript 2015, which simplifies the module loader described above. Native means that it can replace the above specifications and become a common module solution for browsers and servers, which is more efficient than using libraries. The modular design goal of ES6:

  • Simple syntax like CommonJS.
  • Modules must be static structures
  • It supports asynchronous loading and synchronous loading of modules, and can be used on the server and client side at the same time
  • Support 'flexible configuration' of module loading
  • Better support circular references between modules
  • It has language level support, surpassing CommonJS and AMD

ECMAScript began to support module standards in 2015, and has gradually developed since then. Now it has been supported by all mainstream browsers. ECMAScript 2015 version is also known as ECMAScript 6.

modular

ES6 module borrows many excellent features of CommonJS and AMD, as shown below:

  • The module code is executed only after loading.
  • Modules can only be loaded once.
  • The module is a singleton.
  • Modules can define a common interface, and other modules can observe and interact based on this common interface.
  • Modules can request loading of other modules.
  • Support circular dependency.

Some new behaviors have also been added to the ES6 module system.

  • The ES6 module is executed in strict mode by default.
  • ES6 modules do not share a global namespace.
  • The value of this at the top level of the module is undefined; In the regular script is window.
  • var declarations in modules are not added to window objects.
  • ES6 modules are loaded and executed asynchronously.

When the browser runtime knows that a file should be regarded as a module, it will conditionally impose restrictions according to the above ES6 module behavior. JavaScript files associated with < script type = "module" > or loaded through the import statement will be recognized as modules.

export

All variables inside the ES6 module cannot be obtained externally, so the export keyword is provided to export real-time bound functions, objects or original values from the module, so that other programs can use them through the import keyword. Export supports two export methods: named export and default export. Different export methods correspond to different import methods.

In the ES6 module, whether the "use strict;" statement is declared or not, the module runs in strict mode by default. The export statement cannot be used in embedded scripts.

Named export

By prefixing the declaration with the export keyword, a module can export multiple contents. These exported contents are distinguished by name and are called named exports.

// Export a single feature (you can export var, let, const)
export let name = "Xiao Ming";
export function sayHi(name) {
    console.log(`Hello, ${name}!`);
}
export class Sample {
    ...
}

Or export pre-defined properties

let name = "Xiao Ming";
const age = 18;
function sayHi(name) {
    console.log(`Hello, ${name}!`);
}
export {name, age, sayHi}

You can also specify an alias when exporting, which must be specified in the brace syntax of the export clause. Therefore, providing aliases for declared, exported, and non exported values cannot be done on one line.

export {name as username, age, sayHi}

However, the export statement must be at the top level of the module and cannot be nested in a block:

// allow
export ...
// not allow
if (condition) {
    export ...
}

Default export

The default export is as if the module is the same as the exported value. Default export uses the default keyword to declare a value as the default export. Each module can only have one default export. Duplicate default exports result in SyntaxError. As follows:

// Export pre-defined properties as defaults
export default {
    name: "Xiao Ming",
    age: 18,
    sex: "boy"
};
export {sayHi as default}    // The ES 6 module recognizes the default keyword provided as an alias. At this time, although the corresponding value is exported using naming syntax, it will actually be called default export, which is equivalent to export default function sayHi() {}
// Export individual properties as defaults
export default function () {...}
export default class {...}

The ES6 specification limits what can be used and what can not be used in different forms of export statements. Some forms allow declarations and assignments, some allow only expressions, and some allow only simple identifiers. Note that some forms use semicolons, while others do not.

Several export forms that can cause errors are listed below:

// Different forms of errors:
// Variable declarations cannot appear in inline default exports
export default const name = 'Xiao Liu';
// Only identifiers can appear in the export clause
export { 123 as name }
// Aliases can only appear in the export clause
export const name = 'Xiao Hong' as uname;

Note: declaration, assignment and export identifiers should preferably be separated. This is not easy to make mistakes. At the same time, export statements can be concentrated in one piece. Moreover, variables, functions, or classes that are not exported by the export keyword remain private within the module.

Module redirection

Module imported values can also be exported again. In this way, multiple exports of multiple modules in the parent module set can be performed. You can use the export from syntax to implement:

export {default as m1, name} from './module1.js'
// Equivalent to
import {default as m1, name} from "./module1.js"
export {m1, name}

The default export of an external module can also be reused as the default export of the current module:

export { default } from './module1.js';

You can also modify the import module to the default export when re exporting, as shown below:

export { name as default } from './module1.js';

To export all names, you can use the following syntax:

export * from './module1.js';

This syntax ignores the default export. However, this syntax should also pay attention to whether the export names conflict. As follows:

// module1.js
export const name = "module1:name";
// module2.js
export * from './mudule1.js'
export const name = "module2:name";
// index.js
import { name } from './module2.js';
console.log(name); // module2:name

The final output is the value in module2.js, and this "rewrite" occurs silently.

Import

After the external interface of the module is defined with the export keyword, other modules can load the module through the import keyword. However, similar to export, import must also appear at the top level of the module:

// allow
import ...
// not allow
if (condition) {
    import ...
}

The module identifier can be a relative path to the current module or an absolute path to the module file. It must be a pure string and cannot be the result of dynamic calculation. For example, it cannot be a concatenated string.

When you use export to name an export, you can use * to batch obtain and assign an alias to the saved export set without listing each identifier:

const name = "Xiao Ming", age = 18, sex = "boy";
export {name, age, sex}

// The above named export can be imported in the following form (the above code is in module1.js module)
import * as Sample from "./module1.js"
console.log(`My name is ${Sample.name}, A ${Sample.sex},${Sample.age} years old.`);

You can also import by name, just put the name in {}:

import {name, sex as s, age} from "./module1.js";
console.log(`My name is ${name}, A ${s},${age} years old.`);

Import import adopts the Singleton mode. When importing the same module multiple times, only one instance of the module will be introduced:

import {name, age} from "./module1.js";
import {sex as s} from "./module1.js";
// Equivalent to, and only one module1.js instance will be introduced
import {name, sex as s, age} from "./module1.js";

If the default export is used, you can use the default keyword and provide an alias to import, or you can directly use the identifier, which is the alias of the default export:

import {default as Sample} from "./module1.js"
// Equivalent to the following
import Sample from "./module1.js"

The module has both named export and default export, which can be imported in the import statement at the same time. The following three methods are equivalent.

import Sample, {sayHi} from "./module1.js"
import {default as Sample, sayHi} from "./module1.js"
import Sample, * as M1 from "./module1.js"

Of course, you can also import the whole module as a side effect without importing specific content in the module. This will run the global code in the module, but will not actually import any values.

import './module1.js'

The value imported by import is bound to the value exported by export, and the binding is immutable. Therefore, import is read-only to the imported module. However, you can achieve this by calling the functions of the imported module.

import Sample, * as M1 from "./module1.js"
Sample = "Modify Sample";    // error
M1.module1 = "Module 1";    // error
Sample.name = "Xiao Liang";       // allow

The advantage of this is that it can support circular dependency, and a large module can be disassembled into several small modules, as long as you don't try to modify the imported value.

Note: if you want to load a module natively in the browser, the file must have a. js extension, otherwise it may not be parsed. If you use a build tool or a third-party module loader to package or parse ES6 modules, you may not need to include an extension.

import()

The standard import keyword import module is static, which enables all imported modules to be compiled when loaded. The latest ES11 standard introduces the dynamic import function import(), which does not need to load all modules in advance. This function takes the path of the module as a parameter and returns a Promise. The loaded module is used in its then callback:

import ('./module1.mjs')
    .then((module) => {
        // Do something with the module.
    });

This use also supports the await keyword.

let module = await import('./module1.js');

The usage scenario of import() is as follows:

  • Load on demand.
  • Dynamic build module path.
  • Conditional loading.

load

ES6 modules can be loaded either natively through the browser or together with third-party loaders and build tools.

Browsers that fully support ES6 modules can asynchronously load the entire dependency graph from the top-level module. The browser will parse the entry module, determine the dependency, and send a request for the dependent module. After these files are returned through the network, the browser will parse their contents and confirm the dependency. If the secondary dependency has not been loaded, it will send more requests. This asynchronous recursive loading process will continue until the whole dependency graph is resolved. After resolving the dependency, the application can formally load the module.

The module file is loaded on demand, and the requests of subsequent modules are delayed due to the network delay of each dependent module. That is, module1 depends on module2, and module2 depends on module3. The browser does not know to request module 3 until the request for module 2 is completed. This method is efficient and does not require external tools, but it may take a long time to load the depth dependency map of large applications.

HTML

To use ES6 module in HTML pages, you need to put the type="module" attribute in the < script > tag to declare that the code contained in the < script > is executed as a module in the browser. It can be embedded in web pages or imported as an external file:

<script type="module">
    // Module code
</script>
<script type="module" src="./module1.js"></script>

< script type = "module" > modules are loaded in the same order as the scripts loaded by < script defer >. However, the execution will be delayed until the document parsing is completed, but the execution order is the order in which < script type = "module" > appears on the page.

You can also add async attribute to the module tag. The impact is twofold. Not only is the module execution order no longer bound to the order of < script > tags in the page, but also the module will not wait for the document to be parsed. However, the entry module must wait for its dependencies to load.

Worker

In order to support the ES6 module, the Worker can receive the second parameter in the Worker constructor. The default value of its type attribute is classic. You can set the type to module to load the module file. As follows:

// The second parameter defaults to {type: 'classic'}
const scriptWorker = new Worker('scriptWorker.js');
const moduleWorker = new Worker('moduleWorker.js', { type: 'module' });

Within module based workers, the self.importScripts() method is usually used to load external scripts in script based workers, and calling it will throw an error. This is because the import behavior of the module contains importScripts().

Backward compatibility

If the browser supports ES6 module natively, it can be used directly, while unsupported browsers can use the third-party module system (System.js) or translate ES6 module during construction.

Script modules can be set with the type="module" attribute, while browsers that do not support modules can use the nomodule attribute. This property notifies browsers that support ES6 modules not to execute scripts. The property is not recognized by browsers that do not support modules and is ignored. As follows:

// The browser that supports the module will execute this script
// Browsers that do not support modules will not execute this script
<script type="module" src="module.js"></script>
// Browsers that support modules will not execute this script
// Browsers that do not support modules will execute this script
<script nomodule src="script.js"></script>

summary

ES6 supports modules at the language level, ending the long-term division of CommonJS and AMD module loaders, redefining module functions, integrating the two specifications and exposing them through simple syntax declarations.

Modules use different ways to load. js files, which are very different from scripts:

  1. The module always uses use strict to execute strict mode.
  2. Variables created in the module's top-level scope will not be automatically added to the shared global scope. They only exist inside the module's top-level scope.
  3. this value of the module top-level scope is undefined.
  4. The module does not allow HTML style comments in code.
  5. For content that needs to be accessed by external code of the module, the module must export them.
  6. Allow modules to import bindings from other modules.
  7. The module code is executed once. The export is created only once and then shared between imports.

Browser support for native modules is getting better and better, but it also provides robust tools to realize the transition from never supporting to supporting ES6 modules.

More content, please pay attention to the official account of the sea.

Topics: Javascript Front-end Programmer