1. Зачем нужны модули и что такое CommonJS
Node.js был задуман как среда для серверного JavaScript, и сразу столкнулся с проблемой: как делить код на части и переиспользовать их? В браузере до появления ES-модулей (import/export) такого механизма не было, и всё работало через глобальные переменные (что, мягко говоря, не очень хорошо).
CommonJS — это спецификация модульной системы, которую реализовал Node.js. Она определяет, как "экспортировать" и "импортировать" код между файлами.
- Каждый файл в Node.js — это отдельный модуль.
- Всё, что объявлено внутри файла, по умолчанию "невидимо" для других файлов.
- Чтобы что-то "отдать наружу" (экспортировать) — используйте module.exports или exports.
- Чтобы что-то "забрать" из другого файла (импортировать) — используйте require().
Аналогия:
Модули — это как комнаты в большом доме: чтобы что-то вынести наружу, вы кладёте это у двери (module.exports). Чтобы занести что-то из другой комнаты, вы используете ключ (require).
2. require(): импортируем модули
Функция require() — это ваш портал в другие файлы (модули). Она синхронно загружает модуль (один раз), исполняет его код (если ещё не исполнялся), и возвращает то, что модуль экспортировал.
Синтаксис:
const модуль = require('путь_или_имя_модуля');
Примеры:
Импорт стандартного модуля:
const fs = require('fs'); // модуль для работы с файлами
const path = require('path'); // модуль для работы с путями
Импорт своего файла:
const myUtils = require('./my-utils.js'); // относительный путь, обязательно './'
Важно:
Если путь начинается с './' или '../', это ваш файл. Если без точки — это модуль из node_modules или стандартный модуль Node.js.
3. module.exports: как экспортировать из модуля
Всё, что вы хотите "отдать наружу" из файла, вы присваиваете специальному объекту module.exports.
Пример 1: экспорт одной функции
// Файл: greet.js
function greet(name) {
return `Привет, ${name}!`;
}
module.exports = greet;
// Файл: app.js
const greet = require('./greet');
console.log(greet('Вася')); // Привет, Вася!
Пример 2: экспорт объекта
// Файл: math.js
function add(a, b) {
return a + b;
}
function sub(a, b) {
return a - b;
}
module.exports = {
add,
sub
};
// Файл: app.js
const math = require('./math');
console.log(math.add(2, 3)); // 5
console.log(math.sub(5, 2)); // 3
Пример 3: экспорт класса
// Файл: User.js
class User {
constructor(name) {
this.name = name;
}
sayHello() {
return `Привет, я ${this.name}`;
}
}
module.exports = User;
// Файл: app.js
const User = require('./User');
const user = new User('Аня');
console.log(user.sayHello()); // Привет, я Аня
4. exports: короткая запись, но с подвохом
Node.js предоставляет ещё одну переменную — exports. Это просто "сокращение" для module.exports, но тут есть нюансы.
// Это корректно
exports.hello = function() {
console.log('Привет!');
};
В этом случае, вы экспортируете объект с методом hello.
ВАЖНО!
Если вы присвоите что-то напрямую в exports, связь с module.exports теряется!
// ОШИБКА! Это НЕ сработает, как вы ожидаете:
exports = function() { /* ... */ };
// Теперь module.exports всё ещё указывает на старый объект, а exports — на новый.
// require() получит пустой объект!
Вывод:
Если экспортируете отдельные свойства/методы, используйте exports.что_то = ....
Если экспортируете функцию/класс/объект целиком — используйте module.exports = ....
5. Как это работает внутри? (немного магии Node.js)
Когда Node.js загружает ваш файл через require, он оборачивает его в функцию примерно так:
(function(exports, require, module, __filename, __dirname) {
// ваш код тут
});
То есть, внутри каждого модуля уже есть exports, require, module, и даже переменные с путём к файлу и папке.
- module.exports — это то, что реально возвращает require().
- exports — это просто сокращение для удобства, но только пока вы не присвоили ему новое значение.
6. Практика: собираем мини-приложение
Давайте шаг за шагом сделаем простое приложение, используя модули.
Шаг 1. math.js
// math.js
function sum(a, b) {
return a + b;
}
function mul(a, b) {
return a * b;
}
module.exports = {
sum,
mul
};
Шаг 2. greet.js
// greet.js
exports.hello = function(name) {
return `Привет, ${name}!`;
};
Шаг 3. app.js
// app.js
const math = require('./math');
const greet = require('./greet');
console.log(greet.hello('Мир')); // Привет, Мир!
console.log('2 + 3 =', math.sum(2, 3)); // 2 + 3 = 5
console.log('3 * 4 =', math.mul(3, 4)); // 3 * 4 = 12
Запуск
node app.js
Результат:
Привет, Мир!
2 + 3 = 5
3 * 4 = 12
Повторное использование и кеширование модулей
Node.js загружает и исполняет модуль только один раз. Если вы несколько раз делаете require('./math') в разных файлах, модуль не будет пересоздаваться — будет возвращаться тот же объект.
Это удобно для хранения состояния, но может привести к неожиданностям, если вы вдруг решите мутировать экспортируемый объект.
8. Типичные ошибки при работе с CommonJS
Ошибка №1: Неправильный путь в require
Если забыть './' при импорте своего файла, Node.js будет искать модуль в node_modules, а не рядом с вашим файлом. Например, require('math') ищет пакет, а не файл math.js в вашей папке. Всегда пишите ./math.
Ошибка №2: Перезапись exports
Если вы напишете exports = function() {...}, модуль экспортирует пустой объект! Всегда используйте либо module.exports = ..., либо добавляйте свойства к exports.
Ошибка №3: Экспорт функции и свойств одновременно
Если вы экспортируете функцию через module.exports = function() {...} и потом добавляете свойства к exports, эти свойства никто не увидит. Всё, что присвоено module.exports, "перекрывает" exports.
Ошибка №4: Циклические зависимости
Если два модуля требуют друг друга (A -> B -> A), вы получите частично инициализированный объект. Node.js не падает, но результат может быть неожиданным.
Ошибка №5: Изменение экспортируемого объекта после require
Если вы мутируете экспортируемый объект после того, как его уже импортировали где-то, изменения увидят все, кто сделал require этого модуля. Это может быть полезно (например, для синглтонов), но часто приводит к багам.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