🍂落页
登 录

Node.js 笔记

  • 模块:CommonJS
  • 模块:ES Modules
🍂落页
TALAXY
模块:ES Modules
🍂落页
TALAXY

模块:ES Modules

模块:ES Modules
  • 启用方式
  • import 指定
  • import 属性
  • 内置模块
  • import() 表达式
  • import.meta
  • 与 CommonJS 交互
  • JSON 模块
  • Wasm modules
  • 顶层 await
  • HTTPS 及 HTTP 导入
  • Loaders
  • 路径解析及加载算法

原文

ES Modules(ESM)是一种官方的用于模块化 JS 代码的方式。通过 import 和 export 语句来定义模块。

导入导出的例子:

// `addTwo.mjs`
function addTwo(num) {
  return num + 2;
}

export { addTwo };
// `app.mjs`
import { addTwo } from './addTwo.mjs';

// Prints: 6
console.log(addTwo(4));

Node 完全支持 ESM ,并提供了与 CommonJS 交互的方式。

启用方式

  • 通过 .mjs 扩展名来告诉 Node 这是一个 ES 模块;
  • 在 package.json 里指定 "type": "module" ;
  • 使用 --input-type 修饰符,并赋值为 "module" ;
  • 使用 --experimental-default-type 修饰符,并赋值为 "module" 。

如果没有主动告诉 Node 使用 ESM ,Node 会自己在代码中检测 ESM 相关语法。如果 Node 检测到 ESM 语法,则会将其视为 ES 模块,反之视为 CommonJS 模块。

import 指定

有三种指定模块的方式:

  • 相对引用。比如 './startup.js'、'../config.mjs' ;
  • 包名。比如 'some-package'、'some-package/shuffle' ;
  • 绝对引用。比如 'file:///opt/nodejs/config.js' 。

与 CommonJS 一样,可以通过添加相对路径来访问包内的模块文件。但如果 package.json 里指定了 exports 字段,则只能访问其列出的文件路径。

必要的文件扩展名

当通过相对或绝对引用指定导入的模块时,必须带上文件的扩展名。目录的 index 文件同样也需要指明完整的文件名,比如 './startup/index.js' 。

URL

ES 模块会以 URL 的形式解析并缓存。这意味着一些特殊字符需要进行百分号转义,比如 # 转义为 %34 。

Node 支持 file:、node:、data: 形式的 URL 。而 https 并不原生支持,需要一个自定义的 HTTPS loader 。

file: URL

如果一个模块带有不同的参数或片段,则会被重复加载:

import './foo.mjs?query=1'; // loads ./foo.mjs with query of "?query=1"
import './foo.mjs?query=2'; // loads ./foo.mjs with query of "?query=2"

卷轴目录可以通过 /、//、file:/// 引用。考虑到文件路径和 URL 之间的区别,推荐用 url.pathToFileURL 来获取一个正确的引用路径。

data: 导入

data: URLs 支持导入以下 MIME 类型:

  • text/javascript - ES 模块
  • application/json - JSON
  • application/wasm - Wasm
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' with { type: 'json' };

data: URLs only resolve bare specifiers for builtin modules and absolute specifiers. Resolving relative specifiers does not work because data: is not a special scheme. For example, attempting to load ./foo from data:text/javascript,import "./foo"; fails to resolve because there is no concept of relative resolution for data: URLs.

node: 导入

node: URLs are supported as an alternative means to load Node.js builtin modules. This URL scheme allows for builtin modules to be referenced by valid absolute URL strings.

import fs from 'node:fs/promises';

import 属性

Stability: 1.1 - Active development

内置模块

核心模块支持具名导出,也支持默认导出,默认导出会获得原先在 CommonJS 的 export 对象。默认导出可以用来修改 API ,但需要调用 module.syncBuiltinESMExports 来应用更新:

import fs, { readFileSync } from 'node:fs';
import { syncBuiltinESMExports } from 'node:module';
import { Buffer } from 'node:buffer';

fs.readFileSync = () => Buffer.from('Hello, ESM');
syncBuiltinESMExports();

fs.readFileSync === readFileSync;

import() 表达式

CommonJS 和 ESM 均支持动态 import() 调用。在 CommonJS 可以通过 import() 来引入 ES 模块。

import.meta

import.meta 包含以下属性:

import.meta.dirname

Stability: 1.2 - Release candidate

import.meta.filename

Stability: 1.2 - Release candidate

