Урок 03

Модули: 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');

Сравнение

CommonJSESM
Синтаксисrequireimport
ЗагрузкаСинхроннаяАсинхронная
Динамический импортДаawait import()
thismodule.exportsundefined
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 и т.д.

Дальше — файловая система.