Node.js 13.2.0 идет с поддержкой ECMAScript модулей, известных по своему синтаксису import и export. Ранее эта функциональность была за флагом --experimental-modules
, который больше не требуется. Однако, реализация все еще экспериментальная и может меняться.
От переводчика: это долгожданная фича наконец-то позволит нам использовать стандартный модульный синтаксис, уже доступный в современных браузерах, а теперь еще и в Node.js без флагов и транспайлеров
Активация
Node.js будет обрабатывать код как ES-модули в следующих случаях:
- Файлы с расширением
.mjs
- Файлы с расширением
.js
или без расширения вообще, при условии что ближайший к ним родительскийpackage.json
содержит значение"type": "module"
- Код, переданный через аргумент
—-eval
или STDIN, вместе с флагом—-input-type=module
Во всех остальных случаях код будет считаться CommonJS. Это относится к .js
файлам без "type": "module"
в ближайшем package.json
и коду, переданному через командную строку без указания --input-type
. Это сделано в целях сохранения обратной совместимости. Однако, поскольку у нас теперь есть два вида модулей, CommonJS и ES, будет лучше указывать тип модулей явным образом.
Вы можете явно отметить свой код как CommonJS следующими признаками:
- Файлы с расширением
.cjs
- Файлы с расширением
.js
или без расширения вообще, при условии что ближайший родительскийpackage.json
содержит значение"type": "“commonjs”"
- Код, переданный через аргумент
--eval
или STDIN c явным флагом--input-type=commonjs
Чтобы узнать больше об этих фичах, смотрите разделы документации "Package Scope and File Extensions" и про флаг --input-type
Синтаксис import и export
В контексте ES модуля, можно использовать import
, указывающий на другие Javascript файлы. Они могут быть указаны одним из следующих форматов:
- Относительный URL:
"./file.mjs"
- Абсолютный URL c
file://
, например"file:///opt/app/file.mjs"
- Имя пакета:
"es-module-package"
- Путь до файла внутри пакета:
"es-module-package/lib/file.mjs"
В импортах можно использовать дефолтные (import _ from "es-module-package"
) и именованные значения (import { shuffle } from "es-module-package"
), а также импортировать всё как один неймспейс (import * as fs from "fs"
). Все встроенные Node.js пакеты, вроде fs
или path
, поддерживают все три вида импортов.
Импорты, которые указывают на CommonJS код (то есть весь текущий JavaScript, написанный для Node.js с использованием require
и module.exports
), могут использовать только дефолтный вариант (import _ from "commonjs-package"
).
Импорт других форматов файлов, таких как JSON и WASM остается экспериментальным, и требует флагов --experimental-json-modules
и --experimental-wasm-modules
соответственно. Тем не менее, вы можете загружать эти файлы используя module.createRequire
API, который доступен без дополнительных флагов.
В своих ES-модулях вы можете использовать ключевое слово export для экспортирования дефолтных и именованных значений.
Динамический выражения с import()
могут использоваться для загрузки ES-модулей либо из CommonJS, либо ES кода. Заметьте, что import()
возвращает не модуль а его обещание (Promise).
Также в модулях доступен import.meta.url
, который содержит абсолютный URL текущего ES-модуля.
Файлы и новое поле "type" в package.json
Добавьте "type": "module"
в package.json своего проекта, и Node.js начнет воспринимать все .js
-файлы вашего проекта как ES модули.
Если какие-то файлы вашего проекта все еще используют CommonJS и у вас не получается мигрировать весь проект разом, вы можете либо использовать расширение .cjs
для этого кода, либо сложить его в отдельную директорию, и добавить туда package.json
, содержащий { "type": "commonjs" }
, который укажет Node.js, что она должна обрабатываться как CommonJS.
Для каждого загружаемого файла Node.js посмотрит на package.json
в содержащей его директории, затем на уровень выше, и так до тех пор пока не достигнет корневой директории. Этот механизм похож на то, как работает Babel с .babelrc
файлами. Такой подход позволяет Node.js использовать package.json
как источник различных метаданных о пакете и конфигурации, наподобие того, как это уже работает в Babel и других инструментах.
Мы рекомендуем всем разработчикам пакетов указывать поле type
, даже если там будет написано commonjs
.
Входные точки пакета и поле "exports" в package.json
Теперь у нас есть два поля для указания входной точки в пакет: main
и exports
. Поле main
поддерживается всеми версиями Node.js, но его возможности ограничены: с ним можно определить только одну главную входную точку в пакет. Новое поле exports
также позволяет определить главную входную точку, а также дополнительные пути. Это дает дополнительную инкапсуляцию для пакетов, где только явно указанные в exports
пути доступны для импорта снаружи пакета. exports
применяется к обоим типам модулей, CommonJS и ES, неважно, используются они через require
или import
.
Эта функциональность позволит импортам вида pkg/feature
указывать на реальный путь вроде ./node_modules/pkg/esm/feature.js
. Также, Node.js выбросит ошибку, если импорт ссылается на pkg/esm/feature.js
который не указан в exports
.
Дополнительная, все еще экспериментальная, возможность, условные экспорты предоставляет возможность экспортировать разные файлы для различных окружений. Это позволит пакету отдавать CommonJS код для вызова require("pkg")
и ES модульный код для импорта через import "pkg"
, хотя написание такого пакета не лишено других проблем. Вы можете включить условные экспорты флагом —-experimental-conditional-exports
.
Основные грабли новых модулей
Обязательное указание расширений файлов
При использовании импортов, нужно обязательно указывать расширение файла. При импорте индексного файла из директории также нужно полностью указывать путь до файла, то есть "./startup/index.js".
Такое поведение совпадает с тем как работают импорты в браузерах, при обращении к обычному серверу без дополнительной конфигурации.
Больше недоступны require
, exports
, module.exports
, __filename
, __dirname
Эти значения из CommonJS недоступны в контексте ES модулей. Однако, require
можно импортировать в ES модуль через module.createRequire()
. Эквиваленты __filename
и __dirname
можно достать из import.meta.url
.
Создание пакетов
На данный момент мы рекомендуем авторам пакетов использовать либо полностью CommonJS, либо полностью ES модули для своих Node.js проектов. Рабочая группа по модулям для Node.js все продолжает искать способы улучшить поддержку двойных пакетов, с CommonJS для легаси пользователей и ES модулями для новых. Условные экспорты, сейчас являются экспериментальными и мы надеемся выкатить эту функциональность или ее альтернативу к концу января 2020 года, или даже раньше.
Чтобы узнать об этом больше, смотрите наши примеры и рекомендации по созданию двойных CommonJS/ES Module пакетов
Что будет дальше
Загрузчики. Продолжается работа над API для написания кастомных загрузчиков, для реализации транспиляции модулей в рантайме, переопределения путей импортов (пакетов или отдельных файлов), а также инструментирование кода. Экспериментальное API, доступное под флагом —-experimental-loader
, будет подвержено значительным переделкам до того, как мы выведем его из-под флага.
Двойные CommonJS/ES module пакеты. Мы хотим предоставить стандартный способ опубликовать пакет, который может использоваться и через require
в CommonJS и через import
в ES модулях. Больше информации об этом у нас в документации. Мы планируем завершить работу и вывести из-под флага к концу января 2020 года, если не раньше.
Вот и всё! Мы надеемся, поддержка ECMAScript модулей приблизит Node.js к стандартам JavaScript и принесет новые возможности для совместимости во всей экосистеме JavaScript. Рабочий процесс над улучшением поддержки модулей ведется публично здесь: https://github.com/nodejs/modules.
Автор: Борис Сердюк