import.meta.url

  • {string} 模块的 file: URL 。

可以方便加载相对路径的文件模块:

import { readFileSync } from 'node:fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url));

import.meta.resolve(specifier)

Stability: 1.2 - Release candidate

与 CommonJS 交互

import 语句

import 语句只能在 ESM 中使用,但是可以用来导入 ESM 或 CommonJS 模块。而 CommonJS 模块可以用动态 import() 来导入 ESM 模块。

当导入 CommonJS 时,module.exports 将会作为默认导出。而具名导出并不完全是可用的,它依赖静态分析。

require

当 --experimental-require-module 开启时,CommonJS 模块可以同步加载 ES 模块。

See Loading ECMAScript modules using require() for details.

CommonJS 命名空间

import { default as cjs } from 'cjs';
import cjsSugar from 'cjs';

console.log(cjs);   // => <module.exports>
console.log(cjs === cjsSugar);  // => true

CommonJS 模块在 ESM 中的 default 导出永远指向其 module.exports 值。

"Module Namespace Exotic Object" 可通过 import * as m from 'cjs' 或动态导入获取:

import * as m from 'cjs';
console.log(m); // => [Module] { default: <module.exports> }
console.log(m === await import('cjs')); // => true

为了兼容,Node 会通过静态解析给 CommonJS 模块提供具名导出。比如:

// cjs.cjs
exports.name = 'exported';

在 ESM 中支持该模块进行具名导入:

import { name } from './cjs.cjs';
console.log(name);  // => 'exported'

import cjs from './cjs.cjs';
console.log(cjs);   // => { name: 'exported' }

import * as m from './cjs.cjs';
console.log(m);     // => [Module] { default: { name: 'exported' }, name: 'exported' }

在最后一行输出中可以看到 ES 模块的命名空间里存在一个 name 导出,它是从 moduels.exports 对象中复制而来的。

对 module.exports 的动态绑定更新不会同步到具名导出。

ESM 与 CommonJS 的不同

  • 没有 require、exports、module.exports 。
    • import 可以用来加载 CommonJS 模块;
    • 需要的时候,可以通过 module.createRequire 来构建一个 require 函数。
  • 没有 __filename、__dirname 。
  • 不支持 Addon 。不过可以通过 module.createRequire() 或 process.dlopen 加载。
  • 没有 require.resolve 。
    • 可以通过 new URL('./local', import.meta.url) 获取相对路径解析。
  • 没有 NODE_PATH 。
  • 没有 require.extensions 。可以使用模块自定义勾子。
  • 没有 require.cache 。ESM 有自己的缓存。

JSON 模块

Stability: 1 - Experimental

Wasm modules

Stability: 1 - Experimental

顶层 await

await 可以用在 ES 模块的顶层代码中:

// `a.mjs`
export const five = await Promise.resolve(5);
// `b.mjs`
import { five } from './a.mjs';

console.log(five); // => `5`

如果顶层的 await 表达式没有得到 resolve ,Node 进程会以 13 状态码退出。

HTTPS 及 HTTP 导入

Stability: 1 - Experimental

Loaders

The former Loaders documentation is now at Modules: Customization hooks.

路径解析及加载算法

功能特性

The default resolver has the following properties:

  • FileURL-based resolution as is used by ES modules
  • Relative and absolute URL resolution
  • No default extensions
  • No folder mains
  • Bare specifier package resolution lookup through node_modules
  • Does not fail on unknown extensions or protocols
  • Can optionally provide a hint of the format to the loading phase

The default loader has the following properties

  • Support for builtin module loading via node: URLs
  • Support for "inline" module loading via data: URLs
  • Support for file: module loading
  • Fails on any other URL protocol
  • Fails on unknown extensions for file: loading (supports only .cjs, .js, and .mjs)

解析算法

The algorithm to load an ES module specifier is given through the ESM_RESOLVE method below. It returns the resolved URL for a module specifier relative to a parentURL.

The resolution algorithm determines the full resolved URL for a module load, along with its suggested module format. The resolution algorithm does not determine whether the resolved URL protocol can be loaded, or whether the file extensions are permitted, instead these validations are applied by Node.js during the load phase (for example, if it was asked to load a URL that has a protocol that is not file:, data:, node:, or if --experimental-network-imports is enabled, https:).

The algorithm also tries to determine the format of the file based on the extension (see ESM_FILE_FORMAT algorithm below). If it does not recognize the file extension (eg if it is not .mjs, .cjs, or .json), then a format of undefined is returned, which will throw during the load phase.

