Модули Node.js и ключевое слово require в JavaScript

Многие новички, иногда, приходят в конфуз, связанный с тем, что на серверной стороне в Node.js используются включение отдельных скриптов и модулей через ключевое слово require, хотя, если глянуть в стандарт клиентского языка JavaScript, то там он не используется.

Модульная система Node.js

Node.js обрабатывает каждый файл JavaScript как отдельный модуль. Но этот способ подключения не используется на клиентской стороне в целях безопасности

Например, если у вас есть файл, содержащий некоторый код, и этот файл называется xyz.js, то этот файл рассматривается как модуль в Node, и вы можете сказать, что создали модуль с именем xyz.

Node.js абстрагируется от конкретного расширения, потому что есть и другие языки, к примеру TypeScript, который бы потребовал другого именования

Давайте возьмем пример, чтобы понять это лучше.

У вас есть файл с именем circle.js, который состоит из логики для вычисления площади и окружности круга заданного радиуса, как показано ниже:

// constant
const PI = 3.14; 

/**
 * Function to calculate area of a circle with given radius r
 * @param {number} r
 * @returns {number} area of circle 
 */
const calculateArea = r => PI * r * r;

/**
 * Function to calculate circumference of a circle with given radius r
 * @param {number} r
 * @returns {number} circumference of circle 
 */
const calculateCircumference = r => 2 * PI * r;

Вы можете назвать файл circle.js модулем с именем circle.

Вам может быть интересно, почему нужно иметь несколько модулей? Вы могли бы просто написать весь код в одном модуле. Что ж, очень важно написать модульный код. Под модульным я хочу сказать, что ваш код должен быть независимым и должен быть слабо связанным. Представьте, что есть большое приложение, и весь ваш код написан в одном месте, в одном файле. Слишком грязно, верно?

Как работает код, написанный внутри модуля?

Перед выполнением кода, написанного внутри модуля, Node берет весь код и заключает его в оболочку функции. Синтаксис этой функции-обёртки:

Весь код, который вы пишете в модуле, находится в оболочке функции!

Функциональная оболочка для модуля circle будет выглядеть так, как показано ниже:

(function(exports, require, module, __filename, __dirname) {
  // all the code written inside the 'circle' module will breathe inside this function wrapper
  
  // constant
  const PI = 3.14; 

  /**
   * Function to calculate area of a circle with given radius r
   * @param {number} r
   * @returns {number} area of circle 
   */
  const calculateArea = r => PI * r * r;

  /**
   * Function to calculate circumference of a circle with given radius r
   * @param {number} r
   * @returns {number} circumference of circle 
   */
  const calculateCircumference = r => 2 * PI * r;
});

Вы можете видеть, что на корневом уровне есть функция-обертка, охватывающая весь код, написанный внутри модуля circle.

Весь код, написанный внутри модуля, является частным для модуля, если явно не указано, как экспортированное через ключевое слово export.

Это самое значительное преимущество наличия модулей в Node.js. Даже если вы определяете глобальную переменную в модуле, используя ключевые слова var, let или const, переменные ограничиваются локальной областью модуля, а не глобально. Это происходит потому, что каждый модуль имеет свою собственную оболочку функции, а код, написанный внутри одной функции, является локальным для этой функции и недоступен вне этой функции.

Код, написанный внутри модуля, является частным для него!

Представьте, что есть два модуля — A и B. Код, написанный внутри модуля A, заключен в оболочку функции, соответствующую модулю A. Подобное происходит с кодом, написанным внутри модуля B. Потому что код, относящийся к обоим модулям заключен в различные функции, эти функции не смогут получить доступ к коду друг друга. (Помните, что каждая функция в JavaScript имеет свою локальную область видимости?) По этой причине модуль A не может получить доступ к коду, написанному внутри модуля B, и наоборот.

Пять параметров — export, require, module, __filename, __dirname доступны внутри каждого модуля в Node.

