- 断言测试
- 异步上下文跟踪
- 异步钩子
- 缓冲(Buffer)
- C++ 插件
- 使用 Node-API 的 C/C++ 插件
- C++ 嵌入 Node环境
- 子进程(Child processes)
- 集群(Cluster)
- 命令行选项
- 控制台(Console)
- 核心包(Corepack)
- 加密(Crypto)
- 调试器(Debugger)
- 已弃用的 API
- 诊断通道(Diagnostics Channel)
- 域名系统(DNS)
- 域(Domain)
- 错误(Errors)
- 事件(Events)
- 文件系统(File system)
- 全局变量(Globals)
- HTTP
- HTTP/2
- HTTPS
- 检查器(Inspector)
- 国际化
- 模块:CommonJS 模块
- 模块:ECMAScript 模块
- 模块:
node:module
API - 模块:packages 模块
- 网络(Net)
- 系统(OS)
- 路径(Path)
- 性能挂钩(Performance hooks)
- 性能挂钩(Permissions)
- 进程(Process)
- Punycode 国际化域名编码
- 查询字符串(Query strings)
- 命令行库(Readline)
- REPL 交互式编程环境
- 诊断报告
- 单个可执行应用程序
- Stream 流
- 字符串解码器
- 单元测试
- 定时器(Timers)
- 传输层安全/SSL
- 跟踪事件
- TTY
- UDP/数据报
- URL
- 实用程序
- V8
- 虚拟机
- WebAssembly
- Web加密 API(Web Crypto API)
- 网络流 API(Web Streams API)
- 工作线程(Worker threads)
- zlib
Node.js v18.18.2 文档
- Node.js v18.18.2
- ► 目录
-
►
索引
- 断言测试
- 异步上下文跟踪
- 异步钩子
- 缓冲(Buffer)
- C++ 插件
- 使用 Node-API 的 C/C++ 插件
- C++ 嵌入 Node环境
- 子进程(Child processes)
- 集群(Cluster)
- 命令行选项
- 控制台(Console)
- 核心包(Corepack)
- 加密(Crypto)
- 调试器(Debugger)
- 已弃用的 API
- 诊断通道(Diagnostics Channel)
- 域名系统(DNS)
- 域(Domain)
- 错误(Errors)
- 事件(Events)
- 文件系统(File system)
- 全局变量(Globals)
- HTTP
- HTTP/2
- HTTPS
- 检查器(Inspector)
- 国际化
- 模块:CommonJS 模块
- 模块:ECMAScript 模块
- 模块:
node:module
API - 模块:packages 模块
- 网络(Net)
- 系统(OS)
- 路径(Path)
- 性能挂钩(Performance hooks)
- 性能挂钩(Permissions)
- 进程(Process)
- Punycode 国际化域名编码
- 查询字符串(Query strings)
- 命令行库(Readline)
- REPL 交互式编程环境
- 诊断报告
- 单个可执行应用程序
- Stream 流
- 字符串解码器
- 单元测试
- 定时器(Timers)
- 传输层安全/SSL
- 跟踪事件
- TTY
- UDP/数据报
- URL
- 实用程序
- V8
- 虚拟机
- WebAssembly
- Web加密 API(Web Crypto API)
- 网络流 API(Web Streams API)
- 工作线程(Worker threads)
- zlib
- ► 其他版本
- ► 选项
目录
模块:ECMAScript 模块#
介绍#
ECMAScript 模块是打包 JavaScript 代码以供重用的官方标准格式。模块是使用各种import
和
export
语句定义的。
以下 ES 模块导出函数的示例:
// addTwo.mjs
function addTwo(num) {
return num + 2;
}
export { addTwo };
以下 ES 模块示例从addTwo.mjs
导入函数:
// app.mjs
import { addTwo } from './addTwo.mjs';
// Prints: 6
console.log(addTwo(4));
Node.js 完全支持当前指定的 ECMAScript 模块,并提供它们与其原始模块格式 CommonJS之间的互操作性。
启用#
Node.js 有两个模块系统:CommonJS模块和 ECMAScript 模块。
作者可以通过.mjs
文件扩展名、package.json
"type"
字段或
--input-type
告诉 Node.js 使用 ECMAScript 模块加载器标志。除这些情况外,Node.js 将使用 CommonJS 模块加载器。有关更多详细信息,请参阅确定模块系统。
包#
此部分已移至模块:packages 模块。
import
说明符#
术语#
import
语句的说明符是from
关键字后面的字符串,例如import { sep } from 'node:path'
中的'node:path'
。说明符还可用于export from
语句中,并用作import()
表达式的参数
。
说明符分为三种类型:
-
相对说明符,例如
'./startup.js'
或'../config.mjs'
。它们指的是相对于导入文件位置的路径。对于这些文件,文件扩展名始终是必需的。 -
纯粹的说明符,例如
'some-package'
或'some-package/shuffle'
。它们可以通过包名称引用包的主入口点,或者按照示例分别引用包内以包名称为前缀的特定功能模块。仅对于没有"exports"
字段的包,才需要包含文件扩展名。 -
绝对说明符,例如
'file:///opt/nodejs/config.js'
。它们直接且明确地引用完整路径。
裸说明符解析由Node.js 模块解析和加载算法处理。所有其他说明符解析始终仅使用标准相对URL解析语义进行解析。
与 CommonJS 一样,可以通过在包名称后附加路径来访问包内的模块文件,除非包的package.json
包含
"exports"
字段,在这种情况下只能访问包内的文件通过"exports"
中定义的路径。
有关适用于 Node.js 模块解析中的裸说明符的包解析规则的详细信息,请参阅包文档。
强制文件扩展名#
使用import
关键字解析相对或绝对说明符时,必须提供文件扩展名。目录索引(例如'./startup/index.js'
)也必须完全指定。
此行为与import
在浏览器环境中的行为相匹配(假定服务器是典型配置的)。
URL#
ES 模块被解析并缓存为 URL。这意味着特殊字符必须采用百分比编码,例如#
与%23
和?
与%3F
。
支持file:
、node:
和data:
URL 方案。Node.js 本身不支持像'https://example.com/app.js'
这样的说明符
,除非使用自定义 HTTPS 加载器。
file:
URL#
如果用于解析模块的 import
说明符具有不同的查询或片段,则会多次加载模块。
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:
导入#
- ES 模块的
text/javascript
- JSON 的
application/json
- Wasm 的
application/wasm
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' assert { type: 'json' };
data:
URL 仅解析内置模块的裸说明符和绝对说明符。解析
相对说明符不起作用,因为data:
不是
特殊方案。例如,尝试
从data:text/javascript,import "./foo";
加载 ./foo
无法解析,因为data:
网址没有相对解析的概念。
node:
导入#
支持node:
URL 作为加载 Node.js 内置模块的替代方法。此 URL 方案允许通过有效的绝对 URL 字符串引用内置模块。
import fs from 'node:fs/promises';
导入断言#
导入断言提案为模块导入语句添加了内联语法,以便与模块说明符一起传递更多信息。
import fooData from './foo.json' assert { type: 'json' };
const { default: barData } =
await import('./bar.json', { assert: { type: 'json' } });
Node.js 支持以下type
值,对于这些值,断言是强制的:
断言type | 需要用于 |
---|---|
'json' | JSON 模块 |
内置模块#
核心模块提供其公共 API 的命名导出。还提供了默认导出,它是 CommonJS 导出的值。默认导出可用于修改命名导出等。内置模块的命名导出只能通过调用
module.syncBuiltinESMExports()
进行更新。
import EventEmitter from 'node:events';
const e = new EventEmitter();
import { readFile } from 'node:fs';
readFile('./foo.txt', (err, source) => {
if (err) {
console.error(err);
} else {
console.log(source);
}
});
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 和 ES 模块都支持动态import()
。在 CommonJS 模块中,它可用于加载 ES 模块。
import.meta
#
import.meta
元属性是包含以下属性的Object
。
import.meta.url
#
- <string>模块的绝对
file:
URL。
它的定义与浏览器中提供当前模块文件 URL 的定义完全相同。
这可以实现有用的模式,例如相对文件加载:
import { readFileSync } from 'node:fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url));
import.meta.resolve(specifier[, parent])
#
此功能仅在启用--experimental-import-meta-resolve
命令标志时可用
。
specifier
<string>相对于parent
解析的模块说明符。parent
<字符串> | <URL>要解析的绝对父模块 URL。如果未指定,则使用import.meta.url
的默认值。- 返回:< Promise >
提供作用域为每个模块的模块相对解析函数,返回 URL 字符串。
const dependencyAsset = await import.meta.resolve('component-lib/asset.css');
import.meta.resolve
还接受第二个参数,它是从中解析的父模块:
await import.meta.resolve('./dep', import.meta.url);
该函数是异步的,因为 Node.js 中的 ES 模块解析器允许异步。
与 CommonJS 的互操作性#
import
语句#
import
语句可以引用 ES 模块或 CommonJS 模块。
import
语句仅允许在 ES 模块中使用,但
CommonJS 中支持动态import()
表达式来加载 ES 模块。
导入CommonJS 模块时,
提供module.exports
对象作为默认导出。命名导出可能是可用的,由静态分析提供,以方便更好的生态系统兼容性。
require
#
CommonJS 模块require
始终将其引用的文件视为 CommonJS。
不支持使用require
加载 ES 模块,因为 ES 模块是异步执行的。相反,请使用import()
从 CommonJS 模块加载 ES 模块。
CommonJS 命名空间#
CommonJS 模块由一个可以是任何类型的module.exports
对象组成。
导入CommonJS模块时,可以使用ES模块默认导入或其相应的糖语法可靠地导入:
import { default as cjs } from 'cjs';
// The following import statement is "syntax sugar" (equivalent but sweeter)
// for `{ default as cjsSugar }` in the above import statement:
import cjsSugar from 'cjs';
console.log(cjs);
console.log(cjs === cjsSugar);
// Prints:
// <module.exports>
// true
CommonJS 模块的 ECMAScript 模块命名空间表示始终是具有指向 CommonJS
module.exports
值的 default
导出键的命名空间。
使用import * as m from 'cjs'
或动态导入时可以直接观察到此模块命名空间异国对象
:
import * as m from 'cjs';
console.log(m);
console.log(m === await import('cjs'));
// Prints:
// [Module] { default: <module.exports> }
// true
为了更好地兼容 JS 生态系统中的现有用法,Node.js 还尝试确定每个导入的 CommonJS 模块的 CommonJS 命名导出,以使用静态分析过程将它们作为单独的 ES 模块导出提供。
例如,考虑编写的 CommonJS 模块:
// cjs.cjs
exports.name = 'exported';
前面的模块支持 ES 模块中的命名导入:
import { name } from './cjs.cjs';
console.log(name);
// Prints: 'exported'
import cjs from './cjs.cjs';
console.log(cjs);
// Prints: { name: 'exported' }
import * as m from './cjs.cjs';
console.log(m);
// Prints: [Module] { default: { name: 'exported' }, name: 'exported' }
从记录的模块命名空间异国对象的最后一个示例中可以看出,当模块是进口的。
对于这些命名导出,未检测到实时绑定更新或添加到module.exports
的新导出。
命名导出的检测基于常见语法模式,但并不总是正确检测命名导出。在这些情况下,使用上述默认导入表单可能是更好的选择。
命名导出检测涵盖许多常见的导出模式、再导出模式以及构建工具和转译器输出。有关实现的确切语义,请参阅cjs-module-lexer 。
ES模块和CommonJS之间的区别#
没有require
、exports
或module.exports
#
大多数情况下,ES 模块import
可用于加载 CommonJS 模块。
如果需要,可以使用module.createRequire()
在 ES 模块内构造
require
函数。
没有__filename
或__dirname
#
这些 CommonJS 变量在 ES 模块中不可用。
__filename
和__dirname
用例可以通过import.meta.url
复制
。
没有插件加载#
ES 模块导入目前不支持插件。
它们可以使用module.createRequire()
或
process.dlopen
加载。
没有require.resolve
#
相对分辨率可以通过new URL('./local', import.meta.url)
处理。
对于完整的require.resolve
替换,有一个标记的实验性
import.meta.resolve
API。
或者可以使用module.createRequire()
。
没有NODE_PATH
#
NODE_PATH
不是解析import
说明符的一部分。如果需要此行为,请使用符号链接。
没有require.extensions
#
require.extensions
不被import
使用。期望加载器钩子将来能够提供此工作流程。
没有require.cache
#
require.cache
不被import
使用,因为 ES 模块加载器有自己独立的缓存。
JSON 模块#
JSON 文件可以通过import
引用:
import packageConfig from './package.json' assert { type: 'json' };
assert { type: 'json' }
语法是强制性的;请参阅导入断言。
导入的 JSON 仅公开default
导出。不支持命名导出。在 CommonJS 缓存中创建缓存条目以避免重复。如果 JSON 模块已从同一路径导入,则 CommonJS 中将返回相同的对象。
Wasm 模块#
在--experimental-wasm-modules
标志下支持导入 WebAssembly 模块
,允许任何.wasm
文件作为普通模块导入,同时还支持其模块导入。
此集成符合 WebAssembly 的 ES 模块集成提案。
例如,一个index.mjs
包含:
import * as M from './module.wasm';
console.log(M);
执行下:
node --experimental-wasm-modules index.mjs
将为module.wasm
的实例化提供导出接口。
顶级await
#
await
关键字可以用在 ECMAScript 模块的顶级主体中。
假设a.mjs
与
export const five = await Promise.resolve(5);
还有一个b.mjs
import { five } from './a.mjs';
console.log(five); // Logs `5`
node b.mjs # works
如果顶级await
表达式永远无法解析,则node
进程将退出并显示13
状态代码。
import { spawn } from 'node:child_process';
import { execPath } from 'node:process';
spawn(execPath, [
'--input-type=module',
'--eval',
// Never-resolving Promise:
'await new Promise(() => {})',
]).once('exit', (code) => {
console.log(code); // Logs `13`
});
HTTPS 和 HTTP 导入#
在--experimental-network-imports
标志下支持使用https:
和http:
导入基于网络的模块。这允许类似 Web 浏览器的导入在 Node.js 中工作,但由于应用程序稳定性和安全问题在特权环境而不是浏览器沙箱中运行时有所不同,因此存在一些差异。
导入仅限于 HTTP/1#
尚不支持 HTTP/2 和 HTTP/3 的自动协议协商。
HTTP 仅限于环回地址#
http:
容易受到中间人攻击,并且不允许用于 IPv4 地址127.0.0.0/8
之外的地址(127.0.0.1
到
127.255.255.255
)和 IPv6 地址::1
。对http:
的支持旨在用于本地开发。
身份验证永远不会发送到目标服务器。#
Authorization
、Cookie
和Proxy-Authorization
标头不会发送到服务器。避免在导入的 URL 中包含用户信息。正在研究在服务器上安全使用这些的安全模型。
目标服务器上从未检查 CORS#
CORS 旨在允许服务器将 API 的使用者限制为一组特定的主机。不支持这一点,因为它对于基于服务器的实现没有意义。
无法加载非网络依赖项#
这些模块无法访问不超过http:
或https:
的其他模块。要在避免安全问题的同时仍然访问本地模块,请传入对本地依赖项的引用:
// file.mjs
import worker_threads from 'node:worker_threads';
import { configure, resize } from 'https://example.com/imagelib.mjs';
configure({ worker_threads });
// https://example.com/imagelib.mjs
let worker_threads;
export function configure(opts) {
worker_threads = opts.worker_threads;
}
export function resize(img, size) {
// Perform resizing in worker_thread to avoid main thread blocking
}
默认情况下不启用基于网络的加载#
目前,需要--experimental-network-imports
标志才能通过http:
或https:
加载资源。将来,将使用不同的机制来强制执行此操作。需要选择加入才能防止传递依赖项无意中使用可能影响 Node.js 应用程序可靠性的潜在可变状态。
加载器#
该 API 目前正在重新设计,并且仍将发生变化。
要自定义默认模块解析,可以选择通过Node.js 的--experimental-loader ./loader-name.mjs
参数提供加载器挂钩。
使用挂钩时,它们适用于入口点和所有import
调用。它们不适用于require
调用;那些仍然遵循CommonJS规则。
加载器遵循--require
的模式:
node \
--experimental-loader unpkg \
--experimental-loader http-to-https \
--experimental-loader cache-buster
它们按以下顺序调用:cache-buster
调用
http-to-https
,后者又调用unpkg
。
挂钩#
钩子是链的一部分,即使该链仅由一个自定义(用户提供的)钩子和始终存在的默认钩子组成。钩子函数嵌套:每个钩子函数必须始终返回一个普通对象,并且链接是由于每个函数调用next<hookName>()
而发生的,它是对后续加载器钩子的引用。
返回缺少必需属性的值的挂钩会触发异常。如果钩子在不调用next<hookName>()
且不返回
shortCircuit: true
的情况下返回,也会触发异常。这些错误有助于防止链条意外中断。
resolve(specifier, context, nextResolve)
#
加载器 API 正在重新设计。该钩子可能会消失或其签名可能会发生变化。不要依赖下面描述的 API。
specifier
<字符串>context
<对象>conditions
<string[]>相关package.json
的导出条件importAssertions
<对象>parentURL
<字符串> | <undefined>导入此模块的模块,如果这是 Node.js 入口点,则为 undefined
nextResolve
<Function>链中后续的resolve
挂钩,或最后一个用户提供的{{{0234}}之后的 Node.js 默认resolve
挂钩}钩- 返回:<对象>
resolve
钩子链负责解析给定模块说明符和父 URL 的文件 URL,以及可选的格式(例如'module'
)作为load
钩。如果指定了格式,则load
钩子最终负责提供最终的format
值(并且可以忽略resolve
提供的提示);如果resolve
提供了format
,则需要自定义load
挂钩,即使只是将值传递给 Node.js 默认load
钩。
模块说明符是import
语句或
import()
表达式中的字符串。
父 URL 是导入该模块的 URL,
如果这是应用程序的主入口点,则为undefined
。
context
中的conditions
属性是适用于此解析请求的包导出条件的条件数组
。它们可用于在其他地方查找条件映射或在调用默认解析逻辑时修改列表。
当前包导出条件始终位于传递给挂钩的context.conditions
数组中。为了保证调用defaultResolve
时默认的 Node.js 模块说明符解析行为,
传递给它的context.conditions
数组必须包含最初传递到的context.conditions
数组的所有元素
resolve
钩子。
export async function resolve(specifier, context, nextResolve) {
const { parentURL = null } = context;
if (Math.random() > 0.5) { // Some condition.
// For some or all specifiers, do some custom logic for resolving.
// Always return an object of the form {url: <string>}.
return {
shortCircuit: true,
url: parentURL ?
new URL(specifier, parentURL).href :
new URL(specifier).href,
};
}
if (Math.random() < 0.5) { // Another condition.
// When calling `defaultResolve`, the arguments can be modified. In this
// case it's adding another value for matching conditional exports.
return nextResolve(specifier, {
...context,
conditions: [...context.conditions, 'another-condition'],
});
}
// Defer to the next hook in the chain, which would be the
// Node.js default resolve if this is the last user-specified loader.
return nextResolve(specifier);
}
load(url, context, nextLoad)
#
加载器 API 正在重新设计。该钩子可能会消失或其签名可能会发生变化。不要依赖下面描述的 API。
在此 API 的早期版本中,它被分为 3 个独立的钩子(现已弃用)(
getFormat
、getSource
和transformSource
)。
url
<string>resolve
链返回的 URLcontext
<对象>conditions
<string[]>相关package.json
的导出条件format
<字符串> | <空> | <undefined>由resolve
钩子链可选提供的格式importAssertions
<对象>
nextLoad
<Function>链中后续的load
挂钩,或最后一个用户提供的{{{0281}}之后的 Node.js 默认load
挂钩}钩- 返回:<对象>
load
挂钩提供了一种定义自定义方法的方式,以确定应如何解释、检索和解析 URL。它还负责验证导入断言。
format
的最终值必须是以下值之一:
format | 描述 | load 返回的source 可接受的类型 |
---|---|---|
'builtin' | 加载 Node.js 内置模块 | 不适用 |
'commonjs' | 加载 Node.js CommonJS 模块 | 不适用 |
'json' | 加载 JSON 文件 | { string , ArrayBuffer , TypedArray } |
'module' | 加载ES模块 | { string , ArrayBuffer , TypedArray } |
'wasm' | 加载 WebAssembly 模块 | { ArrayBuffer , TypedArray } |
对于类型'builtin'
,source
的值将被忽略,因为目前无法替换 Node.js 内置(核心)模块的值。对于类型'commonjs'
,source
的值将被忽略,因为 CommonJS 模块加载器没有为 ES 模块加载器提供覆盖CommonJS 模块返回值的机制
。这个限制将来可能会被克服。
警告:ESM
load
挂钩和 CommonJS 模块的命名空间导出不兼容。尝试将它们一起使用将导致导入时出现空对象。这可能会在未来得到解决。
这些类型都对应于 ECMAScript 中定义的类。
- 特定的
ArrayBuffer
对象是SharedArrayBuffer
。 - 特定的
TypedArray
对象是Uint8Array
。
如果基于文本的格式(即'json'
、'module'
)的源值不是字符串,则使用util.TextDecoder
将其转换为字符串。
load
挂钩提供了一种定义自定义方法来检索 ES 模块说明符源代码的方法。这将允许加载器潜在地避免从磁盘读取文件。它还可用于将无法识别的格式映射到受支持的格式,例如将yaml
映射到module
。
export async function load(url, context, nextLoad) {
const { format } = context;
if (Math.random() > 0.5) { // Some condition
/*
For some or all URLs, do some custom logic for retrieving the source.
Always return an object of the form {
format: <string>,
source: <string|buffer>,
}.
*/
return {
format,
shortCircuit: true,
source: '...',
};
}
// Defer to the next hook in the chain.
return nextLoad(url);
}
在更高级的场景中,这还可以用于将不受支持的源转换为受支持的源(请参见下面的示例)。
globalPreload()
#
加载器 API 正在重新设计。该钩子可能会消失或其签名可能会发生变化。不要依赖下面描述的 API。
在此 API 的早期版本中,此挂钩名为
getGlobalPreloadCode
。
有时可能需要在应用程序运行的同一全局范围内运行一些代码。此挂钩允许返回在启动时作为草率模式脚本运行的字符串。
与 CommonJS 包装器的工作方式类似,代码在隐式函数作用域中运行。唯一的参数是类似require
的函数,可用于加载诸如 "fs": getBuiltin(request: string)
之类的内置函数。
如果代码需要更高级的require
功能,则必须使用 module.createRequire()
构建自己的 require
。
export function globalPreload(context) {
return `\
globalThis.someInjectedProperty = 42;
console.log('I just set some globals!');
const { createRequire } = getBuiltin('module');
const { cwd } = getBuiltin('process');
const require = createRequire(cwd() + '/<preload>');
// [...]
`;
}
为了允许应用程序和加载器之间进行通信,向预加载代码提供了另一个参数:port
。这可以作为加载器钩子的参数以及钩子返回的源文本内部。必须小心才能正确调用port.ref()
和
port.unref()
,以防止进程处于无法正常关闭的状态。
/**
* This example has the application context send a message to the loader
* and sends the message back to the application context
*/
export function globalPreload({ port }) {
port.onmessage = (evt) => {
port.postMessage(evt.data);
};
return `\
port.postMessage('console.log("I went to the Loader and back");');
port.onmessage = (evt) => {
eval(evt.data);
};
`;
}
例子#
各种加载器挂钩可以一起使用来完成 Node.js 代码加载和评估行为的广泛自定义。
HTTPS 加载程序#
在当前的 Node.js 中,以https://
开头的说明符是实验性的(请参阅
HTTPS 和 HTTP 导入)。
下面的加载器注册钩子以启用对此类说明符的基本支持。虽然这看起来像是对 Node.js 核心功能的重大改进,但实际使用此加载器有很大的缺点:性能比从磁盘加载文件慢得多,没有缓存,而且没有安全性。
// https-loader.mjs
import { get } from 'node:https';
export function load(url, context, nextLoad) {
// For JavaScript to be loaded over the network, we need to fetch and
// return it.
if (url.startsWith('https://')) {
return new Promise((resolve, reject) => {
get(url, (res) => {
let data = '';
res.setEncoding('utf8');
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve({
// This example assumes all network-provided JavaScript is ES module
// code.
format: 'module',
shortCircuit: true,
source: data,
}));
}).on('error', (err) => reject(err));
});
}
// Let Node.js handle all other URLs.
return nextLoad(url);
}
// main.mjs
import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';
console.log(VERSION);
使用前面的加载程序,运行
node --experimental-loader ./https-loader.mjs ./main.mjs
会打印main.mjs
中 URL 处每个模块的 CoffeeScript 的当前版本
。
转译加载器#
Node.js 无法理解的源格式可以使用load
钩子转换为 JavaScript 。
这比运行 Node.js 之前转译源文件的性能要差;转译加载器只能用于开发和测试目的。
// coffeescript-loader.mjs
import { readFile } from 'node:fs/promises';
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import CoffeeScript from 'coffeescript';
const baseURL = pathToFileURL(`${cwd()}/`).href;
export async function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
// Now that we patched resolve to let CoffeeScript URLs through, we need to
// tell Node.js what format such URLs should be interpreted as. Because
// CoffeeScript transpiles into JavaScript, it should be one of the two
// JavaScript formats: 'commonjs' or 'module'.
// CoffeeScript files can be either CommonJS or ES modules, so we want any
// CoffeeScript file to be treated by Node.js the same as a .js file at the
// same location. To determine how Node.js would interpret an arbitrary .js
// file, search up the file system for the nearest parent package.json file
// and read its "type" field.
const format = await getPackageType(url);
// When a hook returns a format of 'commonjs', `source` is ignored.
// To handle CommonJS files, a handler needs to be registered with
// `require.extensions` in order to process the files with the CommonJS
// loader. Avoiding the need for a separate CommonJS handler is a future
// enhancement planned for ES module loaders.
if (format === 'commonjs') {
return {
format,
shortCircuit: true,
};
}
const { source: rawSource } = await nextLoad(url, { ...context, format });
// This hook converts CoffeeScript source code into JavaScript source code
// for all imported CoffeeScript files.
const transformedSource = coffeeCompile(rawSource.toString(), url);
return {
format,
shortCircuit: true,
source: transformedSource,
};
}
// Let Node.js handle all other URLs.
return nextLoad(url);
}
async function getPackageType(url) {
// `url` is only a file path during the first iteration when passed the
// resolved url from the load() hook
// an actual file path from load() will contain a file extension as it's
// required by the spec
// this simple truthy check for whether `url` contains a file extension will
// work for most projects but does not cover some edge-cases (such as
// extensionless files or a url ending in a trailing space)
const isFilePath = !!extname(url);
// If it is a file path, get the directory it's in
const dir = isFilePath ?
dirname(fileURLToPath(url)) :
url;
// Compose a file path to a package.json in the same directory,
// which may or may not exist
const packagePath = resolvePath(dir, 'package.json');
// Try to read the possibly nonexistent package.json
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});
// Ff package.json existed and contained a `type` field with a value, voila
if (type) return type;
// Otherwise, (if not at the root) continue checking the next directory up
// If at the root, stop and return false
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
}
# main.coffee
import { scream } from './scream.coffee'
console.log scream 'hello, world'
import { version } from 'node:process'
console.log "Brought to you by Node.js version #{version}"
# scream.coffee
export scream = (str) -> str.toUpperCase()
对于前面的加载器,运行
node --experimental-loader ./coffeescript-loader.mjs main.coffee
会导致main.coffee
在从磁盘加载其源代码之后、Node.js 执行它之前转换为 JavaScript;对于通过任何已加载文件的import
语句引用的任何 .coffee
、
.litcoffee
或.coffee.md
文件,依此类推。
“import map”加载器#
前两个加载器定义了load
钩子。这是通过resolve
挂钩完成工作的加载器示例。此加载程序读取一个
import-map.json
文件,该文件指定要覆盖另一个 URL 的说明符(这是“导入映射”规范的一小部分的非常简单的实现)。
// import-map-loader.js
import fs from 'node:fs/promises';
const { imports } = JSON.parse(await fs.readFile('import-map.json'));
export async function resolve(specifier, context, nextResolve) {
if (Object.hasOwn(imports, specifier)) {
return nextResolve(imports[specifier], context);
}
return nextResolve(specifier, context);
}
假设我们有这些文件:
// main.js
import 'a-module';
// import-map.json
{
"imports": {
"a-module": "./some-module.js"
}
}
// some-module.js
console.log('some module!');
如果运行node --experimental-loader ./import-map-loader.js main.js
,
输出将为some module!
。
分辨率和加载算法#
特征#
默认解析器具有以下属性:
- ES 模块使用基于 FileURL 的解析
- 相对和绝对 URL 解析
- 没有默认扩展名
- 无文件夹电源
- 通过node_modules进行裸说明符包解析查找
- 在未知扩展或协议上不会失败
- 可以选择向加载阶段提供格式提示
默认加载器具有以下属性
- 支持通过
node:
URL加载内置模块 - 支持通过
data:
URL加载“内联”模块 - 支持
file:
模块加载 - 在任何其他 URL 协议上失败
- 加载
file:
的未知扩展失败(仅支持.cjs
、.js
和.mjs
)
分辨率算法#
加载 ES 模块说明符的算法通过 下面的ESM_RESOLVE方法给出。它返回相对于父 URL 的模块说明符的已解析 URL。
解析算法确定模块加载的完整解析 URL 及其建议的模块格式。解析算法不会确定是否可以加载已解析的 URL 协议,或者是否允许文件扩展名,而是由 Node.js 在加载阶段应用这些验证(例如,如果要求加载具有以下扩展名的 URL)不是file:
、data:
、node:
的协议,或者如果
启用了--experimental-network-imports
则为https:
)。
该算法还尝试根据扩展名确定文件的格式(请参阅下面的ESM_FILE_FORMAT
算法)。如果它无法识别文件扩展名(例如,如果它不是.mjs
、.cjs
或
.json
),则格式为{{{0365}}返回,这将在加载阶段抛出。
ESM_FILE_FORMAT提供了确定已解析 URL 的模块格式的算法,它返回任何文件的唯一模块格式。ECMAScript 模块返回“module”格式,而“commonjs”格式用于指示通过旧版 CommonJS 加载器进行加载。其他格式(例如“addon”)可以在未来的更新中扩展。
在以下算法中,除非另有说明,所有子例程错误都将作为这些顶级例程的错误传播。
defaultConditions是条件环境名称数组
["node", "import"]
。
解析器可能会抛出以下错误:
- 模块说明符无效:模块说明符是无效的 URL、包名称或包子路径说明符。
- 包配置无效:package.json 配置无效或包含无效配置。
- 无效的包目标:包导出或导入为包定义的目标模块是无效类型或字符串目标。
- 未导出包路径:包导出不会在包中为给定模块定义或允许目标子路径。
- 未定义包导入:包导入未定义说明符。
- 找不到模块:请求的包或模块不存在。
- 不支持的目录导入:解析的路径对应的目录不是模块导入支持的目标。
解析算法规范#
ESM_RESOLVE(说明符,parentURL)
- 令resolved为undefined。
- 如果说明符是有效的 URL,则
- 将resolved设置为将说明符解析并重新序列化为URL的结果 。
- 否则,如果说明符以"/"、"./"或"../"开头,则
- 将resolved设置为说明符相对于 parentURL 的URL 解析。
- 否则,如果说明符以“#”开头,则
- 将已解析设置为PACKAGE_IMPORTS_RESOLVE(说明符、 parentURL、defaultConditions )的结果 。
- 否则,
- 注意:说明符现在是一个裸说明符。
- 设置已解析的PACKAGE_RESOLVE(说明符,parentURL )的结果 。
- 令format为undefined。
- 如果resolved是一个“file:” URL,那么
- 如果已解析包含“/”或“\”的任何百分比编码(分别为“%2F” 和“%5C”),则
- 抛出无效模块说明符错误。
- 如果解析的文件是一个目录,那么
- 抛出不支持的目录导入错误。
- 如果已解析的文件不存在,则
- 抛出“找不到模块”错误。
- 将resolved设置为resolved的真实路径,保持相同的URL查询字符串和片段组件。
- 将格式设置为ESM_FILE_FORMAT ( resolved )的结果。
- 否则,
- 设置格式与解析的URL 关联的内容类型的模块格式。
- 返回格式并解析到加载阶段
PACKAGE_RESOLVE (包说明符,父 URL )
- 让packageName为undefined。
- 如果packageSpecifier是空字符串,则
- 抛出无效模块说明符错误。
- 如果packageSpecifier是 Node.js 内置模块名称,则
- 返回与packageSpecifier连接的字符串“node:”。
- 如果packageSpecifier不以“@”开头,则
- 将packageName设置为packageSpecifier的子字符串,直到第一个 “/”分隔符或字符串末尾。
- 否则,
- 如果packageSpecifier不包含“/”分隔符,则
- 抛出无效模块说明符错误。
- 将packageName设置为packageSpecifier的子字符串 ,直到第二个“/”分隔符或字符串末尾。
- 如果packageName以“.”开头 或包含“\”或“%”,则
- 抛出无效模块说明符错误。
- 令packageSubpath为“.” 从packageName长度的位置开始与packageSpecifier的子字符串连接 。
- 如果packageSubpath以“/”结尾,那么
- 抛出无效模块说明符错误。
- 令selfUrl为PACKAGE_SELF_RESOLVE ( packageName , packageSubpath , ParentURL )的结果 。
- 如果selfUrl不是undefined,则返回selfUrl。
- 虽然parentURL不是文件系统根目录,
- 令packageURL为与packageSpecifier 连接的“node_modules/”的 URL 解析,相对于ParentURL。
- 将parentURL设置为parentURL的父文件夹URL 。
- 如果packageURL处的文件夹不存在,则
- 继续下一个循环迭代。
- 令pjson为READ_PACKAGE_JSON ( packageURL )的结果。
- 如果pjson不为null并且pjson。出口不为空或 未定义,那么
- 返回PACKAGE_EXPORTS_RESOLVE(packageURL、 packageSubpath、pjson.exports、defaultConditions )的结果。
- 否则,如果packageSubpath等于“.” , 然后
- 如果pjson.main是一个字符串,那么
- 返回packageURL中main的 URL 解析。
- 否则,
- 返回packageURL中packageSubpath的 URL 解析。
- 抛出“找不到模块”错误。
PACKAGE_SELF_RESOLVE (包名,包子路径,父 URL )
- 令packageURL为LOOKUP_PACKAGE_SCOPE ( parentURL )的结果。
- 如果packageURL为null,则
- 返回未定义。
- 令pjson为READ_PACKAGE_JSON ( packageURL )的结果。
- 如果pjson为null或pjson。导出为null或 undefined,那么
- 返回未定义。
- 如果pjson.name等于packageName,那么
- 返回PACKAGE_EXPORTS_RESOLVE(packageURL、 packageSubpath、pjson.exports、defaultConditions )的结果。
- 否则,返回undefined。
PACKAGE_EXPORTS_RESOLVE(packageURL、子路径、导出、条件)
- 如果导出是一个对象,并且其键都以“.”开头。和一个不以“.”开头的键 ,抛出无效的包配置错误。
- 如果子路径等于“.” , 然后
- 让mainExport为undefined。
- 如果导出是字符串或数组,或者不包含以“.”开头的键的对象 , 然后
- 将mainExport设置为导出。
- 否则,如果导出是包含“.”的对象 财产,那么
- 将mainExport设置为导出[ “.” ]。
- 如果mainExport不是undefined,那么
- 令resolved为PACKAGE_TARGET_RESOLVE( packageURL、mainExport、null、false、conditions )的结果。
- 如果resolved不为null或未定义,则返回resolved。
- 否则,如果导出是一个对象并且导出的所有键都以“.”开头 。, 然后
- 令matchKey为与subpath连接的字符串“./”。
- 令resolved为PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey、exports、packageURL、false、conditions )的结果。
- 如果resolved不为null或未定义,则返回resolved。
- 抛出包路径未导出错误。
PACKAGE_IMPORTS_RESOLVE(说明符、parentURL、条件)
- 断言:说明符以“#”开头。
- 如果说明符完全等于“#”或以“#/”开头,则
- 抛出无效模块说明符错误。
- 令packageURL为LOOKUP_PACKAGE_SCOPE ( parentURL )的结果。
- 如果packageURL不为null,则
- 令pjson为READ_PACKAGE_JSON ( packageURL )的结果。
- 如果pjson.imports是一个非空对象,那么
- 让resolved成为PACKAGE_IMPORTS_EXPORTS_RESOLVE的结果 ( specifier,pjson.imports,packageURL,true,conditions)。
- 如果resolved不为null或未定义,则返回resolved。
- 抛出包导入未定义错误。
PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey、matchObj、packageURL、 isImports、条件)
- 如果matchKey是matchObj的键且不包含"*",则
- 令target为matchObj [ matchKey ]的值。
- 返回PACKAGE_TARGET_RESOLVE的结果(packageURL、 target、null、isImports、conditions)。
- 令expansionKeys为仅包含单个“*”的matchObj的键列表,由排序函数PATTERN_KEY_COMPARE排序 ,该函数按特异性降序排列。
- 对于expansionKeys中的每个密钥expansionKey,执行
- 令patternBase为expansionKey的子字符串,直到但不包括第一个“*”字符。
- 如果matchKey以patternBase开头但不等于patternBase,则
- 令patternTrailer为expandKey的子字符串,该子字符串来自第一个“*”字符之后的索引。
- 如果patternTrailer的长度为零,或者matchKey以patternTrailer结尾 并且matchKey的长度大于或等于expansionKey的长度,则
- 令target为matchObj [ expansionKey ]的值。
- 令patternMatch为matchKey的子字符串,从patternBase长度的索引开始,直到matchKey的长度 减去patternTrailer的长度。
- 返回PACKAGE_TARGET_RESOLVE的结果(packageURL、 target、patternMatch、isImports、conditions)。
- 返回null。
PATTERN_KEY_COMPARE ( keyA , keyB )
- 断言:keyA以“/”结尾或仅包含一个“*”。
- 断言:keyB以“/”结尾或仅包含一个“*”。
- 如果keyA 包含"*" ,则令baseLengthA为keyA中"*"的索引加一,否则为keyA的长度。
- 如果keyB 包含"*" ,则令baseLengthB为keyB中"*"的索引加一,否则为keyB的长度。
- 如果baseLengthA大于baseLengthB,则返回 -1。
- 如果baseLengthB大于baseLengthA,则返回 1。
- 如果keyA不包含"*",则返回 1。
- 如果keyB不包含"*",则返回 -1。
- 如果keyA的长度大于keyB的长度,则返回 -1。
- 如果keyB的长度大于keyA的长度,则返回 1。
- 返回 0。
PACKAGE_TARGET_RESOLVE(packageURL、目标、patternMatch、 isImports、条件)
- 如果目标是一个字符串,那么
- 如果目标不是以“./”开头,那么
- 如果isImports为false,或者目标以“../”或 “/”开头,或者目标是有效的 URL,则
- 抛出无效的包目标错误。
- 如果patternMatch是一个字符串,那么
- 返回PACKAGE_RESOLVE(目标中每个“*”实例都 替换为patternMatch、packageURL + “/”)。
- 返回PACKAGE_RESOLVE(目标,packageURL + “/”)。
- 如果“/”或“\”上的目标拆分包含任何“”、“.” 、“..”或第一个“.”之后的“node_modules ”段 段、不区分大小写并包含百分比编码变体,会抛出无效的包目标错误。
- 令resolvedTarget为packageURL和target连接的URL 解析 。
- 断言:resolvedTarget包含在packageURL中。
- 如果patternMatch为null,那么
- 返回已解决的目标。
- 如果“/”或“\”上的patternMatch拆分包含任何“”、“.” 、 ".."或"node_modules"段,不区分大小写并包含百分比编码变体,会引发无效模块说明符错误。
- 返回resolvedTarget的URL解析,并将每个“*”实例 替换为patternMatch。
- 否则,如果target是非空对象,则
- 如果导出包含任何索引属性键(如 ECMA-262 6.1.7 Array Index中定义) ,则抛出Invalid Package Configuration错误。
- 对于target的每个属性p,按对象插入顺序为:
- 如果p等于“默认”或条件包含p的条目,则
- 令targetValue为target中p属性的值。
- 令resolved为PACKAGE_TARGET_RESOLVE( packageURL、targetValue、patternMatch、isImports、 conditions )的结果。
- 如果已解析等于未定义,则继续循环。
- 退货已解决。
- 返回未定义。
- 否则,如果target是一个数组,那么
- 如果 _target.length 为零,则返回null。
- 对于target中的每个项目targetValue,执行
- 令resolved为PACKAGE_TARGET_RESOLVE( packageURL、targetValue、patternMatch、isImports、 conditions )的结果,在任何无效的包目标错误上继续循环 。
- 如果resolved是undefined,则继续循环。
- 退货已解决。
- 返回或抛出最后的后备解决方案null返回或错误。
- 否则,如果target为null,则返回null。
- 否则抛出无效的包目标错误。
ESM_FILE_FORMAT(URL)
- 断言:url对应于现有文件。
- 如果url以".mjs"结尾,那么
- 返回“模块”。
- 如果url以".cjs"结尾,那么
- 返回“commonjs”。
- 如果url以".json"结尾,那么
- 返回“json”。
- 令packageURL为LOOKUP_PACKAGE_SCOPE ( url )的结果。
- 令pjson为READ_PACKAGE_JSON ( packageURL )的结果。
- 如果pjson?.type存在并且是"module",那么
- 如果url以".js"结尾,那么
- 返回“模块”。
- 返回未定义。
- 否则,
- 返回未定义。
LOOKUP_PACKAGE_SCOPE(URL)
- 令scopeURL为url。
- 虽然scopeURL不是文件系统根目录,
- 将scopeURL设置为scopeURL的父URL 。
- 如果scopeURL以“node_modules”路径段结尾,则返回null。
- 令pjsonURL为rangeURL中 “package.json”的解析。
- 如果pjsonURL处的文件存在,则
- 返回范围URL。
- 返回null。
READ_PACKAGE_JSON (包 URL )
- 令pjsonURL为packageURL中“package.json”的解析。
- 如果pjsonURL处的文件不存在,则
- 返回null。
- 如果packageURL处的文件未解析为有效的 JSON,则
- 抛出无效的包配置错误。
- 返回pjsonURL处文件的已解析 JSON 源。
自定义 ESM 说明符解析算法#
不要依赖这个标志。我们计划在Loaders API发展到可以通过自定义加载器实现等效功能的程度后将其删除 。
当前说明符解析不支持 CommonJS 加载器的所有默认行为。行为差异之一是文件扩展名的自动解析以及导入具有索引文件的目录的能力。
--experimental-specifier-resolution=[mode]
标志可用于自定义扩展解析算法。默认模式是explicit
,它需要向加载器提供模块的完整路径。要启用自动扩展解析并从包含索引文件的目录导入,请使用node
模式。
$ node index.mjs
success!
$ node index # Failure!
Error: Cannot find module
$ node --experimental-specifier-resolution=node index
success!