Many programming languages have the concept of modules, and JavaScript is no exception, but prior to the publication of the ECMAScript 2015 specification, JavaScript had no language-level module syntax. Module is actually a code reuse mechanism. To achieve code reuse, it is necessary to divide different functions into different files. How to use the functions defined in these files in other files? Prior to ECMAScript 2015, Web developers had to look beyond JavaScript syntax for solutions such as: Module loading tools such as SystemJS and RequireJS, and module packaging tools such as Webpack and Browserify are also used by developers. With the release of ECMAScript 2015, JavaScript now has a language-level module syntax called ECMAScript Modules, or ES Modules for short, which makes it easy for Web developers to create and use modules.

In this article, we will introduce the basic usage of ES Modules, the features of ES Modules, and how to use ES modules in a browser.

The basic grammar

ES Modules is the standard system of modules for JavaScript. A module is a simple JavaScript file that contains the export or import keyword. Export is used to export content declared in modules, and import is used to import content from other modules.

Module export 4 ways to write

The keyword used for module export is export, which can only be used at the top level of the module. Modules can export functions, classes, or other primitive types. Module exports can be written in four ways

  • The default is derived
export default function myFunc() {}
export default function () {}
export default class MyClass {}
export { foo as default }
export default 'Hello Es modules! '
Copy the code
  • Inline named export
export function myFunc() {}
export class MyClass {}
export const fooStr = 'Hello Es modules! '
Copy the code
  • Batch named exports through an export clause
function myFunc() {}
class MyClass {}
const fooStr = 'Hello Es modules! '

export {myFunc, MyClass , fooStr } // Export multiple at once in this place
Copy the code
  • To export
// Re-export other_module except for the default export
export * from './other_module.js'
// Re-export the default export in other_module
export { default } from './other_module.js'
// Re-export the default export in other_module and rename sayName in other_module to getName
export { default, sayName as getName } from './other_module.js'
Copy the code

Although there are four ways to write a module export, there are only two ways, one is the default export, the other is named export, in the same module can have more than one named export, the default export can only have one, the two ways can be mixed.

In software development, there are usually several ways to do the same thing, but not all of them are recommended, and module exports are similar. If you have default exports, inline named exports, and batch named exports in the export clause in the same module, your module is likely to get messy. Here I recommend using the default export and putting export Default at the end of the module. If you must name your exports, I recommend batch naming them using the export clause and placing the export clause at the end of the file.

Module specifier in 3

After module exports, it is natural to introduce module imports, but I decided to introduce module specifiers first, because module imports depend on module specifiers. Specifiers are string literals that represent the path to import a module. There are three types of specifiers: relative path, absolute path, and bare mode.

  • Relative paths
import foo from './myModule.js'
import { sayName } from '.. /other_module.js'
Copy the code

The relative path specifiers are /,./,.. /, do not omit the file extension when using the relative path specifier. When using a relative path to import a module in a Web project, you may omit the file extension and it will still work because your project uses a module packaging tool like WebPack.

  • An absolute path
import React from 'https://cdn.skypack.dev/react'
Copy the code

The above code indicates importing a module from the CDN. When importing a module using an absolute path, whether the file extension can be omitted depends on the server configuration.

  • Bare mode
import React from 'react'
import Foo from 'react/lib.js'
Copy the code

Bare mode imports modules from node_module. In Web project development, using this specifier to import modules is common, but ES Modules does not support it. In your project, you can use it because your project uses a module packaging tool like Webpack.

So far, I’ve covered three types of module specifiers. ES Modules only supports two of them: relative and absolute paths.

Module import 6 notation

The keyword used for module import is import. Import, like export, can only be used at the top of the module. The module specifier cannot contain variables, which must be a fixed string literal. Module imports are written in six different ways, as follows:

  • The default import
// You can change myFunc to any variable name you like
import myFunc from './myModule.js'
Copy the code
  • Import the module as an object (i.e., namespace import)
import * as api from './myModule.js'
// Access the default export in mymodule.js via the object's default attribute
console.log(api.default)
Copy the code
  • After the import
