В 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 }
Dreyk
а у меня всегда получается с этим флагом. Webpack 3, пишу export default class… и import Name from '....'