⚡ JavaScript

Модули, пакеты и импорт в JavaScript (Node.js): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты строишь огромный дом. Ты же не будешь сам делать кирпичи, варить стекло и рубить лес? Конечно нет! Ты купишь готовые материалы: кирпичи на заводе, окна в магазине, а доски на лесопилке. Так и в программировании: когда программа становится большой, писать весь код в одном файле — это как пытаться построить небоскрёб из одного гигантского куска пластилина. Получится неудобно, запутанно и сломается при первой же попытке что-то изменить.

Модули — это те самые «готовые кирпичи» для твоей программы. Каждый модуль — это отдельный файл (или кусок кода), который делает что-то одно, но делает это хорошо. Например, один модуль умеет отправлять письма, второй — рисовать графики, третий — работать с базой данных.

Пакеты — это целые «наборы инструментов» или «коробки с деталями». Если модуль — это один кирпич, то пакет — это целый поддон кирпичей, плюс инструкция по кладке, плюс мастерок. В мире JavaScript (Node.js) пакеты — это готовые библиотеки, которые написали другие программисты и выложили в интернет, чтобы ты мог их скачать и использовать.

Импорт — это процесс, когда ты говоришь программе: «Эй, принеси мне вон тот кирпич из того склада!». То есть ты берёшь готовый код из другого файла или из пакета и используешь его в своём проекте.

💡 Важно: Без модулей и импорта каждый раз пришлось бы писать всё с нуля. Представь, что каждый раз, когда тебе нужно посчитать квадратный корень, ты заново изобретаешь математику. Модули позволяют не изобретать велосипед, а брать готовый, проверенный тысячами людей «велосипед» и ехать на нём.

Зачем это нужно?

  • Порядок: Код разбит на маленькие, понятные кусочки. Легко найти ошибку.
  • Переиспользование: Написал один раз модуль для расчёта скидки — используй его в 10 разных проектах.
  • Командная работа: Один программист пишет модуль для входа в систему, другой — для чата. Они не мешают друг другу.
  • Экономия времени: Не нужно писать сложные вещи самому. Скачал пакет (например, для отправки SMS) — и готово.

Теория: как это работает изнутри

Давай заглянем «под капот» Node.js и посмотрим, что происходит, когда ты пишешь require('fs') или import fs from 'fs'.

В Node.js существует две основные системы модулей: CommonJS (CJS) и ES Modules (ESM). Они как два разных языка внутри одного: оба решают одну задачу, но делают это по-разному.

CommonJS (CJS) — старая, но надёжная школа

Раньше в JavaScript не было встроенной системы модулей. Node.js придумал свою — CommonJS. Она работает так:

  • Каждый файл — это отдельный модуль.
  • Переменные и функции, объявленные в файле, не видны снаружи, пока их специально не «экспортировать» (выставить наружу).
  • Чтобы взять что-то из другого файла, используется функция require().
  • Чтобы отдать что-то наружу, используется объект module.exports или просто exports.

Когда Node.js встречает require('./myModule'), он делает следующее:

  1. Ищет файл: Сначала смотрит, есть ли файл myModule.js. Если нет — ищет папку myModule с файлом index.js внутри. Если и это не находит — лезет в папку node_modules (там лежат скачанные пакеты).
  2. Читает файл: Открывает файл и читает его содержимое.
  3. Оборачивает в функцию: Node.js не выполняет код файла как есть. Он оборачивает его в специальную функцию-обёртку, которая даёт модулю свои собственные переменные: exports, require, module, __filename, __dirname.
  4. Выполняет: Запускает код внутри этой функции.
  5. Кэширует: Запоминает результат. Если ты ещё раз запросишь этот же модуль, Node.js не будет выполнять его снова, а просто вернёт сохранённый результат. Это очень быстро!
  6. Возвращает module.exports: То, что модуль положил в module.exports, становится результатом вызова require().

ES Modules (ESM) — новый стандарт

Современный JavaScript (начиная с ES2015) придумал свой, более элегантный способ: import и export. Он работает немного иначе:

  • Использует ключевые слова import и export.
  • Работает асинхронно (может загружать модули параллельно, что быстрее).
  • Строже: импорты должны быть на самом верху файла, их нельзя спрятать внутрь условия if.
  • Чтобы использовать ESM в Node.js, нужно либо поставить в package.json строчку "type": "module", либо давать файлам расширение .mjs.
⚠️ Важное отличие: В CommonJS require() — это обычная функция, которую можно вызвать где угодно (даже внутри цикла). В ESM import — это статическое объявление, которое выполняется до того, как начнёт выполняться основной код. Это позволяет анализаторам кода лучше понимать структуру программы и оптимизировать её.

Синтаксис в JavaScript (Node.js): все формы и варианты

