В JavaScript много забавного. У одного из самых популярных в мире языков программирования до сих пор нет стабильного синтаксиса разбиения кода на части. То есть в стандарте синтаксис ESM с «import» наперевес уже есть, но в браузерах и ноде он спрятан за флагами, а в вебпаке его поддержка появилась совсем недавно во 2-й версии. Добавим к этому миграцию ноды и вебпака с CommonJS «require» на ESM «import» и полмиллиона пакетов NPM, подавляющая часть которых использует CommonJS. Немного разобраться с зоопарком поможет вышедшая на прошлой неделе статья от автора Webpack, адаптированнй перевод которой ждет вас под катом.

Согласуем терминологию


Основной файл Так мы будем называть файл, в котором используется выражение import()
Включаемый файл Так мы будем называть файл, имя которого указано в выражении import()
не-ESM Так мы будем называть CommonJS или AMD модуль, который не экспортирует флаг __esModule
транс-ESM Так мы будем называть CommonJS модуль, который экспортирует флаг __esModule в значении true. Делает он это, потому что был скомпилирован (транспилирован) из ESM модуля
ESM Так мы будем называть EcmaScript (ES6) модуль как его определяет стандарт языка JavaScript
strict-ESM Так мы будем называть EcmaScript модуль как его хочет видеть нода, с расширением файла .mjs
JSON Так мы будем называть JSON файлы. Вебпак и нода умеют и любят включать JSON, превращая его содержимое в JavaScript объекты

Со всеми этими вариантами у нас возможен следующий зоопарк кто-кого-включает:

  • (А) Основной файл: не-ESM, транспилированный-ESM или обычный ESM
  • (Б) Основной файл: strict-ESM (.mjs)
  • (1) Включаемый файл: не-ESM
  • (2) Включаемый файл: транс-ESM (__esModule)
  • (3) Включаемый файл: ESM или strict-ESM (.mjs)
  • (4) Включаемый файл: JSON

Как это выглядит в коде?


Таблички с вариантами — это не совсем то, к чему привыкли разработчики. Давайте посмотрим на код:

// (A) source.js
import("./target").then(result => console.log(result));

// (B) source.mjs
import("./target").then(result => console.log(result));

// (1) target.js
exports.name = "name";
exports.default = "default";

// (2) target.js
exports.__esModule = true;
exports.name = "name";
exports.default = "default";

// (3) target.js or target.mjs
export const name = "name";
export default "default";

// (4) target.json
{ name: "name", default: "default" }

A3 и Б3: import(ESM)


Эти два случая — единственные, описанные в спеке.

Результатом вызова import() будет так называемый «namespace object», соответствующий загружаемому модулю. Для совместимости вебпак автоматически добавит в этот объект флаг __esModule:

{ __esModule: true, name: "name", default: "default" }

Примечание переводчика: Мы так и не смогли понять, что является условием добавления __esModule. И «import… from ...», и «import(...)», и для браузера, и для ноды — namespace object у нас всегда получался без этого флага. Если кто поделится в комментах что здесь имел в виду автор — буду признателен!

A1: import(CJS)


Ситуация, когда мы используем import для загрузки CommonJS модуля. Например, если установили этот модуль с помощью npm. Webpack версии 3 в данном случае возвращал значение, которое загружаемый модуль устанавливал для module.exports. Webpack версии 4 будет всегда создавать namespace object и позволяет обращаться к экспортированным идентификаторм как с помощью синтаксиса «import { property } from ...», так и «import(...)».

Обратите внимание, что Webpack 4 заменит экспортированное свойство «default» на собственное свойство «default» для «всего экспортированного». Соответствующий фрагмент кода из примера выше:

// webpack 3
{ name: "name", default: "default" }

// webpack 4
{ name: "name", default: { name: "name", default: "default" } }

Б1: import(CJS)


Если strict-ESM файл загружает старый CommonJS модуль, то Webpack не позволит обращаться к экспортированным идентификаторам как к полям namespace object. У объекта будет только одно поле «default» со «всем экспортированным»:

{ default: { name: "name", default: "default" } }

А2: import(транс-JS)


Если у загружаемого модуля обнаруживется флаг __esModule, то Webpack сразу прекращает игры с «default» и помещает все экспортированное в namespace object:

{ __esModule: true, name: "name", default: "default" }

Б2: import(транс-ESM)


Сюрприз: если у загружаемого модуля обнаруживается флаг __esModule, но загружающий модель является strict-ESM, то флаг будет проигнорирован! Для консистентности с Node.js:

{ default: { __esModule: true, name: "name", default: "default" } }

А4 и Б4: import(JSON)


«Property picking» сработает в любом случае, даже при загрузке из strict-ESM, и мы получим все содержимое JSON в namespace object. И то же самое еще раз в поле «default»:

{ name: "name", default: { name: "name", default: "default" } }

Итого


По факту между Webpack 3 и Webpack 4 поменялся только один сценарий. Пример:

module.exports = 42;

Для корректной работы нужно использовать поле «default»:

// webpack 3
42

// webpack 4
{ default: 42 }

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


  1. Dreyk
    20.02.2018 14:28

    namespace object у нас всегда получался без этого флага

    а у меня всегда получается с этим флагом. Webpack 3, пишу export default class… и import Name from '....'