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 usarrequire
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 export
ed 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.
Etiquetas: javascript – nodejs