Давай разберём все возможные способы экспортировать и импортировать код. Мы рассмотрим обе системы: CommonJS (CJS) и ES Modules (ESM).

CommonJS (CJS) — синтаксис

Экспорт (отдаём наружу):

Способ Код Описание
Экспорт одного значения module.exports = функция Заменяет весь объект экспорта одной функцией или одним значением
Экспорт нескольких значений module.exports = { func1, func2, var1 } Экспортирует объект с несколькими свойствами
Экспорт через exports exports.func1 = ... Добавляет свойства к объекту exports (это ссылка на module.exports)

Импорт (забираем к себе):

Способ Код Описание
Импорт всего модуля const myModule = require('./myModule') Получаем весь объект, который был экспортирован
Деструктуризация const { func1, var1 } = require('./myModule') Забираем только нужные части модуля
Импорт встроенного модуля const fs = require('fs') Без пути — ищет в node_modules или среди встроенных модулей Node.js

ES Modules (ESM) — синтаксис

Экспорт (отдаём наружу):

Способ Код Описание
Именованный экспорт export const name = 'Alex';
export function greet() {}
Экспортируем переменные или функции по имени. Можно много раз
Групповой именованный экспорт export { name, greet }; Собираем всё, что хотим экспортировать, в одном месте (обычно в конце файла)
Экспорт по умолчанию (default) export default function() {}
export default class MyClass {}
Экспортирует одно главное значение из модуля. Может быть только один default на модуль
Ренэйминг (переименование) export { name as userName }; Экспортируем под другим именем

Импорт (забираем к себе):

Способ Код Описание
Импорт именованного экспорта import { name, greet } from './myModule.js' Забираем конкретные именованные экспорты
Импорт всего модуля как объекта import * as myModule from './myModule.js' Все экспорты становятся свойствами объекта myModule
Импорт экспорта по умолчанию import myDefault from './myModule.js' Забираем то, что было экспортировано как default. Имя может быть любым
Смешанный импорт import myDefault, { name, greet } from './myModule.js' Одновременно забираем default и именованные экспорты
Ренэйминг при импорте import { name as userName } from './myModule.js' Импортируем под другим именем, чтобы избежать конфликта имён

Примеры кода — от простого к сложному

Теперь давай посмотрим на живые примеры. Каждый пример — это полноценная рабочая программа. Создай файлы, как указано, и запусти их с помощью Node.js.

Пример 1: Простейший модуль CommonJS

Файл: math.js

// Это простой модуль, который содержит математические функции

// Функция сложения
function add(a, b) {
    return a + b;
}

// Функция умножения
function multiply(a, b) {
    return a * b;
}

// Переменная с числом Пи
const PI = 3.14159;

// Экспортируем всё, что хотим сделать доступным снаружи
// Мы создаём объект с тремя свойствами
module.exports = {
    add: add,        // можно записать просто add, если имя совпадает
    multiply: multiply,
    PI: PI
};

Файл: app.js

// Импортируем наш модуль. Путь './' означает, что файл в той же папке
const math = require('./math.js');

// Теперь мы можем использовать функции из модуля
console.log('Сложение 5 + 3 =', math.add(5, 3));       // Выведет: 8
console.log('Умножение 4 * 7 =', math.multiply(4, 7)); // Выведет: 28
console.log('Число Пи =', math.PI);                     // Выведет: 3.14159
💡 Запуск: Открой терминал, перейди в папку с файлами и напиши node app.js. Программа выполнится и выведет результаты.

Пример 2: Экспорт одной функции (CommonJS)

Файл: greet.js

// Иногда модуль содержит всего одну функцию
// В этом случае удобно экспортировать её напрямую

function greet(name) {
    return `Привет, ${name}! Добро пожаловать в мир модулей!`;
}

// Экспортируем функцию как единственное значение
// Теперь module.exports — это не объект, а функция
module.exports = greet;

Файл: app2.js

// Импортируем модуль. Теперь greet — это функция, а не объект
const greet = require('./greet.js');

// Вызываем функцию напрямую
console.log(greet('Анна'));
// Выведет: Привет, Анна! Добро пожаловать в мир модулей!

Пример 3: ES Modules — современный стиль

Для этого примера нужно либо переименовать файлы в .mjs, либо добавить в package.json строчку "type": "module".

Файл: utils.mjs

// Используем синтаксис ES Modules

