Модули: ECMAScript-модули
Введение
ECMAScript-модули — это официально установленный стандартный формат для упаковки кода JavaScript для повторного использования. Модули определяются с помощью различных import и export операторов.
Следующий пример ES-модуля экспортирует функцию:
// addTwo.mjs
function addTwo(num) {
return num + 2;
}
export { addTwo }; copy Следующий пример ES-модуля импортирует функцию из addTwo.mjs:
// app.mjs
import { addTwo } from './addTwo.mjs';
// Prints: 6
console.log(addTwo(4)); copy Node.js полностью поддерживает ECMAScript-модули в их текущем определении и обеспечивает взаимодействие между ними и его исходным форматом модулей, CommonJS.
Включение
Node.js имеет две системы модулей: модули CommonJS и ECMAScript-модули.
Авторы могут указать Node.js на использование загрузчика ECMAScript-модулей через расширение файла .mjs, поле package.json "type" или флаг --input-type. В других случаях Node.js будет использовать загрузчик модулей CommonJS. Подробнее см. Определение системы модулей.
Пакеты
Этот раздел был перемещен в Модули: Пакеты.
import Спецификаторы
Терминология
Спецификатор оператора import — это строка после ключевого слова from, например, 'node:path' в import { sep } from '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 поддерживаются. Спецификатор, такой как 'https://example.com/app.js' не поддерживается напрямую в Node.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" copy
Корень тома может быть указан через /, //, или file:///. Учитывая различия между разрешением URL и путей (например, детали кодирования по процентам), рекомендуется использовать url.pathToFileURL при импорте пути.
data: импорты
data: URL поддерживаются для импорта со следующими типами MIME:
-
text/javascriptдля ES-модулей -
application/jsonдля JSON -
application/wasmдля Wasm
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' assert { type: 'json' }; copy data: URL разрешают только «голые» спецификаторы для встроенных модулей и абсолютные спецификаторы. Разрешение относительных спецификаторов не работает, потому что data: не является специальной схемой. Например, попытка загрузить ./foo из data:text/javascript,import "./foo"; не приводит к разрешению, потому что для data: URL нет понятия относительного разрешения.
node: импорты
node: URL поддерживаются как альтернативный способ загрузки встроенных модулей Node.js. Эта схема URL позволяет ссылаться на встроенные модули с помощью допустимых абсолютных строк URL.
import fs from 'node:fs/promises'; copy
Утверждения импорта
Предложение Утверждения импорта добавляет встроенную синтаксическую конструкцию для операторов импорта модулей для передачи дополнительной информации вместе со спецификатором модуля.
import fooData from './foo.json' assert { type: 'json' };
const { default: barData } =
await import('./bar.json', { assert: { type: 'json' } }); copy Node.js поддерживает следующие значения type, для которых утверждение является обязательным:
Утверждение type
|
Необходимо для |
|---|---|
'json' |
JSON-модули |
Встроенные модули
Встроенные модули предоставляют именованные экспорты своей публичной API. Также предоставляется экспорт по умолчанию, являющийся значением CommonJS exports. Экспорт по умолчанию может использоваться, среди прочего, для изменения именованных экспортов. Именованные экспорты встроенных модулей обновляются только путем вызова module.syncBuiltinESMExports().
import EventEmitter from 'node:events'; const e = new EventEmitter(); copy
import { readFile } from 'node:fs';
readFile('./foo.txt', (err, source) => {
if (err) {
console.error(err);
} else {
console.log(source);
}
}); copy 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; copy
import() выражения
Динамический import() поддерживается как в модулях CommonJS, так и в ES-модулях. В модулях CommonJS он может использоваться для загрузки ES-модулей.
import.meta
Свойство meta import.meta — это Object, содержащее следующие свойства.
import.meta.url
-
<строка> Абсолютный
file:URL модуля.
Определяется точно так же, как в браузерах, предоставляя URL текущего файла модуля.
Это позволяет использовать полезные шаблоны, такие как загрузка файлов относительно текущего:
import { readFileSync } from 'node:fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url)); copy
import.meta.resolve(specifier[, parent])
Эта функция доступна только с включенным флагом --experimental-import-meta-resolve.
-
specifier<строка> Спецификатор модуля для разрешения относительноparent. -
parent<строка> | <URL> Абсолютный URL родительского модуля для разрешения. Если не указано, используется значениеimport.meta.urlв качестве значения по умолчанию. - Возвращает: <Promise>
Обеспечивает функцию разрешения относительно модуля, ограниченную каждым модулем, возвращая строку URL.
const dependencyAsset = await import.meta.resolve('component-lib/asset.css'); copy import.meta.resolve также принимает второй аргумент, который является родительским модулем для разрешения:
await import.meta.resolve('./dep', import.meta.url); copy Эта функция асинхронна, так как разрешитель ES-модулей в Node.js может быть асинхронным.
Взаимодействие с CommonJS
import операторы
Оператор import может ссылаться на ES-модуль или CommonJS-модуль. import операторы разрешены только в ES-модулях, но динамические import() выражения поддерживаются в CommonJS для загрузки ES-модулей.
При импорте CommonJS-модулей объект module.exports предоставляется в качестве экспорта по умолчанию. Именованные экспортные значения могут быть доступны, предоставляемые статическим анализом как удобство для лучшей совместимости экосистемы.
require
CommonJS-модуль require всегда обрабатывает файлы, на которые он ссылается, как CommonJS.
Использование require для загрузки ES-модуля не поддерживается, так как ES-модули имеют асинхронное выполнение. Вместо этого используйте import() для загрузки ES-модуля из CommonJS-модуля.
Пространства имен 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 copy Представление CommonJS-модуля в пространстве имен ECMAScript всегда является пространством имен с ключом экспорта default, указывающим на значение CommonJS module.exports.
Этот экзотический объект пространства имен модуля можно напрямую наблюдать, используя 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 copy Для лучшей совместимости с существующим использованием в экосистеме JavaScript, Node.js дополнительно пытается определить именованные экспорты CommonJS каждого импортированного CommonJS-модуля, чтобы предоставить их как отдельные экспорты ES-модулей с использованием процесса статического анализа.
Например, рассмотрим CommonJS-модуль, написанный:
// cjs.cjs exports.name = 'exported'; copy
Предыдущий модуль поддерживает именованные импорты в 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' } copy Как видно из последнего примера экзотического объекта пространства имен модуля, экспорт name копируется из объекта module.exports и устанавливается непосредственно в пространстве имен ES-модуля при импорте модуля.
Обновления или новые экспорты, добавленные в module.exports, не обнаруживаются для этих именованных экспортов.
Обнаружение именованных экспортов основано на общих синтаксических шаблонах, но не всегда точно обнаруживает именованные экспорты. В таких случаях использование формы импорта по умолчанию, описанной выше, может быть лучшим вариантом.
Обнаружение именованных экспортов охватывает многие распространённые шаблоны экспорта, шаблоны переэкспорта и результаты работы инструментов сборки и транспайлеров. См. cjs-module-lexer для точных реализованных семантик.
Различия между ES-модулями и CommonJS
Отсутствие require, exports, или module.exports
В большинстве случаев, ES-модуль import может использоваться для загрузки CommonJS-модулей.
Если это необходимо, функция require может быть создана внутри ES-модуля с использованием module.createRequire().
Отсутствие __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' }; copy Синтаксис assert { type: 'json' } обязателен; см. Утверждения импорта.
Импортированный JSON экспонирует только экспорт default. Поддержка именованных экспортов отсутствует. В кэше CommonJS создается запись, чтобы избежать дублирования. Тот же объект возвращается в CommonJS, если JSON-модуль уже был импортирован из того же пути.
Wasm-модули
Импорт модулей WebAssembly поддерживается с флагом --experimental-wasm-modules, позволяя импортировать любые файлы .wasm как обычные модули, также поддерживая их модульные импорты.
Эта интеграция соответствует Предложению по интеграции ES-модулей для WebAssembly.
Например, файл index.mjs содержащий:
import * as M from './module.wasm'; console.log(M); copy
выполняемый в:
node --experimental-wasm-modules index.mjs copy
предоставит интерфейс экспорта для инициализации module.wasm.
Ожидание в await уровне
Ключевое слово await может использоваться в теле ECMAScript-модуля на верхнем уровне.
Предположим ES-модуль с
export const five = await Promise.resolve(5); copy
И модуль с
import { five } from './a.mjs';
console.log(five); // Logs `5` copy node b.mjs # works copy
Если выражение ожидания на верхнем уровне 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`
}); copy HTTPS и HTTP импорты
Импортирование модулей, основанных на сети, с использованием https: и http: поддерживается под флагом --experimental-network-imports. Это позволяет веб-браузерным импортам работать в 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 }); copy // 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
} copy Загрузка по сети по умолчанию отключена
В настоящее время для включения загрузки ресурсов по http: или https: требуется флаг --experimental-network-imports. В будущем будет использован другой механизм для принудительного выполнения этого требования. Требуется явное включение для предотвращения нежелательного использования зависимостей для доступа к потенциально изменчивому состоянию, которое может повлиять на надёжность приложений Node.js.
Загрузчики
В настоящее время API перерабатывается и будет продолжать меняться.
Для настройки стандартного разрешения модулей можно по желанию предоставить хуки загрузчиков через аргумент --experimental-loader ./loader-name.mjs в Node.js.
При использовании хуков они применяются к точке входа и ко всем вызовам import. Они не применяются к вызовам require; для них по-прежнему действуют правила CommonJS.
Загрузчики следуют шаблону --require:
node \ --experimental-loader unpkg \ --experimental-loader http-to-https \ --experimental-loader cache-buster copy
Они вызываются в следующей последовательности: cache-buster вызывает http-to-https, которое вызывает unpkg.
Хук
Хук является частью цепочки, даже если эта цепочка состоит только из одного пользовательского (предоставленного пользователем) хука и стандартного хука, который всегда присутствует. Функции хука вложены: каждая должна всегда возвращать простой объект, и цепочка создаётся в результате вызова каждой функцией next<hookName>(), который является ссылкой на хук последующего загрузчика.
Хук, возвращающий значение, в котором отсутствует необходимая свойство, вызывает исключение. Хук, возвращающийся без вызова next<hookName>() и без возвращения shortCircuit: true, также вызывает исключение. Эти ошибки помогают предотвратить непреднамеренные разрывы в цепочке.
resolve(specifier, context, nextResolve)
API загрузчиков перерабатывается. Этот хук может исчезнуть или измениться его сигнатура. Не полагайтесь на API, описанный ниже.
-
specifier<строка> -
context<объект>-
conditions<массив строк> Условия экспорта соответствующегоpackage.json -
importAssertions<объект> -
parentURL<строка> | <неопределено> Импортирующий модуль или undefined, если это точка входа в Node.js
-
-
nextResolve<функция> Последующий хукresolveв цепочке или стандартный хук Node.jsresolveпосле последнего пользовательского хукаresolve - Возвращает: <объект>
-
format<строка> | <null> | <неопределено> Подсказка для хука загрузки (может быть проигнорирована)'builtin' | 'commonjs' | 'json' | 'module' | 'wasm' -
shortCircuit<неопределено> | <логическое значение> Сигнал о том, что этот хук намеревается прервать цепочку хуковresolve. По умолчанию:false -
url<строка> Абсолютный URL, к которому разрешается этот ввод
-
Цепочка хуков resolve отвечает за разрешение URL-адресов файлов для данного спецификатора модуля и родительского URL-адреса и, по желанию, его формата (например, 'module') в качестве подсказки для хука load. Если формат указан, то хук load в конечном итоге отвечает за предоставление конечного значения format (и он свободен игнорировать подсказку, предоставленную resolve); если resolve предоставляет format, требуется пользовательский хук load, даже если только для передачи значения в стандартный хук Node.js load.
Спецификатор модуля — это строка в операторе import или выражении import().
Родительский URL — это URL модуля, который импортировал этот модуль, или undefined, если это основная точка входа в приложение.
Свойство conditions в context — это массив условий условий экспорта пакетов, которые применяются к этому запросу разрешения. Они могут использоваться для поиска условных сопоставлений в других местах или для изменения списка при вызове стандартной логики разрешения.
Текущие условия экспорта пакетов всегда находятся в массиве context.conditions, переданном в хук. Чтобы гарантировать стандартное поведение разрешения спецификаторов модулей Node.js при вызове defaultResolve, массив 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);
} copy
load(url, context, nextLoad)
API загрузчиков перерабатывается. Этот хук может исчезнуть или измениться его сигнатура. Не полагайтесь на API, описанный ниже.
В предыдущей версии этого API это было разделено на 3 отдельных, теперь устаревших, хука (
getFormat,getSource, иtransformSource).
-
url<строка> URL, возвращенный цепочкойresolve -
context<объект>-
conditions<массив строк> Условия экспорта соответствующегоpackage.json -
format<строка> | <null> | <неопределено> Формат, необязательно предоставленный цепочкой хуковresolve -
importAssertions<объект>
-
-
nextLoad<функция> Последующий хукloadв цепочке или стандартный хук Node.jsloadпосле последнего пользовательского хукаload - Возвращает: <объект>
-
format<строка> -
shortCircuit<неопределено> | <логическое значение> Сигнал о том, что этот хук намеревается прервать цепочку хуковresolve. По умолчанию:false -
source<строка> | <ArrayBuffer> | <TypedArray> Источник для оценки Node.js
-
Крючок load предоставляет способ определения пользовательского метода интерпретации, получения и разбора URL. Он также отвечает за валидацию утверждения импорта.
Конечное значение format должно быть одним из следующих:
format |
Описание | Допустимые типы для source возвращаемые load
|
|---|---|---|
'builtin' |
Загрузка встроенного модуля Node.js | Не применимо |
'commonjs' |
Загрузка модуля Node.js CommonJS | Не применимо |
'json' |
Загрузка файла JSON | { string, ArrayBuffer, TypedArray } |
'module' |
Загрузка модуля ES | { string, ArrayBuffer, TypedArray } |
'wasm' |
Загрузка модуля WebAssembly | { ArrayBuffer, TypedArray } |
Значение source игнорируется для типа 'builtin', потому что в настоящее время невозможно заменить значение встроенного модуля Node.js (ядра). Значение source игнорируется для типа 'commonjs', потому что загрузчик модулей CommonJS не предоставляет механизма для загрузчика модулей ES для переопределения значения возврата модуля CommonJS CommonJS module return value. Это ограничение может быть преодолено в будущем.
Предупреждение: Крючок 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);
} copy В более сложных сценариях это также можно использовать для преобразования неподдерживаемого источника в поддерживаемый (см. Примеры ниже).
globalPreload()
API загрузчиков перерабатывается. Этот крючок может исчезнуть или его подпись может измениться. Не полагайтесь на описанный ниже API.
В предыдущей версии этого API этот крючок назывался
getGlobalPreloadCode.
-
context<Объект> Информация для помощи коду загрузки-
port<MessagePort>
-
- Возвращает: <строка> Код для выполнения перед запуском приложения
Иногда может потребоваться выполнить код в той же глобальной области, что и приложение. Этот крючок позволяет возвращать строку, которая выполняется как скрипт в режиме sloppy при запуске.
Аналогично тому, как работают обертки CommonJS, код выполняется в неявной функции scope. Единственный аргумент — функция типа require, которая может использоваться для загрузки встроенных модулей, таких как "fs": getBuiltin(request: string).
Если код нуждается в более продвинутых возможностях require, он должен создать свой собственный require с помощью module.createRequire().
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>');
// [...]
`;
} copy Для обеспечения связи между приложением и загрузчиком в код загрузки предоставляется ещё один аргумент: 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);
};
`;
} copy Примеры
Различные крючки загрузчика можно использовать вместе для достижения широкого круга кастомизаций поведения загрузки и оценки кода 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);
} copy // main.mjs
import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';
console.log(VERSION); copy При использовании вышеприведенного загрузчика, выполнение node --experimental-loader ./https-loader.mjs ./main.mjs выведет текущую версию CoffeeScript в соответствии с модулем по URL в main.mjs.
Загрузчик транспайлера
Источники в форматах, которые Node.js не понимает, можно преобразовать в JavaScript с помощью крючка load.
Это менее эффективно, чем транспилирование исходных файлов перед запуском 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, '..'));
} copy # 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}" copy # scream.coffee export scream = (str) -> str.toUpperCase() copy
При использовании вышеприведенного загрузчика, выполнение node --experimental-loader ./coffeescript-loader.mjs main.coffee приведет к тому, что main.coffee будет преобразован в JavaScript после загрузки исходного кода с диска, но перед выполнением его Node.js; и так далее для любых файлов .coffee, .litcoffee или .coffee.md, на которые ссылаются операторы import в любом загруженном файле.
Загрузчик "import map"
Два предыдущих загрузчика определили крючки load. Это пример загрузчика, который выполняет свою работу с помощью крючка resolve. Этот загрузчик считывает файл import-map.json указывающий, какие спецификаторы нужно переопределить на другой URL (это очень упрощенная реализация малой части спецификации "import maps").
// 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);
} copy Предположим, у нас есть эти файлы:
// main.js import 'a-module'; copy
// import-map.json
{
"imports": {
"a-module": "./some-module.js"
}
} copy // some-module.js
console.log('some module!'); copy Если вы выполните node --experimental-loader ./import-map-loader.js main.js результат будет some module!.
Алгоритм разрешения и загрузки
Возможности
Решатель по умолчанию обладает следующими свойствами:
- Разрешение на основе FileURL, как используется в модулях ES
- Разрешение относительных и абсолютных URL
- Отсутствие расширений по умолчанию
- Отсутствие основных папок
- Поиск разрешения пакета с голым спецификатором через node_modules
- Не происходит сбой при неизвестных расширениях или протоколах
- Может необязательно предоставлять подсказку о формате фазе загрузки
Загрузчик по умолчанию обладает следующими свойствами
- Поддержка загрузки встроенных модулей через
node:URL - Поддержка загрузки "встроенных" модулей через
data:URL - Поддержка загрузки модулей
file: - Возникает сбой при любом другом протоколе URL
- Возникает сбой при неизвестных расширениях для загрузки
file:(поддерживаются только.cjs,.jsи.mjs)
Алгоритм разрешения
Алгоритм загрузки спецификатора модуля ES задается с помощью метода ESM_RESOLVE ниже. Он возвращает разрешенный URL для спецификатора модуля относительно parentURL.
Алгоритм разрешения определяет полный разрешенный URL для загрузки модуля, а также его предлагаемый формат модуля. Алгоритм разрешения не определяет, может ли быть загружен протокол разрешенного URL, или разрешены ли расширения файлов, вместо этого эти проверки применяются Node.js на этапе загрузки (например, если его попросили загрузить URL, который имеет протокол, отличный от file:, data:, node:, или если --experimental-network-imports включен, https:).
Алгоритм также пытается определить формат файла на основе расширения (см. алгоритм ESM_FILE_FORMAT ниже). Если он не распознает расширение файла (например, если это не .mjs, .cjs или .json), то возвращается формат undefined, который вызовет ошибку на этапе загрузки.
Алгоритм определения формата модуля разрешенного URL предоставляется ESM_FILE_FORMAT, который возвращает уникальный формат модуля для любого файла. Формат "module" возвращается для модуля ECMAScript, а формат "commonjs" используется для обозначения загрузки через устаревший загрузчик CommonJS. Дополнительные форматы, такие как "addon", могут быть расширены в будущих обновлениях.
В следующих алгоритмах все ошибки подпрограмм распространяются как ошибки этих основных процедур, если не указано иное.
defaultConditions - это массив имен условной среды, ["node", "import"].
Решатель может вызывать следующие ошибки:
- Неверный спецификатор модуля: спецификатор модуля является неверным URL, именем пакета или спецификатором подпути пакета.
- Неверная конфигурация пакета: конфигурация package.json неверна или содержит неверную конфигурацию.
- Неверная цель пакета: экспорт или импорт пакета определяют целевой модуль для пакета, который является неверным типом или строковым целевым объектом.
- Путь пакета не экспортируется: экспорт пакета не определяет или не разрешает целевой подпуть в пакете для данного модуля.
- Импорт пакета не определен: импорт пакета не определяет спецификатор.
- Модуль не найден: запрашиваемый пакет или модуль не существует.
- Неподдерживаемый импорт каталога: разрешенный путь соответствует каталогу, который не является поддерживаемой целью для импорта модулей.
Спецификация алгоритма разрешения
ESM_RESOLVE(specifier, parentURL)
- Пусть resolved будет undefined.
- Если specifier является допустимым URL, то
- Установите resolved в результат анализа и повторной сериализации specifier как URL.
- В противном случае, если specifier начинается с "/", "./" или "../", то
- Установите resolved в результат разрешения URL specifier относительно parentURL.
- В противном случае, если specifier начинается с "#", то
- Установите resolved в результат PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, defaultConditions).
- В противном случае,
- Примечание: specifier теперь является голым спецификатором.
- Установите resolved в результат PACKAGE_RESOLVE(specifier, parentURL).
- Пусть format будет undefined.
- Если resolved является URL "file:", то
- Если resolved содержит какие-либо кодировки процентов "/" или "\" ("%2F" и "%5C" соответственно), то
- Вызовите ошибку Неверный спецификатор модуля.
- Если файл по resolved является каталогом, то
- Вызовите ошибку Неподдерживаемый импорт каталога.
- Если файл по resolved не существует, то
- Вызовите ошибку Модуль не найден.
- Установите resolved в реальный путь resolved, сохраняя те же компоненты строки запроса и фрагмента URL.
- Установите format в результат ESM_FILE_FORMAT(resolved).
- В противном случае,
- Установите format в формат модуля типа контента, связанного с URL resolved.
- Верните format и resolved на этап загрузки
PACKAGE_RESOLVE(packageSpecifier, parentURL)
- Пусть packageName будет undefined.
- Если packageSpecifier является пустой строкой, то
- Вызовите ошибку Неверный спецификатор модуля.
- Если packageSpecifier является именем встроенного модуля Node.js, то
- Верните строку "node:", объединенную с packageSpecifier.
- Если packageSpecifier не начинается с "@", то
- Установите packageName в подстроку packageSpecifier до первого разделителя "/" или конца строки.
- В противном случае,
- Если packageSpecifier не содержит разделителя "/", то
- Вызовите ошибку Неверный спецификатор модуля.
- Установите packageName в подстроку packageSpecifier до второго разделителя "/" или конца строки.
- Если packageName начинается с "." или содержит "\" или "%", то
- Вызовите ошибку Неверный спецификатор модуля.
- Пусть packageSubpath будет ".", объединенной с подстрокой packageSpecifier с позиции длины packageName.
- Если packageSubpath заканчивается на "/", то
- Вызовите ошибку Неверный спецификатор модуля.
- Пусть selfUrl будет результатом PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL).
- Если selfUrl не undefined, верните selfUrl.
- Пока parentURL не является корнем файловой системы,
- Пусть packageURL будет результатом разрешения URL "node_modules/", объединенной с packageSpecifier, относительно parentURL.
- Установите parentURL в родительский URL папки parentURL.
- Если папка по packageURL не существует, то
- Продолжите следующую итерацию цикла.
- Пусть pjson будет результатом READ_PACKAGE_JSON(packageURL).
- Если pjson не null и pjson.exports не null или undefined, то
- Верните результат PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
- В противном случае, если packageSubpath равен ".", то
- Если pjson.main является строкой, то
- Верните результат разрешения URL main в packageURL.
- В противном случае,
- Верните результат разрешения URL packageSubpath в packageURL.
- Вызовите ошибку Модуль не найден.
PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL)
- Пусть packageURL будет результатом LOOKUP_PACKAGE_SCOPE(parentURL).
- Если packageURL равно null, то
- Верните undefined.
- Пусть pjson будет результатом READ_PACKAGE_JSON(packageURL).
- Если pjson равно null или если pjson.exports равно null или undefined, то
- Верните undefined.
- Если pjson.name равно packageName, то
- Верните результат PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
- В противном случае, верните undefined.
PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)
- Если exports является объектом, имеющим как ключ, начинающийся с ".", так и ключ, не начинающийся с ".", вызовите ошибку Неверная конфигурация пакета.
- Если subpath равен ".", то
- Пусть mainExport будет undefined.
- Если exports является строкой или массивом, или объектом, не содержащим ключей, начинающихся с ".", то
- Установите mainExport в exports.
- В противном случае, если exports является объектом, содержащим свойство ".", то
- Установите mainExport в exports["."].
- Если mainExport не undefined, то
- Пусть resolved будет результатом PACKAGE_TARGET_RESOLVE( packageURL, mainExport, null, false, conditions).
- Если resolved не null или undefined, верните resolved.
- В противном случае, если exports является объектом и все ключи exports начинаются с ".", то
- Пусть matchKey будет строкой "./", объединенной с subpath.
- Пусть resolved будет результатом PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions).
- Если resolved не null или undefined, верните resolved.
- Вызовите ошибку Путь пакета не экспортируется.
РАЗРЕШЕНИЕ_ИМПОРТА_ПАКЕТА(спецификатор, родительскийURL, условия)
- Утверждение: спецификатор начинается с "#".
- Если спецификатор точно равен "#" или начинается с "#/", тогда
- Выбросить ошибку Неверный Спецификатор Модуля.
- Пусть url_пакета будет результатом ПОИСК_ОБЛАСТИ_ПАКЕТА(родительскийURL).
- Если url_пакета не null, тогда
- Пусть pjson будет результатом ЧТЕНИЕ_JSON_ПАКЕТА(url_пакета).
- Если pjson.imports является не-null объектом, тогда
- Пусть разрешенный будет результатом РАЗРЕШЕНИЕ_ИМПОРТ_ЭКСПОРТ_ПАКЕТА( спецификатор, pjson.imports, url_пакета, true, условия).
- Если разрешенный не null или undefined, вернуть разрешенный.
- Выбросить ошибку Импорт пакета не определен.
РАЗРЕШЕНИЕ_ИМПОРТ_ЭКСПОРТ_ПАКЕТА(ключ_сопоставления, объект_сопоставления, url_пакета, являетсяИмпортом, условия)
- Если ключ_сопоставления является ключом объект_сопоставления и не содержит "*", тогда
- Пусть цель будет значением объект_сопоставления[ключ_сопоставления].
- Вернуть результат РАЗРЕШЕНИЕ_ЦЕНЫ_ПАКЕТА(url_пакета, цель, null, являетсяИмпортом, условия).
- Пусть ключи_расширения будет списком ключей объект_сопоставления, содержащих только один "*", отсортированных по функции сортировки СРАВНЕНИЕ_КЛЮЧЕЙ_ШАБЛОНА, которая упорядочивает в порядке убывания специфичности.
- Для каждого ключа ключ_расширения в ключи_расширения, выполнить
- Пусть основание_шаблона будет подстрокой ключ_расширения до, но не включая первый символ "*".
- Если ключ_сопоставления начинается с, но не равен основание_шаблона, тогда
- Пусть приставка_шаблона будет подстрокой ключ_расширения с индекса после первого символа "*".
- Если приставка_шаблона имеет нулевую длину, или если ключ_сопоставления заканчивается приставка_шаблона и длина ключ_сопоставления больше или равна длине ключ_расширения, тогда
- Пусть цель будет значением объект_сопоставления[ключ_расширения].
- Пусть совпадение_шаблона будет подстрокой ключ_сопоставления, начиная с индекса длины основание_шаблона до длины ключ_сопоставления минус длина приставка_шаблона.
- Вернуть результат РАЗРЕШЕНИЕ_ЦЕНЫ_ПАКЕТА(url_пакета, цель, совпадение_шаблона, являетсяИмпортом, условия).
- Вернуть null.
СРАВНЕНИЕ_КЛЮЧЕЙ_ШАБЛОНА(ключA, ключB)
- Утверждение: ключA заканчивается на "/" или содержит только один "*".
- Утверждение: ключB заканчивается на "/" или содержит только один "*".
- Пусть длина_основанияA будет индексом "*" в ключA плюс один, если ключA содержит "*", или длина ключA иначе.
- Пусть длина_основанияB будет индексом "*" в ключB плюс один, если ключB содержит "*", или длина ключB иначе.
- Если длина_основанияA больше длина_основанияB, вернуть -1.
- Если длина_основанияB больше длина_основанияA, вернуть 1.
- Если ключA не содержит "*", вернуть 1.
- Если ключB не содержит "*", вернуть -1.
- Если длина ключA больше длины ключB, вернуть -1.
- Если длина ключB больше длины ключA, вернуть 1.
- Вернуть 0.
РАЗРЕШЕНИЕ_ЦЕНЫ_ПАКЕТА(url_пакета, цель, совпадение_шаблона, являетсяИмпортом, условия)
- Если цель является Строкой, тогда
- Если цель не начинается с "./", тогда
- Если являетсяИмпортом ложно, или если цель начинается с "../" или "/", или если цель является корректным URL, тогда
- Выбросить ошибку Неверная Цель Пакеты.
- Если совпадение_шаблона является Строкой, тогда
- Вернуть результат РАЗРЕШЕНИЕ_ПАКЕТА(цель с каждым экземпляром "*" замененным на совпадение_шаблона, url_пакета + "/").
- Вернуть РАЗРЕШЕНИЕ_ПАКЕТА(цель, url_пакета + "/").
- Если цель разбитая по "/" или "\" содержит какие-либо "", ".", "..", или "node_modules" сегменты после первого сегмента ".", регистронезависимо и включая проценты кодированные варианты, выбросить ошибку Неверная Цель Пакеты.
- Пусть разрешеннаяЦель будет разрешением URL конкатенации url_пакета и цели.
- Утверждение: разрешеннаяЦель содержится в url_пакета.
- Если совпадение_шаблона является null, тогда
- Вернуть разрешеннаяЦель.
- Если совпадение_шаблона, разбитый на "/" или "\", содержит какие-либо "", ".", ".." или "node_modules" сегменты, регистронезависимо и включая проценты кодированные варианты, выбросить ошибку Неверный Спецификатор Модуля.
- Вернуть разрешение URL разрешеннаяЦель с каждым экземпляром "*" замененным на совпадение_шаблона.
- В противном случае, если цель является не-null Объектом, тогда
- Если экспорт содержит какие-либо ключи свойств индекса, как определено в ECMA-262 6.1.7 Индекс массива, выбросить ошибку Неверная Конфигурация Пакеты.
- Для каждой свойства p в цели, в порядке вставки объектов,
- Если p равно "default" или условия содержит запись для p, тогда
- Пусть значениеЦели будет значением свойства p в цели.
- Пусть разрешенное будет результатом РАЗРЕШЕНИЕ_ЦЕНЫ_ПАКЕТА( url_пакета, значениеЦели, совпадение_шаблона, являетсяИмпортом, условия).
- Если разрешенное равно undefined, продолжить цикл.
- Вернуть разрешенное.
- Вернуть undefined.
- В противном случае, если цель является Массивом, тогда
- Если _длина_цели равна нулю, вернуть null.
- Для каждого элемента значениеЦели в цели, выполнить
- Пусть разрешенное будет результатом РАЗРЕШЕНИЕ_ЦЕНЫ_ПАКЕТА( url_пакета, значениеЦели, совпадение_шаблона, являетсяИмпортом, условия), продолжая цикл при любой ошибке Неверная Цель Пакеты.
- Если разрешенное равно undefined, продолжить цикл.
- Вернуть разрешенное.
- Вернуть или выбросить последнюю запасную развязывание null возврата или ошибки.
- В противном случае, если цель равна null, вернуть null.
- В противном случае выбросить ошибку Неверная Цель Пакеты.
ФОРМАТ_ФАЙЛА_ESM(url)
- Утверждение: url соответствует существующему файлу.
- Если url заканчивается на ".mjs", тогда
- Вернуть "модуль".
- Если url заканчивается на ".cjs", тогда
- Вернуть "commonjs".
- Если url заканчивается на ".json", тогда
- Вернуть "json".
- Пусть url_пакета будет результатом ПОИСК_ОБЛАСТИ_ПАКЕТА(url).
- Пусть pjson будет результатом ЧТЕНИЕ_JSON_ПАКЕТА(url_пакета).
- Если pjson?.type существует и равно "модуль", тогда
- Если url заканчивается на ".js", тогда
- Вернуть "модуль".
- Вернуть undefined.
- В противном случае,
- Вернуть undefined.
ПОИСК_ОБЛАСТИ_ПАКЕТА(url)
- Пусть url_области будет url.
- Пока url_области не является корнем файловой системы,
- Установить url_области на родительский URL url_области.
- Если url_области заканчивается на сегменте пути "node_modules", вернуть null.
- Пусть url_pjson будет разрешением "package.json" внутри url_области.
- Если файл в url_pjson существует, тогда
- Вернуть url_области.
- Вернуть null.
ЧТЕНИЕ_JSON_ПАКЕТА(url_пакета)
- Пусть url_pjson будет разрешением "package.json" внутри url_пакета.
- Если файл в url_pjson не существует, тогда
- Вернуть null.
- Если файл в url_пакета не парсится как корректный JSON, тогда
- Выбросить ошибку Неверная Конфигурация Пакеты.
- Вернуть парсированный JSON исходника файла в url_pjson.
Настройка алгоритма разрешения спецификаторов ESM
Не полагайтесь на этот флаг. Мы планируем удалить его, как только 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! copy
© Joyent, Inc. and other Node contributors
Licensed under the MIT License.
Node.js is a trademark of Joyent, Inc. and is used with its permission.
We are not endorsed by or affiliated with Joyent.
https://nodejs.org/dist/latest-v18.x/docs/api/esm.html