Especificación CommonJS para Sistemas de Módulos en Node.js

Introducción

Las aplicaciones en Node.js se estructuran mediante módulos, siguiendo la norma CommonJS. Cada archivo constituye un módulo independiente, con su propio ámbito. Las variables, funciones y clases definidas dentro de un archivo son privadas y no son accesibles desde otros archivos.

Para compartir variables entre múltiples archivos, deben asignarse como propiedades del objeto global, aunque esta práctica no es recomendada.

// Ejemplo de módulo privado
let contador = 10;
function incrementar(num) {
  return num + contador;
}

La especificación CommonJS establece que dentro de cada módulo existe una variable module que representa al módulo actual. Su propiedad exports (es decir, module.exports) define la interfaz pública. Al cargar un módulo, se obtiene el valor de module.exports.

// Exportando valores desde un módulo
let contador = 10;
function incrementar(num) {
  return num + contador;
}
module.exports.contador = contador;
module.exports.incrementar = incrementar;

El método require se utiliza para cargar módulos.

// Importando un módulo
let moduloEjemplo = require('./ejemplo.js');
console.log(moduloEjemplo.contador); // 10
console.log(moduloEjemplo.incrementar(5)); // 15

Las características principales de los módulos CommonJS incluyen:

  • Todo el código se ejecuta en el ámbito del módulo, sin contaminar el ámbito global.
  • Los módulos se cargan múltiples veces, pero solo se ejecutan en la primera carga; los resultados se almacenan en caché.
  • El orden de carga sigue la secuencia en que aparecen en el código.

El Objeto module

Node.js proporciona internamente una función constructora Module. Todos los módulos son instancias de esta función.

function Modulo(identificador, padre) {
  this.id = identificador;
  this.exports = {};
  this.padre = padre;
  // ...
}

Cada módulo tiene un objeto module con propiedades como module.id (identificador con ruta absoluta), module.filename, module.loaded (booleano), module.parent (módulo que lo invocó), module.children (módulos dependientes) y module.exports (valor exportado).

// Inspeccionando el objeto module
var jquery = require('jquery');
exports.$ = jquery;
console.log(module);

Si un módulo se ejecuta directamente con node archivo.js, module.parent es null; si se carga mediante require, module.parent apunta al módulo invocador. Esto permite determinar si un módulo es el punto de entrada.

if (!module.parent) {
    // Ejecutado directamente
    app.listen(3000, () => console.log('Servidor activo'));
} else {
    // Cargado como dependencia
    module.exports = app;
}

Propiedad module.exports

module.exports define la interfaz pública del módulo. Al cargar el módulo, se obtiene este valor.

var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
setTimeout(() => module.exports.emit('listo'), 1000);

Para escuchar el evento:

var moduloA = require('./a');
moduloA.on('listo', () => console.log('Módulo A listo'));

Variable exports

Node.js proporciona una variable exports que apunta a module.exports para conveniencia.

var exports = module.exports;

Se pueden añadir métodos al objeto exports:

exports.calcularArea = (radio) => Math.PI * radio * radio;
exports.calcularCircunferencia = (radio) => 2 * Math.PI * radio;

Es importante no reasignar exports directamente, ya que se rompe la referencia a module.exports.

// Esto es inválido
exports = function(x) { console.log(x); };

Si la interfaz es un valor único, se debe usar module.exports.

module.exports = (x) => console.log(x);

Compatibilidad con AMD

CommonJS carga módulos de forma síncrona, adecuado para Node.js donde los archivos están localmente. AMD permite carga asíncrona, útil en navegadores. AMD define módulos con define:

define(['libreria/lib'], (lib) => {
  function saludar() {
    lib.registrar('Hola mundo');
  }
  return { saludar };
});

Para compatibilidad con CommonJS en AMD:

define((require, exports, module) => {
  var moduloA = require("moduloA");
  var moduloB = require("moduloB");
  moduloA.procesar();
  moduloB.ejecutar();
  exports.accion = () => {
    moduloA.procesar();
    moduloB.ejecutar();
  };
});

Comendo require

Uso Básico

require carga y ejecuta un archivo JavaScript, devolviendo el objeto exports.

// contenido.js
var oculto = () => console.log("Oculto");
exports.mensaje = "Hola";
exports.saludar = () => console.log(exports.mensaje);

var moduloContenido = require('./contenido.js');
// { mensaje: "Hola", saludar: [Function] }

Para exportar una función única:

module.exports = () => console.log("Ejecutado");
require('./otro.js')();

Reglas de Carga

Por defecto, require añade la extensión .js.

var foo = require('foo'); // Equivale a require('foo.js')

Las rutas se resuelven así:

  • Si comienza con /, se trata de ruta absoluta.
  • Si comienza con ./, es relativa al script actual.
  • Si no, se busca en módulos núcleo o en node_modules (orden: global, local, etc.).
  • Si es una ruta como modulo/ruta/archivo, se localiza el módulo primero.
  • Si no se encuentra, se intenta con extensiones .js, .json, .node.

Carga de Directorios

Para directorios, se puede definir un archivo de entrada en package.json:

// package.json
{ "nombre": "libreria-ejemplo", "principal": "./src/index.js" }

Si falta package.json, se busca index.js o index.node.

Caché de Módulos

Los módulos se cachean tras la primera carga. Para forzar re-ejecución, se puede exportar una función que se llame cada vez.

require('./ejemplo.js');
require('./ejemplo.js').mensaje = "adiós";
require('./ejemplo.js').mensaje // "adiós" (cacheado)

Para borrar caché:

// Eliminar caché de un módulo específico
delete require.cache[rutaModulo];
// Eliminar toda la caché
Object.keys(require.cache).forEach((clave) => delete require.cache[clave]);

Variable NODE_PATH

La variable de entorno NODE_PATH define rutas adicionales para buscar módulos. Se configura en .bashrc o en scripts:

export NODE_PATH="/usr/local/lib/node"

// package.json
{
  "nombre": "ejemplo-nodepath",
  "version": "1.0.0",
  "scripts": { "inicio": "NODE_PATH=lib node index.js" }
}

Carga Circular

En ciclos de carga (A carga B, B carga A), Node.js devuelve una versión incompleta del módulo.

// a.js
exports.valor = 'a1';
console.log('a.js', require('./b.js').valor);
exports.valor = 'a2';

// b.js
exports.valor = 'b1';
console.log('b.js', require('./a.js').valor);
exports.valor = 'b2';

// principal.js
console.log('principal.js', require('./a.js').valor);
console.log('principal.js', require('./b.js').valor);

Salida esperada:

b.js a1
a.js b2
principal.js a2
principal.js b2

require.main

require.main permite detectar si un módulo es ejecutado directamente o como dependencia.

require.main === module // true si se ejecuta con node

Mecanismo de Carga

CommonJS copia los valores exportados. Cambios internos no afectan al valor exportado.

// libreria.js
var contador = 3;
function incrementarContador() { contador++; }
module.exports = { contador, incrementarContador };

// principal.js
var { contador, incrementarContador } = require('./libreria');
console.log(contador); // 3
incrementarContador();
console.log(contador); // 3 (sin cambios)

Internamente, require delega en Module._load, que gestiona la caché, crea instancias, compila el código y devuelve module.exports. El código se envuelve en una función para aislar el ámbito.

(function(exports, require, module, __filename, __dirname) {
  // Código del módulo aquí
});

Etiquetas: CommonJS nodejs JavaScript Módulos require

Publicado el 7-11 06:24