ES-модули в JavaScript: import и export

Содержание

О проверке кода. Примеры не запускались — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».

Как только проект перерастает один файл, нужен способ разбить код на части. ES-модули — стандартный механизм JavaScript из ECMAScript 6, работающий и в браузере, и в Node. Разберём import/export без путаницы.

Именованный экспорт

Файл отдаёт наружу несколько значений по именам:

// math.js
export const PI = 3.14159;
export function square(x) { return x * x; }
export function cube(x) { return x ** 3; }
// main.js — импорт по именам в фигурных скобках
import { square, PI } from './math.js';

square(5);   // 25
PI;          // 3.14159

Именованный экспорт отдаёт несколько значений, каждое под своим именем. При импорте перечисляют нужные в фигурных скобках — с теми же именами (можно взять не все). Это основной способ, когда файл предоставляет набор утилит.

Переименование при импорте — через as:

import { square as sq } from './math.js';   // локально называем sq

Дефолтный экспорт

Файл отдаёт одно главное значение:

// User.js
export default class User {
  constructor(name) { this.name = name; }
}
// main.js — без скобок, имя любое
import User from './User.js';           // можно назвать как угодно
import Whatever from './User.js';       // тоже сработает

Дефолтный экспорт — одно главное значение файла, и при импорте ему дают любое имя без фигурных скобок. Используют, когда файл представляет одну сущность: класс, компонент, главную функцию. В файле может быть только один default.

Именованный против дефолтного экспорта

Именованный export

Несколько значений

  • export const x, export function
  • Импорт в скобках: import { x }
  • Имена фиксированы (или as)
  • Для наборов утилит

export default

Одно главное значение

  • export default class
  • Импорт без скобок: import X
  • Имя произвольное
  • Один на файл, для одной сущности

Их можно сочетать в одном файле: export default для главного плюс именованные для дополнительного.

import против require

Это частый источник путаницы: два синтаксиса для одного дела.

// CommonJS — старый формат Node.js
const math = require('./math');
module.exports = { square };

// ES-модули — стандарт языка
import { square } from './math.js';
export { square };

import/export — стандарт ES-модулей, работает в браузере и современном Node. require/module.exports — CommonJS, исторический формат Node. Ключевые отличия:

  • import статический — анализируется до запуска, что позволяет сборщикам удалять неиспользуемый код (tree-shaking);
  • require динамический — выполняется в момент вызова, можно вызвать по условию;
  • у ESM импорты поднимаются и разбираются раньше остального кода.

Новый код пишут на ES-модулях. CommonJS остаётся в старых Node-проектах — подробное сравнение отдельно.

Как включить в Node.js и браузере

// package.json — весь проект как ES-модули
{ "type": "module" }
<!-- браузер — атрибут type="module" -->
<script type="module" src="main.js"></script>

По умолчанию Node трактует .js как CommonJS. Чтобы заработали import/export, добавляют "type": "module" в package.json или дают файлам расширение .mjs. В браузере обычный <script> не понимает import — нужен type="module" (он же включает отложенную загрузку и строгий режим). Забыть это — самая частая ошибка с ошибкой Cannot use import statement outside a module.

Динамический импорт

Обычный import статичен — он в начале файла и грузится всегда. Иногда модуль нужен по условию:

button.addEventListener('click', async () => {
  // тяжёлый модуль грузится, только когда реально нужен
  const { generateReport } = await import('./report.js');
  generateReport();
});

import() — функция, загружающая модуль во время выполнения, возвращает промис. В отличие от статического импорта, её вызывают по условию или по требованию: подгрузить редко используемый модуль только при действии пользователя. Это основа «ленивой загрузки» (code splitting) — страница грузит меньше кода сразу, остальное подтягивает по мере надобности.

Типичные ошибки

  • забыть расширение — в браузере и Node ESM путь пишут полностью: './math.js', не './math';
  • забыть "type": "module" — Node ругается на import;
  • перепутать скобки — дефолтный импорт без { }, именованный с { };
  • смешать форматыrequire и import в одном файле обычно не уживаются.

Смежные темы: ESM против CommonJS — подробное сравнение; ECMAScript 6 — откуда модули; промисы — динамический import. Полный список — в уроках JavaScript.

Частые вопросы

Что такое ES-модули?
Стандартный способ разбить JavaScript-код на файлы: одни файлы экспортируют функции и значения через export, другие подключают их через import. Это встроенный механизм языка, работающий и в браузере, и в Node.js.
Чем именованный экспорт отличается от дефолтного?
Именованный экспортирует несколько значений по именам, и импортируют их в фигурных скобках с теми же именами. Дефолтный экспортирует одно главное значение файла, и при импорте ему можно дать любое имя без скобок.
В чём разница между import и require?
import — синтаксис ES-модулей, стандарт языка, работает в браузере и современном Node.js. require — способ CommonJS, исторический формат Node.js. import статический и анализируется до запуска, require динамический и выполняется в момент вызова.
Как включить ES-модули в Node.js?
Добавить “type”: “module” в package.json или использовать расширение .mjs для файлов. Без этого Node трактует файлы как CommonJS и import вызовет ошибку. В браузере нужен атрибут type=module у тега script.
Что такое динамический импорт?
Функция import(), которая загружает модуль во время выполнения и возвращает промис. В отличие от обычного import, её можно вызвать по условию или по требованию — например, подгрузить тяжёлый модуль только когда он реально понадобился.