1. Зачем нужны ES Modules и чем они отличаются от CommonJS
В мире фронтенда ES Modules уже давно стали стандартом. Они позволяют явно объявлять, что именно модуль "выставляет наружу" (export), и что он хочет "забрать" из других модулей (import). Это делает код более прозрачным и предсказуемым. В Node.js долгое время использовалась система CommonJS (require/module.exports), но теперь поддержка ESM стала полноценной.
Кратко:
- CommonJS — старая школа Node.js, синхронные модули, require и module.exports.
- ES Modules (ESM) — современный стандарт, асинхронная загрузка (в браузере), import и export.
Зачем переходить на ESM?
- Совместимость с современным фронтендом.
- Явные зависимости и экспорт.
- Новые возможности JavaScript (top-level await, динамические импорты и др.).
- Более строгий и читаемый синтаксис.
2. Синтаксис export: как делиться своими функциями и переменными
В ESM всё начинается с того, что модуль экспортирует то, чем хочет поделиться с другими. Есть два основных способа:
Именованный экспорт (named export)
Можно экспортировать сколько угодно сущностей по имени:
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export class Circle {
constructor(radius) {
this.radius = radius;
}
area() {
return PI * this.radius * this.radius;
}
}
Можно экспортировать уже объявленные ранее переменные или функции:
const E = 2.718;
function multiply(a, b) {
return a * b;
}
export { E, multiply };
Экспорт по умолчанию (default export)
Модуль может экспортировать что-то "главное". Такой экспорт может быть только один на модуль:
// greeting.js
export default function greet(name) {
return `Hello, ${name}!`;
}
Можно экспортировать по умолчанию любой объект, функцию, класс или даже число:
export default 42;
3. Синтаксис import: как забирать чужое
Импорт именованных экспортов
import { PI, add } from './math.js';
console.log(PI); // 3.14159
console.log(add(2, 3)); // 5
Можно импортировать и переименовывать:
import { PI as piValue } from './math.js';
console.log(piValue);
Импорт "по умолчанию"
import greet from './greeting.js';
console.log(greet('Alice')); // Hello, Alice!
Импортировать всё в объект
import * as math from './math.js';
console.log(math.PI); // 3.14159
console.log(math.add(2, 2)); // 4
Комбинированный импорт
import greet, { PI, add } from './mathAndGreet.js';
4. Как Node.js понимает, что мы хотим использовать ESM
В Node.js по умолчанию всё ещё действует CommonJS. Чтобы использовать ESM, нужно явно указать это в проекте.
Способ 1: type: "module" в package.json
Самый современный и рекомендуемый способ — добавить в package.json:
{
"type": "module"
}
Теперь все файлы с расширением .js в вашем проекте будут рассматриваться как ES-модули.
Важно!
- После этого все импорты должны быть через import/export.
- require и module.exports больше не работают (если только не используете .cjs расширение).
- Пути к модулям должны быть с расширением: import { foo } from './foo.js' (да, даже если файл называется foo.js — без расширения работать не будет).
Способ 2: расширения .mjs и .cjs
- Файлы с расширением .mjs всегда считаются ES-модулями, даже если нет type: "module".
- Файлы с расширением .cjs — всегда CommonJS, даже если в проекте стоит type: "module".
Это позволяет смешивать стили, если вдруг очень хочется (или если есть старые зависимости).
5. Примеры: разбиваем приложение на ESM-модули
Давайте продолжим развивать наше мини-приложение, добавив модуль с математикой и модуль для приветствий.
math.js
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
greeting.js
// greeting.js
export default function greet(name) {
return `Привет, ${name}!`;
}
main.js
// main.js
import { PI, add } from './math.js';
import greet from './greeting.js';
console.log(greet('Мир')); // Привет, Мир!
console.log(`PI = ${PI}`);
console.log(`2 + 3 = ${add(2, 3)}`);
package.json
{
"name": "my-esm-app",
"version": "1.0.0",
"type": "module"
}
Теперь можно запускать:
node main.js
6. Полезные нюансы
Пути с расширениями
Node.js требует, чтобы в ESM-импортах указывалось расширение файла:
import { foo } from './foo.js'; // обязательно .js!
Если забудете — получите ошибку вроде:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module ...
Нельзя импортировать JSON "в лоб"
В CommonJS было можно:
const data = require('./data.json');
В ESM так уже нельзя без дополнительных флагов/опций. Для этого используйте динамический импорт + опцию assert:
import data from './data.json' assert { type: 'json' };
Но: поддержка assert { type: 'json' } появилась только в Node 17+, и не все инструменты её любят.
Top-level await
ESM позволяет использовать await прямо на верхнем уровне модуля (не только внутри функции):
// main.js
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
Это невозможно в CommonJS без async IIFE.
Динамический импорт
Можно импортировать модули "на лету":
if (needExtraMath) {
const math = await import('./math.js');
console.log(math.PI);
}
Как импортировать сторонние модули из npm
Всё работает привычно:
npm install lodash
В коде:
import _ from 'lodash';
console.log(_.random(1, 10));
Но! Некоторые старые npm-библиотеки не поддерживают ESM. Если возникли ошибки — проверьте документацию пакета. Иногда приходится использовать "default" импорт или динамический импорт.
7. Типичные ошибки при работе с ES Modules в Node.js
Ошибка №1: забыли указать расширение файла в импорте.
В отличие от CommonJS, ESM требует писать import { foo } from './foo.js', а не просто './foo'.
Ошибка №2: смешивание синтаксиса.
Нельзя писать require() и module.exports в файле, который работает как ESM (и наоборот). Если используете ESM — только import/export.
Ошибка №3: забыли добавить "type": "module" в package.json.
Node.js не поймёт, что ваши .js — это ES-модули, и выдаст ошибку синтаксиса при виде import.
Ошибка №4: неправильный импорт default/именованных экспортов.
Если в модуле экспорт только default, импортируйте его как import foo from .... Если именованный — через { ... }. Путаница приводит к undefined.
Ошибка №5: попытка импортировать JSON как модуль без assert.
В ESM для импорта JSON нужен синтаксис import data from './data.json' assert { type: 'json' }; и Node.js 17+.
Ошибка №6: попытка использовать top-level await в CommonJS.
Top-level await работает только в ESM.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