Модули: CommonJS и ESM
Зачем модули
Модуль — отдельный файл с кодом, который можно подключать.
Плюсы:
- Изоляция — переменные не текут в глобальную область.
- Переиспользование — импорт в других файлах.
- Организация — проект разбит на части.
Две системы
Node.js поддерживает две системы модулей:
- CommonJS (CJS) — историческая,
require(). - ES Modules (ESM) — современная,
import.
CommonJS
Экспорт:
// math.js
function sum(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { sum, multiply };
Импорт:
// index.js
const { sum, multiply } = require('./math');
console.log(sum(2, 3)); // 5
console.log(multiply(2, 3)); // 6
require — синхронный.
Экспорт по одному
exports.sum = (a, b) => a + b;
exports.multiply = (a, b) => a * b;
exports — алиас на module.exports.
⚠️ Нельзя:
exports = { sum }; // ломает ссылку
Экспорт одной сущности
// logger.js
class Logger {
log(msg) { console.log(msg); }
}
module.exports = Logger;
const Logger = require('./logger');
const logger = new Logger();
ES Modules
package.json:
{
"type": "module"
}
Или файл с .mjs.
Экспорт:
// math.js
export function sum(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
Импорт:
// index.js
import { sum, multiply } from './math.js';
console.log(sum(2, 3)); // 5
console.log(multiply(2, 3)); // 6
Расширение .js обязательно — в отличие от CJS.
Default export
export default function sum(a, b) {
return a + b;
}
import sum from './math.js';
Один default на модуль. Плюс сколько угодно named.
Смешанный экспорт
export default class User { }
export function createUser() { }
export const MAX_AGE = 120;
import User, { createUser, MAX_AGE } from './user.js';
Переименование
import { sum as add } from './math.js';
При экспорте:
export { sum as add };
Namespace-импорт
import * as math from './math.js';
math.sum(2, 3);
math.multiply(2, 3);
Импорт целиком
ESM:
import './setup.js'; // выполнится, но ничего не импортируется
CJS:
require('./setup');
Сравнение
| CommonJS | ESM | |
|---|---|---|
| Синтаксис | require | import |
| Загрузка | Синхронная | Асинхронная |
| Динамический импорт | Да | await import() |
this | module.exports | undefined |
| Tree-shaking | Нет | Да |
| Top-level await | Нет | Да |
| В браузере | Нет | Да |
Динамический импорт
ESM:
const module = await import('./heavy.js');
module.run();
Ленивая загрузка — модуль грузится только когда нужно.
CJS:
const module = require('./heavy');
Синхронно — грузится всегда.
Встроенные модули
Node.js даёт из коробки:
// CJS
const fs = require('node:fs');
const path = require('node:path');
// ESM
import fs from 'node:fs';
import path from 'node:path';
Префикс node: — рекомендован, отделяет встроенные от npm.
Основные модули:
node:fs— файлы.node:path— пути.node:os— система.node:http— сервер.node:https— HTTPS.node:crypto— криптография.node:events— события.node:stream— стримы.node:url— URL.node:util— утилиты.
ESM vs CJS в одном проекте
Проблема: пакеты в CJS, ты пишешь на ESM.
ESM может импортировать CJS:
import express from 'express'; // express в CJS
CJS не может импортировать ESM (без await import()).
Правило: новый проект — ESM. Старый — как есть.
Файловые расширения
.js— что скажетpackage.json..mjs— всегда ESM..cjs— всегда CJS..ts— TypeScript, компилируется.
Практический пример
Проект:
src/
├── utils/
│ ├── math.js
│ └── string.js
├── services/
│ └── user.js
└── index.js
src/utils/math.js:
export function sum(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export default { sum, multiply };
src/utils/string.js:
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
src/services/user.js:
import { capitalize } from '../utils/string.js';
export class User {
constructor(name) {
this.name = capitalize(name);
}
greet() {
return `Привет, ${this.name}`;
}
}
src/index.js:
import { sum, multiply } from './utils/math.js';
import { User } from './services/user.js';
const user = new User('иван');
console.log(user.greet());
console.log(sum(2, 3));
console.log(multiply(2, 3));
package.json:
{
"type": "module",
"scripts": {
"start": "node src/index.js"
}
}
Типичные ошибки
1. Забыл расширение в ESM:
import { sum } from './math'; // ❌
import { sum } from './math.js'; // ✅
2. Не указал type: module:
import ... // SyntaxError
3. Смешал module.exports и export:
module.exports = { };
export const x = 1; // ❌ нельзя вместе
4. Циклические зависимости. Работают, но осторожно.
Полезные приёмы
Динамический импорт для условной загрузки:
if (process.env.NODE_ENV === 'development') {
const devTools = await import('./dev-tools.js');
devTools.enable();
}
Импорт JSON:
import config from './config.json' with { type: 'json' };
Top-level await:
const data = await fetch('https://api.example.com').then(r => r.json());
export default data;
Итог
- CommonJS — старый стандарт,
require. - ESM — современный,
import. type: module— включает ESM..mjs/.cjs— явные расширения.- ESM — асинхронный, tree-shaking, top-level await.
- Новый проект — ESM.
- Встроенные модули —
node:fs,node:pathи т.д.
Дальше — файловая система.