Хотя эти параметры являются глобальными для кода в модуле, они являются локальными для модуля (из-за функции-оболочки, как описано выше). Эти параметры предоставляют ценную информацию, связанную с модулем.

Давайте вернемся к модулю circle, который вы рассматривали ранее. В этом модуле определены три конструкции: константа-переменная PI, функция с именем CalculaArea и другая функция с именем CalculateCircumference. Важно помнить, что все эти конструкции по умолчанию являются частными для модуля circle. Это означает, что вы не можете использовать эти конструкции в любом другом модуле, если это не указано явно.

Итак, возникает вопрос: как указать что-то в модуле, который может быть использован другим модулем? Это когда модуль и требуются параметры функции оболочки являются полезными. Давайте обсудим эти два параметра в этой статье.

Параметр module

Параметр module (скорее ключевое слово в модуле в Node) относится к объекту, представляющему текущий модуль. exports— это ключевое слово объекта модуля, соответствующее значение которого является объектом.

Значением по умолчанию для модуля module.exports является {} (пустой объект). Вы можете проверить это, зарегистрировав значение ключевого слова module внутри любого модуля. Давайте проверим, каково значение параметра module внутри модуля circle


// constant
const PI = 3.14; 

/**
 * Function to calculate area of a circle with given radius r
 * @param {number} r
 * @returns {number} area of circle 
 */
const calculateArea = r => PI * r * r;

/**
 * Function to calculate circumference of a circle with given radius r
 * @param {number} r
 * @returns {number} circumference of circle 
 */
const calculateCircumference = r => 2 * PI * r;

// logging the contents of module object
console.log(module);

Обратите внимание, в коде есть строчка вывода в консоль console.log (module). Переде тем, как вывести модуль регистрирует объект module, у которого есть ключ с именем exports, и значение, соответствующее этому ключу, равно {} (пустой объект).

Теперь, что делает объект module.exports? Ну, это используется для определения вещей, которые могут быть экспортированы модулем.

Все, что экспортируется из модуля, может, в свою очередь, быть доступным для других модулей.

Экспортировать что-то довольно легко. Вам просто нужно добавить его в объект module.exports. Есть три способа добавить что-то к объекту module.exports для экспорта. Давайте обсудим эти методы один за другим.

Экспорт свойств из модуля

Для экспорта вы сначала определяете конструкции, а затем используете несколько операторов module.exports, где каждая инструкция используется для экспорта чего-либо из модуля.

Давайте посмотрим на этот способ в действии и посмотрим, как можно экспортировать две функции, определенные в модуле circle.

// constant
const PI = 3.14; 

/**
 * Function to calculate area of a circle with given radius r
 * @param {number} r
 * @returns {number} area of circle 
 */
const calculateArea = r => PI * r * r;

/**
 * Function to calculate circumference of a circle with given radius r
 * @param {number} r
 * @returns {number} circumference of circle 
 */
const calculateCircumference = r => 2 * PI * r;

// exporting stuff by adding to module.exports object
module.exports.calculateArea = calculateArea; // exporting function named calculateArea
module.exports.calculateCircumference = calculateCircumference; // exporting function named calculateCircumference

Как я уже говорил ранее, module — это объект, имеющий ключ с именем exports, и этот ключ (module.exports), в свою очередь, состоит из другого объекта. Теперь, если вы заметили приведенный выше код, все, что вы делаете, это добавляете новые свойства (пары ключ-значение) в объект module.exports.

Первое свойство имеет ключ calculateArea (определен в строке 19), а значение, записанное в правой части оператора присваивания, является функцией, определенной с именем calculateArea (в строке 9).

Второе свойство (определено в строке 20) имеет ключ calculateCircumference, а значением является функция, определенная с именем calculateCircumference (в строке 16).

Таким образом, вы присвоили два свойства (пары ключ-значение) объекту module.exports.

