Пошук уроків, статей та іншого контенту
Порівняємо CommonJS і ESM за синтаксисом, сумісністю, завантаженням та налаштуванням Node.js-проєкту.
Node.js підтримує дві основні модульні системи:
CommonJS (CJS) — історичний формат модулів Node.js;
ECMAScript Modules (ESM) — стандартний формат модулів JavaScript.
Обидві системи дають змогу розділяти програму на файли та повторно використовувати код, але мають різний синтаксис, правила завантаження й налаштування проєкту.
У CommonJS:
залежності підключаються через require();
значення експортуються через module.exports або exports;
модулі завантажуються синхронно;
за замовчуванням файл із розширенням .js у Node.js обробляється як CommonJS, якщо проєкт не налаштований інакше.
Файл math.cjs:
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = {
add,
multiply,
};Файл app.cjs:
const { add, multiply } = require("./math.cjs");
console.log(add(2, 3));
console.log(multiply(4, 5));Запуск:
node app.cjsДля CommonJS можна використовувати розширення .cjs, щоб явно вказати формат файлу.
Якщо модуль має експортувати одне значення, часто використовують module.exports:
// logger.cjs
function log(message) {
console.log(`[LOG] ${message}`);
}
module.exports = log;// app.cjs
const log = require("./logger.cjs");
log("Застосунок запущено");exports є посиланням на початковий об’єкт module.exports, тому ці два варіанти еквівалентні лише для додавання властивостей:
exports.add = add;module.exports.add = add;Але так робити небезпечно:
exports = add;Це лише змінює локальну змінну exports, але не змінює справжнє значення, яке буде повернуто з require().
Для експорту одного значення використовуйте:
module.exports = add;В ESM:
залежності підключаються через import;
значення експортуються через export;
імпорти є статично визначеними;
модулі можуть використовувати верхньорівневий await;
формат можна вказати через .mjs або через "type": "module" у package.json.
Файл math.mjs:
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}Файл app.mjs:
import { add, multiply } from "./math.mjs";
console.log(add(2, 3));
console.log(multiply(4, 5));Запуск:
node app.mjsНазви іменованих імпортів мають відповідати назвам експортів:
import { add } from "./math.mjs";Модуль може мати один експорт за замовчуванням:
// logger.mjs
export default function log(message) {
console.log(`[LOG] ${message}`);
}// app.mjs
import log from "./logger.mjs";
log("Застосунок запущено");Для default-імпорту назву можна вибрати самостійно:
import writeLog from "./logger.mjs";В одному ESM-модулі може бути лише один default-експорт, але багато іменованих експортів.
const path = require("node:path");
const { readFile } = require("node:fs/promises");
module.exports = {
path,
readFile,
};import path from "node:path";
import { readFile } from "node:fs/promises";
export {
path,
readFile,
};Вбудовані модулі Node.js можна імпортувати в ESM через префікс node:. Це явно показує, що модуль є вбудованим у Node.js.
package.jsonЄ кілька способів вказати Node.js, який формат використовувати.
"type": "module"Якщо в найближчому package.json вказано:
{
"type": "module"
}то файли .js у цьому пакеті вважаються ESM:
// math.js
export function add(a, b) {
return a + b;
}// app.js
import { add } from "./math.js";
console.log(add(2, 3));Запуск:
node app.jsУ ESM локальні імпорти потрібно писати з розширенням файлу:
import { add } from "./math.js";Варіант без розширення:
import { add } from "./math";для звичайного локального ESM-файлу не є коректним.
"type": "commonjs"Можна явно вказати CommonJS:
{
"type": "commonjs"
}Тоді файли .js обробляються як CommonJS:
// app.js
const os = require("node:os");
console.log(os.platform());Якщо поле "type" відсутнє, Node.js зазвичай трактує .js як CommonJS. Проте краще явно фіксувати формат у проєкті, особливо якщо код використовують інші розробники або інструменти.
.mjs і .cjsРозширення мають пріоритет над "type":
.mjs завжди є ESM;
.cjs завжди є CommonJS.
Наприклад, навіть у проєкті з "type": "module" файл .cjs залишиться CommonJS:
// legacy.cjs
const config = require("./config.cjs");
module.exports = config;Це зручно для поступової міграції проєкту або для файлів, які повинні мати однозначний формат.
require()require() виконується під час роботи програми та повертає експорт модуля:
const config = require("./config.cjs");Модуль завантажується синхронно. Node.js також кешує результат завантаження: повторний виклик require() для того самого модуля зазвичай повертає вже завантажений об’єкт.
Статичний імпорт пишеться на верхньому рівні:
import { readFile } from "node:fs/promises";Node.js може визначити залежності модуля ще до виконання його основного коду. Це важливо для аналізу графа залежностей і відрізняє ESM від динамічного виклику require().
Імпорт має бути оголошений на верхньому рівні:
import { add } from "./math.js";Такий запис усередині умови не використовується:
if (useMath) {
// Некоректно: статичний import не можна розміщувати всередині блоку
import { add } from "./math.js";
}import()Якщо модуль потрібно завантажити за умовою або під час виконання, використовуйте динамічний import():
const moduleName = "./math.mjs";
const math = await import(moduleName);
console.log(math.add(2, 3));import() повертає Promise, тому його використовують з await або .then():
import("./math.mjs")
.then((math) => {
console.log(math.multiply(3, 4));
})
.catch((error) => {
console.error("Не вдалося завантажити модуль:", error);
});Динамічний import() доступний і в CommonJS:
// app.cjs
async function main() {
const math = await import("./math.mjs");
console.log(math.add(2, 3));
}
main().catch((error) => {
console.error(error);
});Це основний спосіб підключити ESM-модуль із CommonJS-коду.
У реальному проєкті часто зустрічаються залежності обох форматів.
ESM може імпортувати CommonJS-модуль:
// config.cjs
module.exports = {
port: 3000,
environment: "development",
};// app.mjs
import config from "./config.cjs";
console.log(config.port);
console.log(config.environment);Для CommonJS-модуля default-імпорт зазвичай є найнадійнішим варіантом.
Іменовані імпорти з CommonJS також можуть працювати:
import { port } from "./config.cjs";Але Node.js визначає такі експорти статичним аналізом CommonJS-коду. Тому named imports для CommonJS можуть бути недоступними або поводитися не так, як очікується. Якщо потрібна надійна сумісність, імпортуйте весь модуль як default.
CommonJS не може використовувати звичайний статичний import у своєму файлі. Для ESM використовуйте динамічний імпорт:
// app.cjs
async function start() {
const { add } = await import("./math.mjs");
console.log(add(10, 20));
}
start().catch((error) => {
console.error("Помилка запуску:", error);
});Такий код асинхронний, оскільки import() повертає Promise.
У CommonJS доступні спеціальні змінні:
console.log(__filename);
console.log(__dirname);__filename — абсолютний шлях до поточного файлу;
__dirname — абсолютний шлях до його каталогу.
В ESM цих змінних немає. Замість них використовується import.meta.url:
// app.mjs
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const currentFile = fileURLToPath(import.meta.url);
const currentDirectory = dirname(currentFile);
console.log(currentFile);
console.log(currentDirectory);import.meta.url містить URL поточного модуля, тому для роботи зі звичайними шляхами його перетворюють за допомогою fileURLToPath().
Структура:
my-app/
├── package.json
├── app.js
└── math.jspackage.json:
{
"name": "my-app",
"type": "module",
"private": true
}math.js:
export function add(a, b) {
return a + b;
}
export function divide(a, b) {
if (b === 0) {
throw new Error("Ділення на нуль неможливе");
}
return a / b;
}app.js:
import { add, divide } from "./math.js";
const firstNumber = 12;
const secondNumber = 4;
console.log("Сума:", add(firstNumber, secondNumber));
console.log("Частка:", divide(firstNumber, secondNumber));Запуск із каталогу my-app:
node app.jsРезультат:
Сума: 16
Частка: 3Для CommonJS той самий приклад можна записати з файлами .cjs:
math.cjs:
function add(a, b) {
return a + b;
}
function divide(a, b) {
if (b === 0) {
throw new Error("Ділення на нуль неможливе");
}
return a / b;
}
module.exports = {
add,
divide,
};app.cjs:
const { add, divide } = require("./math.cjs");
const firstNumber = 12;
const secondNumber = 4;
console.log("Сума:", add(firstNumber, secondNumber));
console.log("Частка:", divide(firstNumber, secondNumber));Оберіть ESM, якщо:
починаєте новий Node.js-проєкт;
хочете використовувати стандартний синтаксис JavaScript-модулів;
плануєте спільне використання модулів у Node.js та браузері;
потрібен верхньорівневий await.
Оберіть або залиште CommonJS, якщо:
проєкт уже побудований навколо require() і module.exports;
важлива сумісність зі старим кодом;
міграція всіх модулів зараз недоцільна.
Головне правило — не змішувати формати випадково. Визначте формат проєкту через "type" або використовуйте явні розширення .mjs і .cjs.
Не обов’язково переписувати весь проєкт одразу. Можна:
залишити наявні CommonJS-файли з розширенням .cjs;
нові модулі писати як ESM із розширенням .mjs;
підключати ESM із CommonJS через динамічний import();
поступово переводити файли до одного формату;
після міграції перейти на .js разом із "type": "module".
Під час міграції важливо перевіряти кожен локальний імпорт, розширення файлу та спосіб експорту.
import { add } from "./math";Для локального ESM-модуля потрібно:
import { add } from "./math.js";require() у ESM// app.js у проєкті з "type": "module"
const fs = require("node:fs");У ESM використовуйте import:
import fs from "node:fs";Або динамічний імпорт:
const fs = await import("node:fs");import у CommonJS без динамічного імпортуУ CommonJS для ESM-модуля потрібно:
const module = await import("./feature.mjs");Звичайний статичний import у файлі CommonJS не є правильним способом підключення.
Якщо CommonJS-модуль має такий експорт:
module.exports = {
add,
multiply,
};в ESM його надійно підключати як один об’єкт:
import math from "./math.cjs";
console.log(math.add(2, 3));Не варто без перевірки припускати, що кожна властивість CommonJS-модуля буде доступна як іменований ESM-експорт.
Якщо "type": "module" вже додано, але частина файлів містить require(), такі файли потрібно перейменувати на .cjs або переписати на ESM.
CommonJS використовує require() і module.exports.
ESM використовує import і export.
.mjs завжди означає ESM, а .cjs — CommonJS.
"type": "module" перетворює .js у ESM у межах пакета.
У ESM локальні імпорти зазвичай мають містити розширення файлу.
CommonJS завантажує модулі синхронно через require().
Динамічний import() асинхронний і дає змогу підключати модулі під час виконання.
ESM може імпортувати CommonJS, а CommonJS підключає ESM через динамічний import().
Для нового проєкту формат потрібно явно зафіксувати й послідовно використовувати в усіх файлах.