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.

Комментарии (5)


  1. ReklatsMasters
    23.11.2019 21:04

    Пока в 2022 не закончится поддержка 12 версии, cjs модули будут жить, как минимум из-за энтерпрайза и обратной совместимости.


    1. justboris Автор
      23.11.2019 23:42
      +2

      Да, так и есть. Одно радует, лёд тронулся и процесс миграции начался.


  1. Cobalt
    24.11.2019 09:33

    Давно ожидаемая фича. С 2015 года запускаю все свои NodeJS проекты с Babel через require('@babel/register'); И во всем остальном коде использую import вместо require кроме моделей и миграций Sequelize и конфигов Webpack. На 8й Node еще приходилось включать флаг для поддержки async/await. Радует что с каждым новым проектом все меньше и меньше костылей приходится добавлять в код


  1. murzilka
    24.11.2019 10:45
    +1

    А чем новые модули лучше старых? Я пока вижу только один аргумент: унификация способа подключения модулей в ноде и на фронте. Может быть есть что-то ещё?


    1. Myateznik
      24.11.2019 11:44

      Это даже не унификация, а именно стандартизация. ES Modules это часть спецификации ECMAScript т.е. новые модули это нативная реализация модулей, работающая в идеале одинаково во всех реализация движков и производных ECMAScript.


      Ещё одно из отличий ES Modules от Common JS это то, что ES Modules по своей природе асинхронны, а Common JS синхронны.