Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «CommonJS проти es модулів у Node.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**CommonJS vs ES Modules** - дві модульні системи в Node.js з різним синтаксисом і поведінкою завантаження. ```js const { add } = require('./math'); // CJS: синхронно, під час виконання import { add } from './math.js'; // ESM: статично, до запуску коду ``` **Головне:** CommonJS завантажує модулі при виклику `require()`. ESM аналізує імпорти статично до запуску будь-якого коду, що дає tree-shaking і top-level `await`. Для нових проєктів бери ESM; для старих кодових баз або пакетів без підтримки ESM CJS досі підходить.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**CommonJS vs ES Modules** - Node.js має дві модульні системи, і вибір не тієї призводить до реальних проблем під час виконання. ## Теорія ### TL;DR - CommonJS використовує `require()` / `module.exports`; ESM використовує `import` / `export` - CommonJS завантажує синхронно під час виконання; ESM аналізується статично до запуску будь-якого коду - CJS не може зробити `require()` ESM-файлу напряму; ESM може імпортувати CJS (тільки default export) - Новий проєкт? Бери ESM. Підтримуєш старий код? CJS досі нормально працює. - `__dirname` в ESM не існує; відтворюєш через `import.meta.url` ### Швидкий приклад ```js // CommonJS const { add } = require('./math'); // файл читається на цьому рядку console.log(add(2, 3)); // 5 // ES Modules import { add } from './math.mjs'; // розв'язується до запуску коду console.log(add(2, 3)); // 5 ``` Різниця в синтаксисі очевидна. Менш очевидно те, *коли* кожен файл фактично читається. CommonJS читає файл у момент виклику `require()`. ESM розв'язує всі імпорти до того, як запуститься будь-який рядок твого коду. ### Головна різниця CommonJS створювався для серверного коду, де читання з диска прийнятне і модулі завантажуються по одному. ESM створювався для браузера, де порядок завантаження важливий і бандлери повинні заздалегідь знати, які саме експорти є у файлі. Node.js додав підтримку ESM пізніше, тому обидві системи співіснують. У них окремі кеші модулів: завантажити один файл один раз як CJS і один раз як ESM означає отримати два різних екземпляри модуля. ### Таблиця порівняння | Характеристика | CommonJS | ES Modules | |---|---|---| | Синтаксис | `require()` / `module.exports` | `import` / `export` | | Завантаження | Синхронне, під час виконання | Статичне, до запуску | | Tree-shaking | Ні | Так | | Top-level `await` | Ні | Так | | `__dirname` | Є | Через `import.meta.url` | | За замовчуванням | Так (файли `.js`) | `.mjs` або `"type":"module"` | | CJS імпортує ESM | Тільки через `await import()` | N/A | | ESM імпортує CJS | Так (тільки default export) | N/A | ### Коли що використовувати - Новий пакет або застосунок: бери ESM. Отримуєш tree-shaking, top-level `await` і збіг із браузерним JavaScript. - Бібліотека для Node.js нижче 12: CommonJS досі доречний. - Змішана кодова база: ESM може імпортувати CJS-файли, тому мігрувати можна поступово. - Публікація dual-формату: вказуй і `"main"` (CJS), і `"exports"` (ESM) у `package.json`. ### Як Node.js вирішує, яку систему використовувати Node.js дивиться спочатку на розширення файлу, потім на `package.json`. Файл `.cjs` завжди CommonJS. Файл `.mjs` завжди ESM. Звичайний `.js` файл слідує полю `"type"` у найближчому `package.json`. Якщо поля `"type"` немає, за замовчуванням береться `"type": "commonjs"`. Окремі кеші модулів означають, що singleton-патерн поводиться по-різному залежно від системи. ### `__dirname` в ESM CommonJS автоматично надає `__dirname` і `__filename`. В ESM їх немає. Відтворюєш вручну: ```js import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); ``` Цей шаблонний код трапляється часто при міграції існуючого Node.js-коду на ESM. ### Сумісність між системами ```js // ESM імпортує CJS - працює, але тільки default export import mathModule from './math.cjs'; // CJS намагається require() ESM-файл - кидає помилку під час виконання const { add } = require('./math.mjs'); // Error: require() of ES Module not supported // CJS завантажує ESM через динамічний import - працює const { add } = await import('./math.mjs'); ``` Однобічне обмеження має значення. Якщо опублікувати пакет тільки у форматі ESM, кожен CJS-споживач змушений переходити на `await import()` і потрапляти в async-контекст. Частина команд вважає це достатньою причиною залишитися на CJS ще якийсь час. Саме тому популярні пакети на кшталт lodash і axios досі постачають CJS або dual-формат. ### Типові помилки **1. Пропущене розширення файлу в ESM-імпортах** ```js // CommonJS - розширення не обов'язкове, Node підбирає сам const math = require('./math'); // ESM - розширення обов'язкове import { add } from './math'; // Error: Cannot find module import { add } from './math.js'; // Правильно ``` Node.js не підбирає розширення автоматично в режимі ESM. Браузери ніколи так не робили. ESM в Node.js слідує тому ж правилу. **2. Використання `require()` в `.mjs`-файлі** ```js // math.mjs const fs = require('fs'); // ReferenceError: require is not defined in ES module scope ``` `require` не існує в ESM-скопі. Використовуй `import`, або, якщо без нього ніяк, `createRequire` з вбудованого пакету `module`. **3. Додавання `"type": "module"` без перейменування CJS-файлів** Якщо додати `"type": "module"` до `package.json`, кожен `.js` файл у тій директорії стає ESM. Будь-який файл, що досі використовує `require()`, одразу ламається. Спочатку перейменуй такі файли на `.cjs`. **4. Кругові залежності (circular dependencies) з різним таймінгом** CommonJS при circular `require()` повертає неповний експорт у момент циклу. ESM використовує live bindings, тому значення оновлюються, коли модуль-експортер їх нарешті встановлює. Обидва варіанти технічно працюють, але з різним таймінгом. Покладатися на кругові залежності в будь-якій системі - шлях до заплутаних багів. ### Де зустрічається в реальному коді - React-проєкти на Vite: ESM за замовчуванням, tree-shaking з коробки - Express-застосунки: здебільшого CommonJS, поступова міграція через `"type":"module"` - Node.js CLI-інструменти: часто CommonJS через сумісність зі старим інструментарієм - npm-пакети у dual-форматі: поле `"exports"` у `package.json` покриває обидва варіанти ### Питання на співбесіді **Q:** Чи можна використовувати top-level `await` у CommonJS? **A:** Ні. `await` у CommonJS працює тільки всередині `async`-функції. Top-level `await` - це виключно ESM. **Q:** Що станеться, якщо два пакети завантажать один і той самий CJS-модуль? **A:** CommonJS кешує модулі за шляхом файлу, тому вони отримають один і той самий екземпляр. Саме тому singleton-патерн надійно працює в CJS. **Q:** Чому деякі пакети досі не підтримують ESM? **A:** Публікація пакету тільки у форматі ESM ламає всіх CJS-споживачів, поки вони не оновляться під `await import()`. Більшість мейнтейнерів постачають dual-формат, щоб уникнути цього. **Q:** Як TypeScript співпрацює з CommonJS і ESM? **A:** TypeScript компілює в те, що вказано в `tsconfig.json`. `"module": "CommonJS"` дає CJS. `"module": "NodeNext"` дає ESM. Синтаксис TypeScript-коду виглядає як ESM в будь-якому разі. **Q:** Що таке dual-format пакет? **A:** Пакет, який постачає і CJS, і ESM збірки. Поле `"exports"` у `package.json` вказує Node.js, який файл брати залежно від того, чи споживач використовує `require()` або `import`: ```json { "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs" } } } ``` ## Приклади ### Базовий експорт і імпорт: обидві системи поруч ```js // --- CommonJS --- // math.js function multiply(a, b) { return a * b; } module.exports = { multiply }; // app.js const { multiply } = require('./math'); console.log(multiply(3, 4)); // 12 // --- ES Modules --- // math.mjs export function multiply(a, b) { return a * b; } // app.mjs import { multiply } from './math.mjs'; console.log(multiply(3, 4)); // 12 ``` Обидва варіанти працюють. Різниця проявляється, коли бандлер обробляє код: ESM дозволяє прибрати `multiply`, якщо функція ніде не використовується. CommonJS цього не може, бо бандлер не знає заздалегідь, що знадобиться під час виконання. ### Міграція файлу маршрутів Express на ESM ```js // До (CommonJS) const express = require('express'); const { getUser } = require('./services/user'); const router = express.Router(); router.get('/user/:id', async (req, res) => { const user = await getUser(req.params.id); res.json(user); }); module.exports = router; // Після (ESM) - додай "type": "module" до package.json import express from 'express'; import { getUser } from './services/user.js'; // тепер розширення обов'язкове const router = express.Router(); router.get('/user/:id', async (req, res) => { const user = await getUser(req.params.id); res.json(user); }); export default router; ``` Логіка ідентична. Змінюються три речі: `require` стає `import`, `module.exports` стає `export default`, і розширення файлу в шляху імпорту стає обов'язковим. ### Динамічний import у CommonJS-файлі ```js // server.js (CommonJS) async function loadConfig() { // Єдиний спосіб для CJS завантажити ESM-модуль const { parseConfig } = await import('./config.mjs'); return parseConfig(process.env); } loadConfig().then(config => { console.log('Server config:', config); }); ``` Цей патерн трапляється, коли є CJS-кодова база, але якась залежність публікується тільки у форматі ESM. Динамічний `import()` працює в обох системах, тому слугує мостом між ними без переписування всього коду.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.