Понимание module.exports и exports в Node.js
В программировании на Node.js модули — это самодостаточные единицы функциональности, которые можно совместно использовать и переиспользовать в разных проектах. Они облегчают нам жизнь как разработчикам, поскольку мы можем использовать их для дополнения наших приложений функциональностью, которую нам не пришлось писать самим. Они также позволяют упорядочить и разделить код, что приводит к созданию приложений, которые легче понять, отладить и поддерживать.
В этой статье я рассмотрю, как работать с модулями в Node.js, уделяя особое внимание их экспорту и использованию.
Различные форматы модулей Node.js
Поскольку в JavaScript изначально не было понятия модулей, со временем появилось множество конкурирующих форматов. Ниже приведён список основных из них, о которых следует знать:
- Формат Asynchronous Module Definition (AMD) применяется в браузерах и использует функцию
define
для определения модулей. - Формат CommonJS (CJS) применяется в Node.js и использует
require
иmodule.exports
для определения зависимостей и модулей. На этом формате построена экосистема npm. - Формат ES-модулей (ESM). Начиная с ES6 (ES2015), JavaScript поддерживает собственный формат модулей. Он использует ключевое слово
export
для экспорта публичного API модуля и ключевое словоimport
для его импорта. - Формат System.register был разработан для поддержки модулей ES6 в рамках ES5.
- Формат Universal Module Definition (UMD) может использоваться как в браузере, так и в Node.js. Он полезен, когда модуль должен быть импортирован несколькими различными загрузчиками модулей.
Обратите внимание, что в этой статье рассматривается исключительно формат CommonJS, являющийся стандартом в Node.js.
Запрос модуля
Node.js поставляется с набором встроенных модулей, которые мы можем использовать в нашем коде без необходимости их установки. Для этого необходимо затребовать модуль с помощью ключевого слова require
и присвоить результат переменной. В дальнейшем она может быть использована для вызова любых методов модуля.
Например, для получения списка содержимого каталога можно использовать модуль файловой системы и его метод readdir
:
const fs = require('fs');
const folderPath = '/home/jim/Desktop/';
fs.readdir(folderPath, (err, files) => {
files.forEach(file => {
console.log(file);
});
});
Обратите внимание, что в CommonJS модули загружаются синхронно и обрабатываются в порядке их появления.
Создание и экспорт модуля
Теперь рассмотрим, как создать собственный модуль и экспортировать его для использования в других частях нашей программы. Начнём с создания файла user.js
и добавления в него следующих элементов:
const getName = () => {
return 'Jim';
};
exports.getName = getName;
Теперь создайте файл index.js
в той же папке и добавьте в него следующее:
const user = require('./user');
console.log(`User: ${user.getName()}`);
Запустите программу с помощью node index.js
, и вы должны увидеть в терминале следующий результат:
User: Jim
Что же здесь произошло? Если вы посмотрите на файл user.js
, то заметите, что мы определяем функцию getName
, затем используем ключевое слово exports
, чтобы сделать её доступной для импорта в другом месте. Затем в файле index.js
мы импортируем эту функцию и выполняем её. Также обратите внимание, что в операторе require
имя модуля имеет префикс ./
, поскольку это локальный файл. Также обратите внимание, что нет необходимости добавлять расширение файла.
Экспорт нескольких методов и значений
Мы можем экспортировать несколько методов и значений одним и тем же способом:
const getName = () => {
return 'Jim';
};
const getLocation = () => {
return 'Munich';
};
const dateOfBirth = '12.01.1982';
exports.getName = getName;
exports.getLocation = getLocation;
exports.dob = dateOfBirth;
А в index.js
:
const user = require('./user');
console.log(
`${user.getName()} lives in ${user.getLocation()} and was born on ${user.dob}.`
);
Приведённый выше код выводит следующее:
Jim lives in Munich and was born on 12.01.1982.
Обратите внимание, что имя, которое мы даём экспортируемой переменной dateOfBirth
, может быть любым (в данном случае dob
). Оно необязательно должно совпадать с именем исходной переменной.
Варианты синтаксиса
Следует также отметить, что экспортировать методы и значения можно по ходу работы, а не только в конце файла.
Например:
exports.getName = () => {
return 'Jim';
};
exports.getLocation = () => {
return 'Munich';
};
exports.dob = '12.01.1982';
А благодаря деструктурирующему присваиванию мы можем выбирать то, что хотим импортировать:
const { getName, dob } = require('./user');
console.log(
`${getName()} was born on ${dob}.`
);
Как и следовало ожидать, этот код выведет:
Jim was born on 12.01.1982.
Экспорт значения по умолчанию
В приведённом выше примере мы экспортируем функции и значения по отдельности. Это удобно для вспомогательных функций, которые могут понадобиться во всем приложении, но когда у вас есть модуль, экспортирующий только одну вещь, чаще всего используется module.exports
:
class User {
constructor(name, age, email) {
this.name = name;
this.age = age;
this.email = email;
}
getUserStats() {
return `
Name: ${this.name}
Age: ${this.age}
Email: ${this.email}
`;
}
}
module.exports = User;
А в index.js
:
const User = require('./user');
const jim = new User('Jim', 37, 'jim@example.com');
console.log(jim.getUserStats());
Приведённый выше код выводит это на экран:
Name: Jim
Age: 37
Email: jim@example.com
В чем разница между module.exports и exports
В своих путешествиях по Сети вы можете встретить следующий синтаксис:
module.exports = {
getName: () => {
return 'Jim';
},
getLocation: () => {
return 'Munich';
},
dob: '12.01.1982',
};
Здесь мы назначаем функции и значения, которые хотим экспортировать, свойству exports
module
— и, конечно, это прекрасно работает:
const { getName, dob } = require('./user');
console.log(
`${getName()} was born on ${dob}.`
);
Приведённый выше код выводит следующее:
Jim was born on 12.01.1982.
Так в чем же разница между module.exports
и exports
? Является ли один из них просто удобным псевдонимом для другого?
Ну, вроде как, но не совсем…
Чтобы проиллюстрировать сказанное, изменим код в файле index.js
для вывода значения модуля:
console.log(module);
Это выводит:
Module {
id: '.',
exports: {},
parent: null,
filename: '/home/jim/Desktop/index.js',
loaded: false,
children: [],
paths:
[ '/home/jim/Desktop/node_modules',
'/home/jim/node_modules',
'/home/node_modules',
'/node_modules' ] }
Как видно, у module
есть свойство exports
. Давайте добавим в него что-нибудь:
// index.js
exports.foo = 'foo';
console.log(module);
Это выводит:
Module {
id: '.',
exports: { foo: 'foo' },
...
Присвоение свойств exports
также добавляет их в module.exports
. Это происходит потому, что (по крайней мере, первоначально) exports
является ссылкой на module.exports
.
Так какой из них использовать
Поскольку и module.exports
, и exports
указывают на один и тот же объект, обычно не имеет значения, какой из них использовать. Например:
exports.foo = 'foo';
module.exports.bar = 'bar';
В результате этого кода экспортируемый объект модуля будет иметь вид { foo: 'foo', bar: 'bar' }
.
Однако здесь есть оговорка. То, чему вы присвоили module.exports
, будет экспортировано из вашего модуля.
Итак, возьмём следующее:
exports.foo = 'foo';
module.exports = () => { console.log('bar'); };
В результате будет экспортирована только анонимная функция. Переменная foo
будет проигнорирована.
Если вы хотите подробнее ознакомиться с различиями, рекомендую статью Node.js: В чём разница между exports и module.exports.
Заключение
Модули стали неотъемлемой частью экосистемы JavaScript, позволяя нам составлять большие программы из более мелких частей. Я надеюсь, что эта статья дала вам хорошее представление о работе с ними в Node.js, а также помогла разобраться в их синтаксисе.