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