CommonJS — спецификация модульной системы, разработанная для серверной среды JavaScript задолго до появления стандартных ES-модулей. На протяжении многих лет CommonJS являлся основным способом организации кода в Node.js и до сих пор широко используется в существующих проектах, библиотеках и инструментах сборки.
Главная задача CommonJS заключается в разделении приложения на независимые модули, каждый из которых содержит собственную область видимости и может экспортировать необходимые данные наружу.
Основные возможности:
В экосистеме Node.js большинство старых пакетов распространяется именно в формате CommonJS.
Модуль CommonJS представляет собой обычный JavaScript-файл.
Для экспорта используются:
module.exports
или
exports
Для импорта используется функция:
require()
Структура может выглядеть следующим образом:
project/
├── app.js
├── math.js
└── logger.js
Наиболее распространённый способ экспорта — присвоение значения
объекту module.exports.
Файл:
// math.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
Использование:
// app.js
const sum = require("./math");
console.log(sum(5, 3));
Результат:
8
В данном случае модуль экспортирует единственную функцию.
Чаще требуется экспортировать несколько сущностей одновременно.
// math.js
function sum(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = {
sum,
multiply
};
Импорт:
const math = require("./math");
console.log(math.sum(2, 3));
console.log(math.multiply(2, 3));
Либо с деструктуризацией:
const { sum, multiply } = require("./math");
console.log(sum(2, 3));
console.log(multiply(2, 3));
Node.js создаёт сокращённую ссылку на
module.exports:
exports
Пример:
exports.sum = function(a, b) {
return a + b;
};
exports.multiply = function(a, b) {
return a * b;
};
Импорт:
const math = require("./math");
console.log(math.sum(10, 5));
Фактически код эквивалентен:
module.exports.sum = ...
module.exports.multiply = ...
Это один из самых важных моментов при работе с CommonJS.
Изначально:
exports === module.exports
Однако присваивание нового значения ломает связь.
Правильно:
exports.sum = sum;
Неправильно:
exports = sum;
В таком случае экспорт не сработает.
Если требуется экспортировать одно значение целиком, необходимо использовать:
module.exports = sum;
Пример ошибки:
exports = {
sum
};
Результат:
{}
Потому что Node.js экспортирует именно
module.exports.
Функция require() загружает модуль и возвращает
экспортированное значение.
Импорт локального файла:
const math = require("./math");
Импорт из родительской директории:
const config = require("../config");
Импорт встроенного модуля Node.js:
const fs = require("fs");
Импорт стороннего пакета:
const express = require("express");
При вызове:
require("./utils")
Node.js выполняет поиск в определённом порядке.
Сначала:
utils.js
Затем:
utils.json
После этого:
utils.node
Если путь указывает на каталог:
utils/
Node.js ищет:
utils/package.json
и поле:
{
"main": "index.js"
}
Если файл не найден, проверяется:
utils/index.js
Каждый модуль загружается только один раз.
Файл:
// counter.js
let count = 0;
count++;
console.log("Module loaded");
module.exports = count;
Использование:
const a = require("./counter");
const b = require("./counter");
Вывод:
Module loaded
Сообщение появляется один раз, поскольку результат сохраняется в кэше.
Кэш доступен через:
require.cache
Просмотр:
console.log(require.cache);
Удаление конкретного модуля:
delete require.cache[
require.resolve("./math")
];
Следующий вызов:
require("./math");
загрузит файл заново.
Перед выполнением Node.js автоматически оборачивает код файла в функцию.
Упрощённо:
(function(
exports,
require,
module,
__filename,
__dirname
) {
// код файла
});
Благодаря этому каждый модуль имеет собственную область видимости.
Переменные одного файла не становятся глобальными.
Каждый CommonJS-модуль получает объект module.
Пример:
console.log(module);
Некоторые полезные свойства:
module.id
module.path
module.filename
module.loaded
module.parent
module.children
module.exports
Наиболее важным является:
module.exports
Именно он определяет экспортируемое значение.
Содержит абсолютный путь к текущему файлу.
Пример:
console.log(__filename);
Результат:
C:\project\app.js
Используется для получения расположения текущего модуля.
Содержит абсолютный путь к каталогу текущего файла.
Пример:
console.log(__dirname);
Результат:
C:\project
Часто применяется для работы с файлами.
const path = require("path");
const filePath =
path.join(__dirname, "data.json");
Циклическая зависимость возникает, когда два модуля импортируют друг друга.
Пример:
// a.js
const b = require("./b");
module.exports = {
value: "A"
};
// b.js
const a = require("./a");
module.exports = {
value: "B"
};
Node.js способен обработать такую ситуацию благодаря механизму кэширования, однако часть экспортируемых данных может оказаться недоступной на момент загрузки.
Поэтому циклические зависимости считаются плохой архитектурной практикой.
Особенность CommonJS заключается в синхронной работе функции
require().
const math = require("./math");
Выполнение программы останавливается до завершения загрузки модуля.
Для серверной среды это приемлемо, поскольку большинство модулей загружается во время запуска приложения.
Для браузеров такой подход менее эффективен, поэтому позже появились ES-модули с поддержкой асинхронной загрузки.
С появлением стандарта ECMAScript возник новый формат модулей — ES Modules (ESM).
Сравнение синтаксиса:
const math = require("./math");
module.exports = {
sum
};
import { sum } from "./math.js";
export { sum };
Основные отличия:
| Возможность | CommonJS | ES Modules |
|---|---|---|
| Импорт | require() | import |
| Экспорт | module.exports | export |
| Загрузка | Синхронная | Асинхронная |
| Стандарт ECMAScript | Нет | Да |
| Анализ зависимостей | Во время выполнения | До выполнения |
| Tree Shaking | Ограничен | Полноценный |
Esbuild полностью поддерживает CommonJS и умеет:
require().Пример файла:
const fs = require("fs");
module.exports = function() {
console.log("Hello");
};
Сборка:
esbuild app.js --bundle --outfile=bundle.js
Esbuild распознаёт CommonJS-модуль и корректно включает его в итоговый бандл.
Исходный файл:
module.exports = {
hello() {
console.log("Hello");
}
};
Команда:
esbuild app.js \
--bundle \
--format=esm \
--outfile=app.mjs
Esbuild создаёт совместимый ESM-код.
Это особенно полезно при миграции старых проектов на современную модульную систему.
Исходный код:
export function hello() {
console.log("Hello");
}
Сборка:
esbuild app.js \
--bundle \
--format=cjs \
--outfile=app.js
Результат становится пригодным для выполнения в среде Node.js, использующей CommonJS.
Одним из ограничений CommonJS является сложность статического анализа.
Например:
const moduleName = getModuleName();
const mod = require(moduleName);
Во время сборки невозможно заранее определить импортируемый модуль.
Из-за этого инструменты оптимизации работают менее эффективно.
ESM выглядит более предсказуемо:
import { sum } from "./math.js";
Поэтому при использовании CommonJS возможности tree shaking ограничены.
Esbuild всё равно выполняет множество оптимизаций, однако максимальная эффективность достигается при использовании ES Modules.
Несмотря на активное распространение ES Modules, CommonJS продолжает играть важную роль:
По этой причине понимание принципов работы require(),
exports и module.exports остаётся важным
навыком при разработке приложений на Node.js и при использовании Esbuild
для сборки, оптимизации и трансформации модульного кода.