Понимание module.exports и exports в Node.js

Источник: «Understanding module.exports and exports in Node.js»
Рассмотрим, как работать с модулями в Node.js, уделяя особое внимание их экспорту и использованию, а также объясняя разницу между module.exports и exports.

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

В этой статье я рассмотрю, как работать с модулями в Node.js, уделяя особое внимание их экспорту и использованию.

Различные форматы модулей Node.js

Поскольку в JavaScript изначально не было понятия модулей, со временем появилось множество конкурирующих форматов. Ниже приведён список основных из них, о которых следует знать:

Обратите внимание, что в этой статье рассматривается исключительно формат 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, а также помогла разобраться в их синтаксисе.

Дополнительные материалы

Предыдущая Статья

Руководство по использованию Websockets в Laravel

Следующая Статья

Как включить TLS 1.3 в Apache и Nginx на Ubuntu и CentOS