Devi sapere che Javascript (meglio dire EcmaScript) non specifica alcuna funzione per leggere e scrivere file.
In effetti, Javascript è solo il linguaggio usato da molti ambienti (il browser, o NodeJS, sono esempi di ambienti) che offrono più oggetti e funzioni con cui lavorare.
Node è stato il primo ambiente ad offrire un modo per organizzare il codice in moduli utilizzando una funzione speciale chiamata require()
. Come funziona? Proviamo ad implementarla da zero.
Ecco un esempio di require
al lavoro:
//test.jsmodule.exports = "Hello World";
//main.jsconst test = require("./test.js"); console.log(test)
Scriviamo la funzione require
.
Cosa dovrebbe fare una funzione require()
una funzione require
si aspetta che:
- leggere il contenuto di un file javascript in una stringa
- valutare quel codice
- salvare la funzione/oggetto esportato in una cache per un uso successivo (leggere i file solo una volta)
Disclaimer
Non ricostruiremo l’intero NodeJS in un solo post. Infatti, non implementerò molti controlli NodeJS e risatine, ci interessa solo capire come funzionano le cose.
Abbiamo ancora bisogno della vera funzione require
per caricare il modulo fs
. Non sto barando, è solo che questo post deve finire prima o poi 🙂
funzione myRequire()
ecco il codice:
//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)}...
Hai dimenticato di dichiarare la variabile myRequire?
No. In Javascript, le funzioni dichiarate con la parola chiave function
sono valutate prima di qualsiasi altro codice (le funzioni sono “issate”) quindi possono essere referenziate anche prima di essere dichiarate.
Inoltre, le funzioni possono avere proprietà (questo è javascript!) quindi puoi aggiungere la proprietà cache
alla funzione myRequire
(passo 1).
Infine creiamo la proprietà cache
con Object.create
. Con questa funzione possiamo specificare il prototipo dell’oggetto, noi abbiamo scelto di non specificare un prototipo. Perché? In questo modo non ci incasiniamo con altre funzioni o proprietà dichiarate dal runtime. Ecco una spiegazione.
Torniamo a myRequire
. Se il file che stiamo importando non è nella cache, leggiamo il file dal disco (passo 2).
Poi dichiariamo un oggetto vuoto module
con una sola proprietà, exports
(passo 3).
Aggiungiamo questo modulo vuoto alla cache, usando il nome del file come chiave, e poi avviene la magia (passo 4).
Il costruttore di funzioni
In JS possiamo valutare una stringa di codice js in due modi. Il primo modo è tramite la funzione eval()
, che è un po’ pericolosa (incasina lo scope) quindi è altamente sconsigliato usarla.
Il secondo modo per valutare il codice che abbiamo in una stringa è tramite il costruttore Function
. Questo costruttore prende una stringa con gli argomenti e una stringa con il codice. In questo modo tutto ha il proprio ambito e non incasina le cose per gli altri.
Quindi, fondamentalmente stiamo creando una nuova funzione con queste variabili (passo 5): require
, exports
e module
. Pensiamo per un momento al primo esempio di questo post, il file test.js
: diventa
function(require, exports, module) { module.exports = "Hello World" }
e il secondo file, main.js
:
function(require, exports, module) { const test = require("./test.js"); console.log(test) }
Le variabili che sembravano “globali” nei file sono effettivamente passate come argomenti di funzione.
Ultimo passo: eseguire la funzione
Abbiamo creato (passo 6) una variabile wrapper
che contiene una funzione, ma la funzione non viene mai eseguita. Lo facciamo alla riga:
wrapper(myRequire, module.exports, module);
Nota che la seconda variabile (che dovrebbe essere exports
) è solo un handle per module.exports
; i creatori di NodeJS hanno pensato che questo avrebbe potuto aiutare a scrivere meno codice…
Quando Node esegue la funzione, tutto ciò che è stato “esportato” (la tua API pubblica) viene collegato alla cache.
(ricordi la riga myRequire.cache = module;
? Quando è stata trovata per la prima volta dal compilatore puntava a un oggetto fittizio { exports: {} }
; ora contiene il tuo modulo.)
NOTE. Dato che passiamo
myRequire
alla funzione wrapper, d’ora in poi possiamo usarerequire
nei nostri file di test, ma il nostro require viene chiamato. Aggiungete un console.log se non vi fidate di me 😉
Finalmente… myRequire
restituisce la roba export
dichiarata (passo 7), e che abbiamo salvato nella cache in modo da non dover rivalutare nuovamente questo codice.
Considerazioni finali
Un esempio di questo codice può essere trovato qui, insieme ad alcuni log della console che spiegano cosa sta succedendo.
L’idea di questo articolo viene dalla spiegazione di questa funzione al capitolo 10 (Moduli). Il libro (Eloquent Javascript) è eccellente, ma ho avuto la voglia di capire meglio, e provare con un debugger, quello che non riuscivo a capire con la mia sola mente.
Si dovrebbe assolutamente leggere il libro se si vuole capire meglio javascript.
Tags: javascript – nodejs