Статьи

Как объявить модули в Node.js

Эта статья была первоначально написана Эдвином Далорсо в блоге Informatech CR.

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

Модули, импорт и экспорт

Давайте начнем с самой очевидной и простой вещи. Наверное, все узнают с первого дня работы с Node: каждый файл кода считается модулем. Переменные, свойства, функции, конструкторы, которые мы объявили в нем, являются частными для модуля, и другие модули не могут получить к ним доступ или использовать их, если программист модуля не предоставляет их публично; а именно все, что мы объявляем внутри модуля, по умолчанию инкапсулировано и скрыто от внешнего мира, если явно не указано иное. Чтобы показать что-то, программист имеет доступ к специальному объекту module, который называется , который имеет специальное свойство exports. Все, что вы публикуете в module.exportsобъекте, становится общедоступным для других модулей. Например, в приведенном ниже коде переменнаяpiнедоступен для любых других модулей, но foo.js, в то время как названное свойство barстановится общедоступным для любых других модулей, импортирующих модуль foo.js. Обратите внимание, что это принципиальное отличие от JavaScript в Node.js по сравнению с JavaScript, выполняемым в браузере, где объекты публично отображаются в глобальном объекте (то есть window).

//module foo.js
var pi = 3.14;
module.exports.bar = 'Hello World';

Теперь второй модуль baz.jsможет «импортировать» модуль foo.jsи получить доступ к свойству bar. В Node мы достигаем этого эффекта с помощью глобальной функции с именем require. Примерно так:

//module baz.js
var foo = require('./foo');
console.log(foo.bar); //yields Hello World

Техника 1 — Расширение exportsобъекта с дополнительной функциональностью

Итак, один из способов раскрытия функциональности в модуле состоит в добавлении функций и свойств к module.exportsобъекту. В этом случае Node предоставляет прямой доступ к exportsобъекту, чтобы нам было проще. Например:

//module foo.js
exports.serviceOne = function(){ };
exports.serviceTwo = function(){ };
exports.serviceThree = function(){ };

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

//module bar.js
var foo = require('./foo');
foo.serviceOne();
foo.serviceTwo();
foo.serviceThree();

Техника 2 — Замена exportsобъекта по умолчанию другим объектом

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

//module foo.js
var service = {
   serviceOne: function(){ },
   serviceTwo: function(){ },
   serviceThree = function(){ }
};
 
module.exports = service;

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

Техника 3 — Замена exportsобъекта по умолчанию с помощью функции конструктора

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

//module Foo.js
function Foo(name){
   this.name = name;
}
 
Foo.prototype.serviceOne = function(){ };
Foo.prototype.serviceTwo = function(){ };
Foo.prototype.serviceThree = function(){ };
 
module.exports = Foo;

И пользователь этого модуля может просто сделать что-то вроде этого:

//module bar.js
var Foo = require('./Foo');
var foo = new Foo('Obi-wan');
foo.serviceOne();
foo.serviceTwo();
foo.serviceThree();

Техника 4 — Замена exportsобъекта по умолчанию простой старой функцией

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

//foo.js
var serviceA = {};
serviceA.serviceOne = function(){ };
serviceA.serviceTwo = function(){ };
serviceA.serviceThree = function(){ };
 
var serviceB = {};
serviceB.serviceOne = function(){ };
serviceB.serviceTwo = function(){ };
serviceB.serviceThree = function(){ };
 
module.exports = function(name){
   switch(name){
      case 'A': return serviceA;
      case 'B': return serviceB;
      default: throw new Error('Unknown service name: ' + name);
   }
};

Теперь пользователь, который импортирует этот модуль, получает ссылку на нашу анонимную функцию, объявленную выше, и затем он может просто вызвать функцию, чтобы получить доступ к одному из наших инкапсулированных объектов. Например:

//module bar.js
var foo = require('./foo');
var obj = foo('A');
obj.serviceOne();
obj.serviceTwo();
obj.serviceThree();

Многие программисты обычно вызывают функцию, немедленно возвращаемую requireвместо того, чтобы сначала назначить ее для ссылки. Например:

//module bar.js
var foo = require('./foo')('A');
foo.serviceOne();
foo.serviceTwo();
foo.serviceThree();

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

О модулях и использовании Global State

Интересным аспектом модулей является то, как они оцениваются. Модуль оценивается в первый раз, когда он требуется, а затем он кэшируется. Это означает, что после того, как он был оценен, независимо от того, сколько раз нам это нужно, мы всегда получим один и тот же экспортированный объект.

Это означает, что, хотя Node предоставляет глобальный объект, вероятно, лучше использовать модули для хранения общего ресурса, а не помещать его непосредственно в globalобъект. Например, следующий модуль предоставляет конфигурацию базы данных Mongo.

//module config.js
 
dbConfig = {
  url:'mongodb://foo',
  user: 'anakin',
  password: '*******'
}

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

//foo.js
var dbConfig1 = require('./config');
var dbConfig2 = require('./config');
var assert = require('assert');
assert(dbConfig1==dbConfi2);