Debes saber que Javascript (mejor decir EcmaScript) no especifica ninguna función para leer y escribir archivos.

De hecho, Javascript no es más que el lenguaje utilizado por muchos entornos (el navegador, o NodeJS, son ejemplos de entornos) que ofrecen más objetos y funciones con los que trabajar.

Node fue el primer entorno que ofreció una forma de organizar el código en módulos mediante una función especial llamada require(). ¿Cómo funciona? Intentemos implementarlo desde cero.

Aquí tienes un ejemplo de require en funcionamiento:

//test.jsmodule.exports = "Hello World";
//main.jsconst test = require("./test.js"); console.log(test) 

Escribamos esa función require.

¿Qué debe hacer una función require()

se espera que una función require:

  • Leer el contenido de un archivo javascript en una cadena
  • Evaluar ese código
  • Guardar la función/objeto exportado en una caché para su uso posterior (sólo leer los archivos una vez)

Descargo de responsabilidad

No vamos a reconstruir todo NodeJS en un solo post. De hecho, no implementaré muchas comprobaciones de NodeJS y risas, sólo nos interesa entender cómo funcionan las cosas.

Seguiremos necesitando la función real require para cargar el módulo fs. No estoy engañando, es que este post tiene que terminar tarde o temprano 🙂

función myRequire()

aquí está el código:

//file setup.jsconst fs = require('fs');myRequire.cache = Object.create(null); //(1)function myRequire(name) { if (!(name in myRequire.cache)) { let code = fs.readFileSync(name, 'utf8'); //(2) let module = {exports: {}}; //(3) myRequire.cache = module; //(4) let wrapper = Function("require, exports, module", code); //(5) wrapper(myRequire, module.exports, module); //(6) } return myRequire.cache.exports; //(7)}...

¿Te has olvidado de declarar la variable myRequire?

No. En Javascript, las funciones declaradas con la palabra clave function se evalúan antes que cualquier otro código (las funciones son «hoisted») por lo que pueden ser referenciadas incluso antes de ser declaradas.

Además, las funciones pueden tener propiedades (¡esto es javascript!) por lo que puedes añadir la propiedad cache a la función myRequire (paso 1).

Finalmente vamos a crear la propiedad cache con Object.create. Con esta función podemos especificar el prototipo del objeto, nosotros hemos optado por no especificar un prototipo. ¿Por qué? Así no nos metemos con otras funciones o propiedades declaradas por el runtime. He aquí una explicación.

Volvamos a myRequire . Si el archivo que estamos importando no está en la caché, leemos el archivo desde el disco (paso 2).

Luego declaramos un objeto module vacío con una sola propiedad, exports (paso 3).

Agregamos este módulo vacío a la caché, usando el nombre del archivo como clave, y entonces ocurre la magia (paso 4).

El constructor de la función

En JS podemos evaluar una cadena de código js de dos maneras. La primera forma es mediante la función eval(), que es un poco peligrosa (desordena el ámbito) por lo que se desaconseja mucho su uso.

La segunda forma de evaluar el código que tenemos en una cadena es mediante el constructor Function. Este constructor toma una cadena con los argumentos y una cadena con el código. De esta manera todo tiene su propio ámbito y no estropea las cosas a los demás.

Entonces, básicamente estamos creando una nueva función con estas variables (paso 5): require, exports, y module. Pensemos por un momento en el primer ejemplo de este post, el fichero test.js: se convierte en

function(require, exports, module) { module.exports = "Hello World" }

y en el segundo fichero, main.js:

function(require, exports, module) { const test = require("./test.js"); console.log(test) }

Las variables que parecían «globales» en los ficheros sí que se pasan como argumentos de la función.

Último paso: ejecutar la función

Hemos creado (paso 6) una variable wrapper que contiene una función, pero la función nunca se ejecuta. Lo hacemos en la línea:

wrapper(myRequire, module.exports, module); 

Nota que la segunda variable (que debería ser exports) es sólo un «handle» de module.exports; los creadores de NodeJS pensaron que esto podría haber ayudado a escribir menos código…

Cuando Node ejecuta la función, todo lo que fue «exportado» (su API pública) se vincula a la caché.

(¿Recuerdas la línea myRequire.cache = module;? Cuando fue encontrada por primera vez por el compilador apuntaba a un objeto { exports: {} } ficticio; ahora contiene tu módulo.)

NOTA. Desde que pasamos myRequire a la función wrapper, podemos a partir de ahora usar require en nuestros archivos de prueba, pero nuestro require es llamado. Añade un console.log si no te fías de mí 😉

Finalmente… myRequire devuelve el exported que declaraste (paso 7), y que guardamos en la caché para no tener que volver a evaluar este código.

Consideraciones finales

Un ejemplo de este código se puede encontrar aquí, junto con algunos logs de consola que explican lo que está pasando.

La idea de este artículo viene de la explicación de esta función en el capítulo 10 (Módulos). El libro (Eloquent Javascript) es excelente, pero tenía ganas de entender mejor, y probar con un depurador, lo que no podía entender sólo con mi mente.

Definitivamente deberías leer el libro si quieres entender mejor javascript.

  • Comparte en Facebook
  • Tweet
  • Comparte en LinkedIn

Etiquetas: javascript – nodejs

Deja una respuesta

Tu dirección de correo electrónico no será publicada.