1. Основные отличия: CommonJS vs ESM
Node.js долгое время поддерживал только CommonJS. Позже, когда браузерный мир массово перешёл на ESM, Node.js решил: «А почему бы и нам не поддерживать оба стандарта?» И вот теперь у нас есть два параллельных способа описывать модули. Прогресс — штука сложная!
- CommonJS — это система модулей, с которой Node.js стартовал в 2009 году. Она была придумана специально для серверного JavaScript, когда о фронтенде ещё никто и не мечтал.
- ESM (ECMAScript Modules) — это стандартная система модулей, появившаяся в самом языке JavaScript (начиная с ES6, 2015 год). Она изначально создавалась для браузеров, чтобы можно было писать модульный код прямо на клиенте.
Давайте сравним эти две системы по ключевым параметрам.
| Характеристика | CommonJS | ESM (ECMAScript Modules) |
|---|---|---|
| Синтаксис импорта | |
|
| Синтаксис экспорта | |
|
| Расширение файлов | (по умолчанию) |
или с type:module |
| Загрузка модулей | Синхронная | Асинхронная |
| Контекст | Модуль — это функция | Модуль — это файл |
| this в модуле | |
|
| Доступ к require | Доступен всегда | Обычно недоступен |
| Возможность top-level await | Нет | Да (Node 14.8+ и ESM) |
| Динамический импорт | Можно: |
Можно: |
Синтаксис: require vs import
CommonJS:
// Импорт
const fs = require('fs');
// Экспорт
module.exports = function sayHello() { ... };
// или
exports.sayBye = function() { ... };
ESM:
// Импорт
import fs from 'fs';
// Экспорт
export default function sayHello() { ... }
export function sayBye() { ... }
Расширения файлов и package.json
CommonJS: Всё просто — любой .js файл по умолчанию считается CommonJS-модулем.
ESM:
- Можно использовать расширение .mjs (например, index.mjs).
- Или в package.json прописать "type": "module", тогда все .js файлы будут считаться ESM-модулями.
- Если "type": "commonjs" или поле отсутствует — .js по умолчанию CommonJS.
Пример package.json для ESM:
{
"type": "module"
}
Как Node.js понимает, какой модуль использовать?
- Если файл заканчивается на .mjs — это ESM.
- Если файл заканчивается на .cjs — это CommonJS (даже если type: module).
- Если файл заканчивается на .js:
- Если "type": "module" — это ESM.
- Если "type": "commonjs" или поле отсутствует — это CommonJS.
Загрузка модулей: синхронно или асинхронно?
CommonJS: Загрузка модулей происходит синхронно — код модулей выполняется сразу при require.
ESM: Импорт асинхронный — модули могут загружаться параллельно, а сам import работает как промис (особенно при динамическом импорте).
Экспорт по умолчанию и именованный экспорт
CommonJS: Можно экспортировать что угодно (функцию, объект, класс, число).
ESM: Можно делать export default (экспорт по умолчанию) и именованные экспорты (export const, export function).
2. Взаимная совместимость: можно ли смешивать CommonJS и ESM?
Это самый частый вопрос у всех, кто начинает работать с современным Node.js. Давайте разберёмся!
Импорт CommonJS из ESM
В целом, ESM может импортировать CommonJS-модули через обычный import, но есть нюансы:
// math.cjs (CommonJS)
module.exports = {
sum(a, b) { return a + b; }
};
// main.mjs (ESM)
import math from './math.cjs';
console.log(math.sum(2, 3)); // 5
- В этом случае весь экспортированный объект попадёт в default (то есть import math from ...).
- Если в CJS используется exports.foo = ..., то в ESM это будет math.foo.
Импорт ESM из CommonJS
Тут всё сложнее. В CommonJS нельзя использовать import (без дополнительных ухищрений). Но можно сделать динамический импорт через промис:
// myModule.mjs (ESM)
export function greet() {
console.log('Hello from ESM!');
}
// index.cjs (CommonJS)
(async () => {
const esmModule = await import('./myModule.mjs');
esmModule.greet();
})();
- Динамический импорт возвращает промис, поэтому нужен async/await или .then().
- Нельзя использовать статический import в CommonJS.
require внутри ESM
В ESM-модулях нельзя использовать require напрямую — это вызовет ошибку. Для импорта CJS-модуля используйте обычный import.
import внутри CommonJS
В CommonJS нельзя использовать статический import — только динамический через import().
3. Особенности совместимости: подводные камни
Экспорт по умолчанию
- В CommonJS можно сделать module.exports = function() {} или module.exports = { ... }.
- В ESM — export default ....
Импортируя CommonJS-модуль в ESM, весь экспорт попадёт в default:
// math.cjs
module.exports = function(a, b) { return a + b; };
// main.mjs
import sum from './math.cjs';
console.log(sum(2, 2)); // 4
Импортируя ESM-модуль в CommonJS, результат будет объект с полями:
// math.mjs
export function sum(a, b) { return a + b; }
// main.cjs
(async () => {
const math = await import('./math.mjs');
console.log(math.sum(2, 2)); // 4
})();
this и глобальные переменные
- В CommonJS this внутри модуля ссылается на module.exports.
- В ESM — this равен undefined.
Top-level await
- В CommonJS нельзя использовать await на верхнем уровне.
- В ESM (Node 14.8+) можно писать await прямо в файле.
Пример:
// esm.mjs
const data = await fetch('https://api.example.com/data');
4. Когда использовать CommonJS, а когда ESM?
CommonJS:
- Если вы пишете код только для Node.js и не планируете использовать его в браузере.
- Если используете старые версии Node.js (<12).
- Если работаете с большим количеством сторонних библиотек, которые написаны на CommonJS.
ESM:
- Если хотите писать современный код, совместимый с браузерами и Node.js.
- Если используете современные фичи (top-level await, tree-shaking).
- Если ваш проект новый, и вам не нужно поддерживать старые версии Node.js.
Совет: В новых проектах лучше сразу использовать ESM с "type": "module" в package.json, чтобы не мучиться с переходом позже.
Совместимость сторонних библиотек
Большинство старых npm-пакетов — CommonJS. Их можно импортировать в ESM, но иногда экспорты будут только в поле default.
Некоторые новые библиотеки публикуют две версии: CommonJS и ESM (например, через поле "exports" в package.json).
Если вы пишете библиотеку, старайтесь поддерживать оба формата, чтобы не оттолкнуть часть пользователей.
5. Примеры совместимости на практике
Пример 1: Импорт CommonJS-модуля в ESM
math.cjs
module.exports = {
sum(a, b) { return a + b; }
};
main.mjs
import math from './math.cjs';
console.log(math.sum(1, 2)); // 3
Пример 2: Импорт ESM-модуля в CommonJS
math.mjs
export function sum(a, b) { return a + b; }
main.cjs
(async () => {
const math = await import('./math.mjs');
console.log(math.sum(1, 2)); // 3
})();
Пример 3: Смешанный проект
Если у вас проект, где часть файлов — CommonJS, а часть — ESM, то:
- Используйте расширения .cjs для CommonJS и .mjs для ESM.
- Или настройте "type": "module" и используйте только ESM.
6. Типичные ошибки
Ошибка №1: Использование require в ESM
Если вы попробуете написать в ESM-модуле const fs = require('fs'), получите ошибку: ReferenceError: require is not defined in ES module scope. Используйте import fs from 'fs'.
Ошибка №2: Использование import в CommonJS
Попытка использовать import ... from ... в .js файле без "type": "module" вызовет ошибку: SyntaxError: Cannot use import statement outside a module.
Ошибка №3: Путаница с расширениями
Node.js не понимает, какой модуль загружать, если вы используете не те расширения (.js, .cjs, .mjs) или не настроили type в package.json.
Ошибка №4: Экспорт по умолчанию
Импортируя CommonJS-модуль в ESM, не забывайте, что весь экспорт попадёт в default. То есть, если в CJS module.exports = fn, то в ESM: import fn from './mod.cjs', а не import { fn } from ....
Ошибка №5: Смешивание синтаксиса
Иногда встречается код, где в одном файле используются и require, и import. Так делать нельзя — выберите один стиль для каждого файла.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