Кроме того, давайте не будем забывать, что вы использовали здесь точечную запись. В качестве альтернативы вы можете использовать нотацию в квадратных скобках для назначения свойств объекту module.exports и добавить функции — возложить на calculateArea и calculateCircumference, указав ключи, следующие за нотацией в скобках. Таким образом, вы можете написать следующие две строки, чтобы добавить свойства к объекту module.exports, используя скобочные обозначения, заменив последние две строки в приведенном выше коде:

...
// exporting stuff by adding to module.exports object using the bracket notation
module.exports['calculateArea'] = calculateArea;
module.exports['calculateCircumference'] = calculateCircumference; 
...

Давайте теперь попробуем записать значение объекта module.exports после добавления свойств. Обратите внимание, что следующий оператор вывода значения объекта module добавляется в конец кода в приведенном ниже файле:

// constant
const PI = 3.14; 

/**
 * Function to calculate area of a circle with given radius r
 * @param {number} r
 * @returns {number} area of circle 
 */
const calculateArea = r => PI * r * r;

/**
 * Function to calculate circumference of a circle with given radius r
 * @param {number} r
 * @returns {number} circumference of circle 
 */
const calculateCircumference = r => 2 * PI * r;

// exporting stuff by adding to module.exports object
module.exports.calculateArea = calculateArea; // exporting function named calculateArea
module.exports.calculateCircumference = calculateCircumference; // exporting function named calculateCircumference

// logging the contents of module.exports object after adding properties to it
console.log(module.exports);

Давайте проверим вывод этого кода и посмотрим, все ли работает нормально. Для этого сохраните свой код и выполните следующую команду в своем терминале:

$ node circle

Это выведет

{ 
   calculateArea: [Function: calculateArea],
   calculateCircumference: [Function: calculateCircumference] 
}

Конструкции — calculateArea и calculateCircumference, добавленные в module.exports, регистрируются в объекте module.exports. Таким образом, вы успешно добавили два свойства в объект module.exports, чтобы можно было экспортировать функции — calculateArea и calculateCircumference в другой модуль.

В этом методе вы сначала определили все конструкции, а затем использовали несколько операторов module.exports, где каждый оператор используется для добавления свойства к объекту module.exports.

Можно было не использовать несколько выводов, а ограничиться одним выводом объектас нужными свойствами:

...
// using object literal notation to export stuff by adding all at once to module.exports object
module.exports = {
  calculateArea : calculateArea,
  calculateCircumference : calculateCircumference
}
...

Еще один способ вывода — это использовать экспортирование на этапе вычислений нужных свойств и сразу же применить

// constant
const PI = 3.14; 

/**
 * Function to calculate area of a circle with given radius r
 * @param {number} r
 * @returns {number} area of circle 
 */
// adding function definition as the value corresponding to the key calculateArea while defining the function
module.exports.calculateArea = r => PI * r * r;

/**
 * Function to calculate circumference of a circle with given radius r
 * @param {number} r
 * @returns {number} circumference of circle 
 */
// adding function definition as the value corresponding to the key calculateCircumference while defining the function
module.exports.calculateCircumference = r => 2 * PI * r;

// no need to write any module.exports statement later

// logging the contents of module.exports object after adding properties to it
console.log(module.exports);

Функция require

Ключевое слово require относится к функции, которая используется для импорта всех конструкций, экспортированных с использованием объекта module.exports из другого модуля.

Если у вас есть модуль x, в который вы экспортируете некоторые конструкции, используя объект module.exports, и вы хотите импортировать эти экспортированные конструкции в модуль y, вам потребуется импортировать модуль x в модуле y с помощью функции require. Значение, возвращаемое функцией require в модуле y, равно объекту module.exports в модуле x.

функция require возвращает объект module.exports

Давайте разберемся в этом на примере, который мы обсуждали ранее. У вас уже есть модуль circle, из которого вы экспортируете функции calculateArea и calculateCircumference. Теперь давайте посмотрим, как вы можете использовать функцию require для импорта экспортированного материала в другой модуль.