// Import fooStr in mymodule.js
import { fooStr } from './myModule.js'
// Name the default export in mymodule.js myFunc
import { default as myFunc } './myModule.js'
// 将 myModule.js中的 fooStr 命名为 myStr
import { fooStr as myStr } from './myModule.js'
Copy the code

When a module exports a lot of content and you only need a portion of the exported content, you can use this notation to import only what you need, which is crucial when doing tree shaking.

  • Only load modules, not import anything
import './myModule.js'
Copy the code

Nothing in myModule.js is imported into the current module, but the body of the myModule.js module is executed, which is usually used to perform some initialization operations.

  • Mix default imports with named imports
import myFunc, { fooStr  } from './myModule.js'
Copy the code
  • Mix default imports with namespace imports
import myFunc, * as api from './myModule.js'
Copy the code

Addendum: The same module can be imported more than once, but its module body is executed only once

Four features of ES Modules

An import is a read-only reference to an export

For example, module A exports A variable count, and module B imports the count of module A. Count is read-only for module B, so it cannot change count directly in module B, as shown in the following code:

// The code for module A is as follows:
export var count = 0 // Note that the var keyword is used here

// The code for module B is as follows:
import { count  } from './moduleA.js'
count++ // Uncaught TypeError: Assignment to constant variable
Copy the code

Run the above code in a browser and the browser will report an error of type TypeError. If the object obj is exported by module A, it cannot be directly assigned to obj in module B, but attributes in OBj can be added, deleted, or changed.

Now that I’ve covered what read-only means, here’s what reference means. A reference means that multiple modules in a project use the same variable, for example: Module B and module C both import the count and changeCount functions of module A. Module B changes the value of count through changeCount, and the count in module C will be changed together. The code is as follows:

// The code for module A is as follows:
export var count = 0 
export function changeCount() {
	count++
}

// The code for module B is as follows:
import { count, changeCount } from './moduleA.js'
changeCount ()
console.log(count) / / 1

// The code for module C is as follows:
import { count } from './moduleA.js'
console.log(count) / / 1
Copy the code

Modules B and C import references, not copies, and the variables exported by the module are a singleton throughout the project.

Support for circular dependencies

Cyclic dependency is when two modules depend on each other, for example, module A imports module B, and module B imports module A. Although ES modules support circular dependencies, they should be avoided because they strongly couple the two modules. ES modules support circular dependencies because imports are read-only references to exports.

Imports will be promoted

If you know JavaScript function enhancements, you can easily understand the import enhancements of ES Modules. Since the import of ES Modules is promoted to the beginning of the module’s scope, you don’t need to import it before using it. The following code will work:

foo()
import foo from './myModule.js'
Copy the code

Exports and static imports must be at the top level of the module

The fact that exports must be at the top of a module goes without saying. Dynamic imports have been added to the ECMAScript 2020 specification so that module imports don’t have to be at the top of a module. Dynamic imports are covered separately later, but static imports are covered here.

Before ECMAScript 2020, JavaScript ES Modules were a static module system, which meant that module dependencies were determined when you wrote code, rather than at runtime. This allowed code packaging tools like Webpack, It is easy to analyze the dependency in ES module, which provides convenience for tree shaking optimization.

Even though ECMAScript 2020 adds dynamic imports, static and dynamic imports are written differently, with static imports using the import keyword and dynamic imports using import(). Static imports can only be at the top level of the module.

Differences between modules and regular JavaScript scripts

  • The module runs in strict mode
  • Modules have lexical top scope

Variables created in a module, such as: foo, cannot be accessed through window.foo. The code is as follows:

var foo = 'hi'
console.log(window.foo) // undefined
console.log(foo) // hi
export {} // Mark this file as a module
Copy the code

Variables declared in a module are specific to that module, which means that any variables declared in a module are not available to other modules unless they are explicitly exported.

  • The this keyword in the module does not refer to globalThis, it is undefined. To access globalThis in the module, use globalThis, which is the window object in the browser.
  • Export and static import import can only be used in modules
  • The await keyword can be used at the top level of a module, whereas in normal JavaScript scripts you can only use the await keyword in async functions

