Модули, пакеты и импорт в 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'), он делает следующее:
- Ищет файл: Сначала смотрит, есть ли файл
myModule.js. Если нет — ищет папкуmyModuleс файломindex.jsвнутри. Если и это не находит — лезет в папкуnode_modules(там лежат скачанные пакеты). - Читает файл: Открывает файл и читает его содержимое.
- Оборачивает в функцию: Node.js не выполняет код файла как есть. Он оборачивает его в специальную функцию-обёртку, которая даёт модулю свои собственные переменные:
exports,require,module,__filename,__dirname. - Выполняет: Запускает код внутри этой функции.
- Кэширует: Запоминает результат. Если ты ещё раз запросишь этот же модуль, Node.js не будет выполнять его снова, а просто вернёт сохранённый результат. Это очень быстро!
- Возвращает module.exports: То, что модуль положил в
module.exports, становится результатом вызоваrequire().
ES Modules (ESM) — новый стандарт
Современный JavaScript (начиная с ES2015) придумал свой, более элегантный способ: import и export. Он работает немного иначе:
- Использует ключевые слова
importиexport. - Работает асинхронно (может загружать модули параллельно, что быстрее).
- Строже: импорты должны быть на самом верху файла, их нельзя спрятать внутрь условия
if. - Чтобы использовать ESM в Node.js, нужно либо поставить в
package.jsonстрочку"type": "module", либо давать файлам расширение.mjs.
⚠️ Важное отличие: В CommonJSrequire()— это обычная функция, которую можно вызвать где угодно (даже внутри цикла). В ESMimport— это статическое объявление, которое выполняется до того, как начнёт выполняться основной код. Это позволяет анализаторам кода лучше понимать структуру программы и оптимизировать её.
Синтаксис в 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 не будет искать