Cómo Declarar Módulos de Node.js

Creo que una de las cosas que me causó un poco de confusión cuando comencé a trabajar con Node.js fue entender exactamente cómo se pueden exponer diferentes tipos objetos utilizando módulos. Al principio pareciera intuitivamente sencillo, pero más tarde se da uno cuenta de que puede exponer funciones, constructores, objetos, o simplemente nuevas propiedades dentro del módulo y para cada caso existen diferentes formas de hacerlo. Veamos unos cuantos ejemplos.

Sobre los Módulos, Importar y Exportar

Empecemos por lo más obvio y sencillo y lo que probablemente todo el mundo entiende desde el primer día de trabajo con Node.js. En node un archivo de código se considera un módulo. Las variables, propiedades, funciones o constructores que se declaren en él son privadas al módulo y otros módulos no pueden verlas ni usarlas a no ser que el programador explícitamente las exponga al público; es decir todo lo que declaramos dentro de un módulo está encapsulado y oculto al mundo exterior de forma predeterminada a no ser que el programador exponga algo explícitamente. Para este propósito cada módulo tiene acceso a un objeto especial llamado module, y este objeto module, a su vez, tiene una propiedad llamada exports. Todo lo que el programador coloque en el objeto exports se vuelve público para ese módulo. Por ejemplo, el módulo foo.js expone una propiedad llamada bar que contiene una cadena de caracteres 'Hello World'. Cualquier otra declaración dentro del módulo sería privada y solo accesible al módulo mismo. Por ejemplo, la variable pi declarada abajo es inaccessible para cualquier otro módulo diferente de foo.js, mientras que la propiedad bar es públicamente accessible para otros módulos que importen el módulo foo.js.

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

Así las cosas, un segundo módulo baz.js podría “importar” el módulo foo.js y tener acceso a la propiedad bar. En node, conseguimos tal efecto por medio de usar la función require. Por ejemplo de la siguiente manera:

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

Técnica 1 – Extender exports con Funcionalidad Adicional

Así las cosas, una técnica para exponer la funcionalidad de un módulo consiste en agregar nuevas funciones y propiedades a module.exports. Cuando este es el caso, node.js provee un acceso directo al objeto exports y no es necesario acceder a él a través de la forma un poco más larga module.exports. Por ejemplo:

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

Y como se podrán imaginar los usuarios de este módulo, al importarlo, obtendrán una referencia al mismo objeto exports y entonces podrán consumir toda esta misma funcionalidad.

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

Técnica 2 – Sustituir el Objeto exports Predeterminado por Otro Objeto

Probablemente ustedes podrán intuir, llegados a este punto, que dado que module.exports es el objeto que se expone públicamente para un módulo, entonces perfectamente podríamos sustituir el objeto que nos da node.js de forma predeterminada por nuestro propio objeto. Por ejemplo:

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

module.exports = service;

El código en este último ejemplo se comporta exactamente igual que el código del ejemplo anterior. Es solo que esta vez hemos definido nuestro propio objeto a exportar.

Técnica 3 – Sustituir el Objeto exports Predeterminado por un Constructor

En todos los ejemplos hasta el momento, siempre exponemos una instancia de un objeto, pero nada nos impide sustituir al objeto module.exports por otros tipos de objetos como funciones y constructores. En el ejemplo siguiente exponemos un constructor, que el usuario del módulo puede utilizar más tarde para crear una instancia de este tipo de objeto.

//Foo.js
function Foo(name){
   this.name = name;
}

Foo.prototype.serviceOne = function(){ };
Foo.prototype.serviceTwo = function(){ };
Foo.prototype.serviceThree = function(){ };

module.exports = Foo;

Así las cosas, el usuario que importa este módulo recibe un constructor de Foo que puede usar para crear tantas instancias de este tipo como considere apropiado.

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

Técnica 4 – Sustituir el Objeto exports Predeterminado por una Función

Es fácil imaginar ahora que, si podemos exponer un constructor, entonces también podemos usar una función común y corriente como el objeto que se expone a través de module.exports. El siguiente ejemplo expone una función que nos permit obtener acceso a un objeto privado del módulo, en este caso un objeto de servicio.

//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);
   }
};

Y ahora el usuario del módulo, al importarlo, recibirá una referencia a nuestra función anónima, y al invocarla pues termina obteniendo acceso al objeto encapsulado de servicio. Por ejemplo:

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

Algunos programadores acostumbran invocar la función de inmediato en vez de almacenarla en una referencia e invocarla después. Por ejemplo:

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

Así que como podemos ver, todo lo que exponemos en module.exports es lo que se hace disponible al usuario que importa el módulo directamente y como vimos podemos exponer toda clase de objetos, funciones y constructores usando diferentes técnicas.

Sobre los Módulos y el Uso de Estado Global

Un aspecto significativo de los módulos en node.js es que todo lo que declaramos dentro del módulo es privado a dicho módulo. Esto marca una diferencia significativa de JavaScript en node en comparación con JavaScript en el browser en donde existe un objeto global (p.ej. window) que es el receptáculo final de todo lo que declaramos dentro de un archivo de JavaScript (a menos que lo coloquemos dentro de un alcance de función). En node existe también un objeto global pero nada se coloca en este objeto de manera automática como en el caso de JavaScript en el browser.

Sin embargo, es raro que sea necesario colocar algo en el objeto global en node.js dado que los módulos de Node.js se evalúan una sola vez y después esto se retorna el mismo objeto que module.exports exponga. Por eso es mejor utilizar un módulo para compartir estado global que colocar datos en el objeto global. Por ejemplo, el siguiente código expone un objeto con la configuración de una conexión a una base de datos de Mongo.

//config.js

dbConfig = {
  url:'mongodb://foo',
  user: 'anakin',
  password: '*******'
}

module.exports = dbConfig;

Ahora cada vez que necesitemos tener acceso a nuestra configuración de la base de datos podemos importar el módulo config.js. El código del módulo se evaluará sola la primera vez, así que podemos tener la garantía de que todos los demás módulos que usen nuestro módulo de configuración obtendrán una referencia al mismo objeto que se publico en module.exports la primera vez que este módulo sea requerido.

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