Note: Because JavaScript runtime treats modules differently from regular JavaScript scripts, make sure to display flags when you write your code that the JavaScript file is a module. As long as the JavaScript file contains the export or import keyword, The JavaScript runtime considers this file to be a module

The five differences covered in this section are differences that are independent of the JavaScript runtime environment, and the use of ES Modules in the browser will be covered in later sections, where some of the new differences will be added.

Use ES Modules in your browser

Modern browsers support ES modules. You can tell the browser that the script is a module by setting the type attribute of the script tag to module as follows:

<! -- External module -->
<script type="module" src="./module.js"></script>
<! -- Inline module -->
<script type="module">
   import {count} from './moduleA.js';
   import React from 'https://cdn.skypack.dev/react'
   console.log(count, React)
</script>
Copy the code

For compatibility reasons, you may also need , which I won’t cover here.

Having covered the differences between modules and regular JavaScript scripts that are independent of the runtime environment, let’s look at the differences in the browser environment

  • Modules are executed only once

No matter how many times a module is introduced, it is executed only once, whereas a regular JavaScript script executes as many times as it is added to the DOM. For example, here is the code:

<! -- External module -->
<script type="module" src="./module.js"></script>
 <script type="module" src="./module.js"></script>
 <script type="module" src="./module.js"></script>
<! -- Inline module -->
<script type="module">
        import { count } from './module.js';
</script>
<script src='./classic.js'></script>
<script src='./classic.js'></script>
Copy the code

Module.js will only be executed once in the above code, classic.js will be executed twice

  • Downloading module scripts does not block HTML parsing

By default, when the browser downloads a regular external script, it pauses parsing the HTML. We can add the defer attribute to the script tag so that the browser does not pause parsing the HTML during the script download. When the module script is downloaded, the browser default module script is defer.

The following figure shows the process for the browser to get external module scripts and regular scripts

Above is the result of (html.spec.whatwg.org/multipage/s…).

  • The module script is obtained from CORS

Module scripts and their dependencies are obtained through CORS. This problem needs special attention when obtaining a cross-domain module script. The cross-domain response header access-Control-allow-Origin must contain the current domain, otherwise the module will fail to obtain the domain.

To ensure that browsers always have credentials (cookies) on them when obtaining scripts from the same source module, it is recommended to add the Crossorigin attribute to script tags.

Dynamic import

So far, static import modules have been introduced. Static imports must wait until all module code has been downloaded before executing the program, which may degrade the performance of the first screen rendering of the site. The dynamic import module allows users to download resources on demand based on their operations on the interface to save traffic. The dynamic import module is released in ECMAScript 2020 and requires import() as follows:

// Import through a relative path
import('./exportDefault.js').then((module) = > {
    console.log(module) // line A
})
// Import through absolute path
import('https://cdn.skypack.dev/react').then((react) = > {
    console.log(react) // line B
})
Copy the code

As can be seen from the above code, the return value of import() is a promise object. After the module is successfully loaded, the state of the promise object will become fulfilled. Import () can be used with async/await

The variables module and React identified by line A and line B in the above code are JavaScript objects. We can access any method or property exported by the module using the dot and parenthesis syntax of the object. The default export of the module is accessed by the default property name.

There are three differences between dynamic import and static import:

  • Dynamically imported module specifiers can be variables, but statically imported module specifiers can only be string literals
  • Dynamic imports can be used in modules and regular scripts, but static imports can only be used in modules
  • Dynamic imports need not be at the top of a file, but static imports can only be at the top of a module

Although there are differences between dynamically imported modules and statically imported modules, they both obtain module scripts through CORS, so when obtaining cross-domain module scripts, the access-Control-Allow-Origin response headers of the scripts must be configured correctly.

Dynamic and static imports have their own usage scenarios. Use static imports for modules to be used during initial rendering. In other cases, especially those related to user operations, dynamic imports can be used to load dependent modules on demand. This can improve the performance of the first screen rendering, but can reduce performance during user operations. So, which modules use static imports and which use dynamic imports depends on your situation.

Note: Import () is used when dynamically importing modules. It looks like a function call, but it is not a function call. It is a special syntax. MyImport ().