CommonJS и ECMAScript Modules в JavaScript: в чём разница и как с ними жить?

CommonJS и ECMAScript Modules в JavaScript: в чём разница и как с ними жить?

Olga Krylova

В языке JavaScript существует две основные системы модулей: CommonJS (далее будем называть его просто CJS) и ECMAScript Modules (далее ESM). Основная причина наличия двух различных систем модулей заключается в истории развития JavaScript как языка и его экосистемы.

Что за модули?

По сути модуль в JavaScript — это просто файл, в котором есть некоторый код. Чаще всего модули импортируют что-либо извне и экспортируют свои переменные или функции наружу. И под системой модулей имеется в виду синтаксис для импортов и экспортов, а также работа этого механизма под капотом. Базово про то, что такое модуль, можно прочитать в этой статье, а дальше мы будем разбирать особенности именно систем модулей.

CommonJS

Эта система модулей разработана первоначально для серверной среды Node.js. Она предназначена для синхронной загрузки модулей, что идеально подходит для серверных сценариев, где все файлы находятся локально. Для импорта внешних модулей используется ключевое слово require, а для экспорта синтаксис module.exports. Например:

// someConst.js
const someConst = 1;
module.exports = { someConst };

// другой файл
const { someConst } = require('./someConst');
console.log(someConst);

Есть также ещё 2 варианта, как можно экспортировать переменные:

1. Использовать ключевое слово exports, которое ссылается на module.exports:

// someConst.js
exports.someConst = someConst;

// другой файл
const { someConst } = require('./someConst');

2. Использовать дефолтный экспорт:

// someConst.js
module.exports = someConst;

// другой файл
const someConst = require('./someConst');

ECMAScript Modules

Эта система модулей введена в ECMAScript 2015 (ES6) и сейчас представляет собой стандарт для работы с модулями в JavaScript. ESM поддерживает асинхронную загрузку модулей и использует синтаксис import и export для управления зависимостями. Это делает ESM более подходящим для использования в браузерах, где модули могут быть загружены асинхронно из различных источников. Пример:

// constants.js
export const a = 1;
export default b = 2;

// другой файл
import b, { a } from './constants';

Здесь также поддержан дефолтный экспорт. Но вообще, есть несколько причин, почему такими экспортами лучше не злоупотреблять.

Как определить систему модулей

На самом деле, кроме формата файлов .js могут также использоваться .cjs и .mjs для CJS и ESM соответственно. Тогда, при запуске скрипта через node будет понятно, какой синтаксис используется и файл будет обработан корректно.

Но чаще используется другой подход. С помощью специального свойства type в package.json можно определить, какая система модулей будет использоваться в проекте по умолчанию:

  • "type": "module" определит все JavaScript файлы как ESM. Если нужно использовать CJS, то необходимо дать таким файлам расширение .cjs, и тогда они будут обрабатываться корректно.
  • "type": "commonjs" или отсутствие этого поля в package.json говорят о том, что по умолчанию используются CJS модули. Чтобы использовать ESM, нужно указывать у таких файлов расширение .mjs соответственно.

Могут ли эти модули дружить?

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

Первый для демонстрации CJS — test.cjs

const cjsConst = 'Hello from CJS!';
console.log('CJS:', cjsConst); // Для проверки работы скрипта
module.exports = { cjsConst };

Второй для ESM — test.mjs

export const esmConst = 'Hello from ESM!';
console.log('ESM:', esmConst); // Для проверки работы скрипта

Импорт CJS в ESM

Так как import в ESM позволяет обрабатывать синхронную загрузку, в этом случае нет особых проблем. Например, импортируем переменную cjsConst из test.cjs:

import { cjsConst } from './test.cjs';

export const esmConst = 'Hello from ESM!';
console.log('ESM:', esmConst);

console.log('ESM:': cjsConst);

Если запустить этот файл командой node test.mjs, то вывод в консоли будет следующий:

CJS: Hello from CJS!
ESM: Hello from ESM!
ESM: Hello from CJS!

Сначала сработал вывод из файла test.cjs. Это вполне ожидаемо, так при импорте модуля код в нём выполняется. Затем вывелась переменная из файла, который был запущем, и затем наша импортированная переменная. Как можно заметить, всё работает без проблем.

Импорт ESM в CJS

В этой ситуации действовать нужно немного сложнее. Если мы попробуем импортировать обычным способом переменную из test.mjs:

// test.cjs
const { esmConst } = require('./test.mjs');

то получим следующую ошибку:

Error [ERR_REQUIRE_ESM]: require() of ES Module {path} not supported.
Instead change the require of {path} to a dynamic import() which is available in all CommonJS modules.

Что это значит? Это значит, что в таком случае нужно использовать динамические (или асинхронные) импорты, которые задаются выражением import(), возвращающим Promise со всеми экспортами модуля. Однако, так как использовать ключевое слово await на верхнем уровне CJS запрещено, то есть 2 варианта использования такого импорта.

Первый — использовать его внутри асинхронной функции:

const cjsConst = 'Hello from CJS!';

const main = async () => {
    const esModule = await import('./test.mjs');
    console.log('CJS:', esModule.esmConst)
}

console.log('CJS:', cjsConst);
main();

Второй — использовать конструкцию .then:

const cjsConst = 'Hello from CJS!';

import('./test.mjs').then((esModule) => {
    console.log('CJS:', esModule.esmConst)
});

console.log('CJS:', cjsConst);

В обоих случаях вывод в консоль будет одинаковый:

CJS: Hello from CJS!
ESM: Hello from ESM!
CJS: Hello from ESM!

Сначала выведется внутренняя переменная, так как console.log отработает до завершения промиса. Затем выведется переменная внутри импортируемого файла, так как скрипт выполнится при импорте. И затем выведется переменная, которую мы импортировали.

Импортируемый скрипт в обоих случаях (ESM и CJS) выполнится только при первом импорте. Если импортировать модуль несколько раз внутри одного и даже нескольких модулей, то он выполнится только один раз.

В этой статье мы разобрали, что такое системы модулей и чем они отличаются друг от друга. Ставьте реакции, если хотите узнать, как эти системы работают в современном фронтенде на примере UI-библиотеки, написанной на React!

Report Page