// Именованный экспорт: экспортируем функцию с именем
export function capitalize(str) {
    // Делает первую букву заглавной, остальные — маленькими
    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

// Именованный экспорт: экспортируем константу
export const VERSION = '1.0.0';

// Экспорт по умолчанию: главная функция модуля
export default function formatDate(date) {
    // Форматирует дату в виде ДД.ММ.ГГГГ
    const day = String(date.getDate()).padStart(2, '0');
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const year = date.getFullYear();
    return `${day}.${month}.${year}`;
}

Файл: app3.mjs

// Импортируем всё вместе
// formatDate — это экспорт по умолчанию (можем назвать как угодно)
// capitalize и VERSION — именованные экспорты (названия должны совпадать)
import formatDate, { capitalize, VERSION } from './utils.mjs';

console.log('Версия утилит:', VERSION);  // Выведет: Версия утилит: 1.0.0
console.log(capitalize('приВЕТ МИР!'));  // Выведет: Привет мир!
console.log('Сегодня:', formatDate(new Date())); // Выведет: Сегодня: 15.03.2025

Пример 4: Импорт встроенных модулей Node.js

Node.js поставляется с набором встроенных модулей. Их не нужно скачивать — просто импортируй и используй.

Файл: fileReader.js

// Импортируем встроенный модуль 'fs' (file system — файловая система)
const fs = require('fs');
const path = require('path');

// Создаём функцию, которая читает файл и выводит его содержимое
function readMyFile(filename) {
    // path.join создаёт правильный путь к файлу независимо от операционной системы
    const filePath = path.join(__dirname, filename);
    
    // fs.readFileSync читает файл синхронно (программа ждёт, пока файл прочитается)
    // 'utf-8' — это кодировка, чтобы мы получили текст, а не набор байтов
    const content = fs.readFileSync(filePath, 'utf-8');
    
    console.log('Содержимое файла:');
    console.log(content);
}

// Экспортируем функцию
module.exports = readMyFile;

Файл: app4.js

const readMyFile = require('./fileReader.js');

// Попробуем прочитать файл, который мы создадим рядом
readMyFile('test.txt');
💡 Совет: Создай рядом файл test.txt с любым текстом и запусти app4.js. Увидишь, как Node.js читает файлы из твоей файловой системы.

Пример 5: Создание и использование пакета из npm

npm (Node Package Manager) — это огромный магазин готовых пакетов. Давай установим пакет chalk, который умеет раскрашивать текст в консоли.

Шаг 1: В терминале в папке проекта выполни:

npm init -y   # Создаёт файл package.json
npm install chalk  # Устанавливает пакет chalk

Файл: colorApp.js

// Импортируем установленный пакет chalk
// Обрати внимание: путь не начинается с './' — это значит, что пакет ищется в node_modules
const chalk = require('chalk');

// Используем функции пакета для раскрашивания текста
console.log(chalk.blue('Этот текст синий!'));
console.log(chalk.red.bold('Этот текст красный и жирный!'));
console.log(chalk.green.underline('Зелёный подчёркнутый текст'));
console.log(chalk.yellow.bgBlue('Жёлтый текст на синем фоне'));

// Можно комбинировать стили
const name = 'Мария';
console.log(chalk.cyan(`Привет, ${chalk.magenta.bold(name)}!`));

Пример 6: Модуль с классом (ESM)

Файл: user.mjs

// Экспортируем класс по умолчанию
export default class User {
    constructor(name, age) {
        this.name = name;
        this.age = age;
        this.id = Date.now(); // Уникальный ID на основе времени
    }

    // Метод для приветствия
    sayHello() {
        return `Привет! Меня зовут ${this.name}, мне ${this.age} лет.`;
    }

    // Метод для проверки, совершеннолетний ли пользователь
    isAdult() {
        return this.age >= 18;
    }
}

// Именованный экспорт — вспомогательная функция
export function createUser(name, age) {
    return new User(name, age);
}

Файл: app5.mjs

// Импортируем класс и функцию
import User, { createUser } from './user.mjs';

// Создаём пользователя через конструктор класса
const user1 = new User('Иван', 25);
console.log(user1.sayHello());  // Привет! Меня зовут Иван, мне 25 лет.
console.log('Совершеннолетний?', user1.isAdult());  // true

// Создаём пользователя через вспомогательную функцию
const user2 = createUser('Оля', 16);
console.log(user2.sayHello());  // Привет! Меня зовут Оля, мне 16 лет.
console.log('Совершеннолетний?', user2.isAdult());  // false

Пример 7: Динамический импорт (ESM)

Иногда нужно импортировать модуль не в начале файла, а по условию. Для этого есть import() — он возвращает Promise.

Файл: calculator.mjs

export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { 
    if (b === 0) throw new Error('На ноль делить нельзя!');
    return a / b; 
}

Файл: app6.mjs

// Динамический импорт — модуль загружается только когда он нужен
async function runCalculator() {
    // Спрашиваем пользователя, что он хочет сделать
    const operation = process.argv[2] || 'add'; // Берём из аргументов командной строки
    
    // Динамически импортируем модуль
    const calc = await import('./calculator.mjs');
    
    let result;
    switch(operation) {
        case 'add':
            result = calc.add(10, 5);
            break;
        case 'subtract':
            result = calc.subtract(10, 5);
            break;
        case 'multiply':
            result = calc.multiply(10, 5);
            break;
        case 'divide':
            result = calc.divide(10, 5);
            break;
        default:
            console.log('Неизвестная операция');
            return;
    }
    
    console.log(`Результат: ${result}`);
}

runCalculator();
💡 Запуск: Попробуй node app6.mjs multiply — увидишь результат умножения. Если не передать аргумент, по умолчанию выполнится сложение.

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python, тебе будет проще понять модули в JavaScript. Давай сравним их бок о бок.

Аспект JavaScript (Node.js) Python
Система модулей Две системы: CommonJS (CJS) и ES Modules (ESM). Нужно выбирать. Одна система: import и from ... import. Единый стандарт.
Импорт модуля CJS: const fs = require('fs')
ESM: import fs from 'fs'
import os или import os as operating_system
Импорт части модуля CJS: const { readFile } = require('fs')
ESM: import { readFile } from 'fs'
from os import getcwd или from os import getcwd as get_current_dir
Экспорт CJS: module.exports = { ... } или exports.func = ...
ESM: export function ... или export default ...
Любая переменная или функция на верхнем уровне файла автоматически доступна для импорта. Не нужно явно экспортировать.
Экспорт по умолчанию ESM: export default class {} — один на модуль Нет понятия «экспорт по умолчанию». Всё, что не начинается с _, считается публичным.
Поиск модулей Сначала ищет встроенные модули, потом node_modules, потом по пути. Сначала ищет встроенные модули, потом site-packages, потом по пути PYTHONPATH.
Кэширование Модули кэшируются после первого импорта. Повторный импорт возвращает тот же объект. Модули кэшируются в sys.modules. Повторный импорт не выполняет код заново.
Динамический импорт ESM: const module = await import('./path') importlib.import_module('module_name')
Установка пакетов npm install package-name pip install package-name
Файл зависимостей package.json — хранит список зависимостей и их версии requirements.txt — хранит список зависимостей
💡 Главное отличие: В Python всё, что объявлено на верхнем уровне файла, автоматически становится доступным для импорта. В JavaScript нужно явно указывать, что именно ты хочешь сделать доступным снаружи (экспортировать). Это делает код более предсказуемым, но требует чуть больше дисциплины.

Типичные ошибки и подводные камни

Даже опытные программисты иногда спотыкаются на модулях. Вот список самых частых проблем и как их избежать.

Ошибка 1: «Cannot find module»

Ситуация: Ты пытаешься импортировать модуль, а Node.js пишет, что не может его найти.

Причины:

  • Неправильный путь: забыл ./ для локальных файлов. require('myModule') ищет в node_modules, а require('./myModule') — в текущей папке.
  • Опечатка в имени файла: myModul.js вместо myModule.js.
  • Не установлен пакет: забыл выполнить npm install.

Решение: Проверь путь, имя файла и наличие пакета в node_modules.

Ошибка 2: «exports is not defined» в ESM

Ситуация: Ты используешь exports или module.exports в файле с расширением .mjs или с "type": "module".

Причина: В ES Modules нет объекта module и exports. Это особенности CommonJS.

Решение: Используй export и export default вместо module.exports.

Ошибка 3: Круговые зависимости (circular dependencies)

Ситуация: Модуль A импортирует модуль B, а модуль B импортирует модуль A. Node.js может запутаться.

Пример:

// a.js
const b = require('./b.js');
console.log('A загружен');

// b.js
const a = require('./a.js');
console.log('B загружен');

Решение: Избегай круговых зависимостей. Если они неизбежны, используй динамический импорт (import()) внутри функций, а не на верхнем уровне файла.

Ошибка 4: Изменение импортированного модуля

Ситуация: Ты импортировал модуль и случайно изменил его внутренние данные, что повлияло на другие части программы.

Пример:

// config.js
module.exports = { theme: 'light' };

// app.js
const config = require('./config.js');
config.theme = 'dark'; // Меняем глобальное состояние!

// other.js
const config = require('./config.js');
console.log(config.theme); // Выведет 'dark' — неожиданно!

Решение: Не изменяй импортированные объекты. Если нужно изменить конфигурацию, создай новый объект или используй функции-геттеры.

Ошибка 5: Забыл указать расширение файла в ESM

Ситуация: В ES Modules нужно указывать расширение файла при импорте локальных модулей.

Неправильно: import { func } from './myModule'

Правильно: import { func } from './myModule.js'

Причина: В ESM строгие правила разрешения путей. Без расширения Node.js не будет искать