The algorithm to determine the module format of a resolved URL is provided by ESM_FILE_FORMAT, which returns the unique module format for any file. The "module" format is returned for an ECMAScript Module, while the "commonjs" format is used to indicate loading through the legacy CommonJS loader. Additional formats such as "addon" can be extended in future updates.

In the following algorithms, all subroutine errors are propagated as errors of these top-level routines unless stated otherwise.

defaultConditions is the conditional environment name array, ["node", "import"].

The resolver can throw the following errors:

  • Invalid Module Specifier: Module specifier is an invalid URL, package name or package subpath specifier.
  • Invalid Package Configuration: package.json configuration is invalid or contains an invalid configuration.
  • Invalid Package Target: Package exports or imports define a target module for the package that is an invalid type or string target.
  • Package Path Not Exported: Package exports do not define or permit a target subpath in the package for the given module.
  • Package Import Not Defined: Package imports do not define the specifier.
  • Module Not Found: The package or module requested does not exist.
  • Unsupported Directory Import: The resolved path corresponds to a directory, which is not a supported target for module imports.

Resolution Algorithm Specification

ESM_RESOLVE(specifier, parentURL)

  1. Let resolved be undefined.
  2. If specifier is a valid URL, then
    1. Set resolved to the result of parsing and reserializing specifier as a URL.
  3. Otherwise, if specifier starts with "/", "./", or "../", then
    1. Set resolved to the URL resolution of specifier relative to parentURL.
  4. Otherwise, if specifier starts with "#", then
    1. Set resolved to the result of PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, defaultConditions).
  5. Otherwise,
    1. Note: specifier is now a bare specifier.
    2. Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL).
  6. Let format be undefined.
  7. If resolved is a "file:" URL, then
    1. If resolved contains any percent encodings of "/" or "\" ("%2F" and "%5C" respectively), then
      1. Throw an Invalid Module Specifier error.
    2. If the file at resolved is a directory, then
      1. Throw an Unsupported Directory Import error.
    3. If the file at resolved does not exist, then
      1. Throw a Module Not Found error.
    4. Set resolved to the real path of resolved, maintaining the same URL querystring and fragment components.
    5. Set format to the result of ESM_FILE_FORMAT(resolved).
  8. Otherwise,
    1. Set format the module format of the content type associated with the URL resolved.
  9. Return format and resolved to the loading phase

PACKAGE_RESOLVE(packageSpecifier, parentURL)

  1. Let packageName be undefined.
  2. If packageSpecifier is an empty string, then
    1. Throw an Invalid Module Specifier error.
  3. If packageSpecifier is a Node.js builtin module name, then
    1. Return the string "node:" concatenated with packageSpecifier.
  4. If packageSpecifier does not start with "@", then
    1. Set packageName to the substring of packageSpecifier until the first "/" separator or the end of the string.
  5. Otherwise,
    1. If packageSpecifier does not contain a "/" separator, then
      1. Throw an Invalid Module Specifier error.
    2. Set packageName to the substring of packageSpecifier until the second "/" separator or the end of the string.
  6. If packageName starts with "." or contains "\" or "%", then
    1. Throw an Invalid Module Specifier error.
  7. Let packageSubpath be "." concatenated with the substring of packageSpecifier from the position at the length of packageName.
  8. If packageSubpath ends in "/", then
    1. Throw an Invalid Module Specifier error.
  9. Let selfUrl be the result of PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL).
  10. If selfUrl is not undefined, return selfUrl.
  11. While parentURL is not the file system root,
    1. Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL.
    2. Set parentURL to the parent folder URL of parentURL.
    3. If the folder at packageURL does not exist, then
      1. Continue the next loop iteration.
    4. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
    5. If pjson is not null and pjson.exports is not null or undefined, then
      1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
    6. Otherwise, if packageSubpath is equal to ".", then
      1. If pjson.main is a string, then
        1. Return the URL resolution of main in packageURL.
    7. Otherwise,
      1. Return the URL resolution of packageSubpath in packageURL.
  12. Throw a Module Not Found error.

Customizing ESM specifier resolution algorithm

Module customization hooks provide a mechanism for customizing the ESM specifier resolution algorithm. An example that provides CommonJS-style resolution for ESM specifiers is commonjs-extension-resolution-loader.

TALAXY 落于 2024年3月21日 。