Trebuie să știți că Javascript (mai bine zis EcmaScript) nu specifică nici o funcție de citire și scriere a fișierelor.
De fapt, Javascript este doar limbajul folosit de multe medii (browserul, sau NodeJS, sunt exemple de medii) care oferă mai multe obiecte și funcții cu care se poate lucra.
Node a fost primul mediu care a oferit o modalitate de organizare a codului în module prin utilizarea unei funcții speciale numită require()
. Cum funcționează aceasta? Să încercăm să o implementăm de la zero.
Iată un exemplu de require
la lucru:
//test.jsmodule.exports = "Hello World";
//main.jsconst test = require("./test.js"); console.log(test)
Să scriem acea funcție require
.
Ce ar trebui să facă o funcție require()
se așteaptă ca o funcție require
să:
- să citească conținutul unui fișier javascript într-un șir de caractere
- să evalueze acel cod
- să salveze funcția/obiectul exportat într-un cache pentru utilizare ulterioară (citește fișierele doar o singură dată)
Disclaimer
Nu vom reconstrui întregul NodeJS într-un singur post. De fapt, nu voi implementa multe verificări NodeJS și chicoteli, ne interesează doar să înțelegem cum funcționează lucrurile.
Vom avea în continuare nevoie de funcția reală require
pentru a încărca modulul fs
. Nu trișez, doar că această postare trebuie să se termine mai devreme sau mai târziu 🙂
funcția myRequire()
iată codul:
//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)}...
Ai uitat să declari variabila myRequire?
Nu. În Javascript, funcțiile declarate cu cuvântul cheie function
sunt evaluate înaintea oricărui alt cod (funcțiile sunt „ridicate”), astfel încât pot fi referite chiar înainte de a fi declarate.
De asemenea, funcțiile pot avea proprietăți (acesta este javascript!), astfel încât puteți adăuga proprietatea cache
la funcția myRequire
(pasul 1).
În cele din urmă vom crea proprietatea cache
cu Object.create
. Cu această funcție putem specifica prototipul obiectului, noi am ales să nu specificăm un prototip. De ce? În acest fel nu ne încurcăm cu alte funcții sau proprietăți declarate de runtime. Iată o explicație.
Să ne întoarcem lamyRequire
. Dacă fișierul pe care îl importăm nu se află în cache, citim fișierul de pe disc (pasul 2).
Apoi declarăm un obiect module
gol cu o singură proprietate, exports
(pasul 3).
Aducem acest modul gol în memoria cache, folosind numele fișierului ca și cheie, iar apoi se întâmplă magia (pasul 4).
Constructorul funcției
În JS putem evalua un șir de cod js în două moduri. Primul mod este prin intermediul funcției eval()
, care este puțin periculoasă (încurcă domeniul de cuprindere), așa că este foarte descurajată utilizarea ei.
Cel de-al doilea mod de a evalua codul pe care îl avem într-un șir de caractere este prin intermediul constructorului Function
. Acest constructor primește un șir cu argumente și un șir cu codul. În acest fel, totul are propriul domeniu de aplicare și nu încurcă lucrurile pentru ceilalți.
Așa că, practic, creăm o nouă funcție cu aceste variabile (pasul 5): require
, exports
, și module
. Să ne gândim pentru o clipă la primul exemplu din această postare, fișierul test.js
: devine
function(require, exports, module) { module.exports = "Hello World" }
și al doilea fișier, main.js
:
function(require, exports, module) { const test = require("./test.js"); console.log(test) }
Variabilele care păreau „globale” în fișiere sunt într-adevăr trecute ca argumente ale funcției.
Ultimul pas: executarea funcției
Am creat (pasul 6) o variabilă wrapper
care deține o funcție, dar funcția nu este niciodată executată. Facem acest lucru la linia:
wrapper(myRequire, module.exports, module);
Rețineți că a doua variabilă (care ar trebui să fie exports
) este doar un handle către module.exports
; creatorii NodeJS s-au gândit că acest lucru ar fi putut ajuta la scrierea mai puțin cod…
Când Node execută funcția, tot ceea ce a fost „exportat” (API-ul dvs. public) este legat de cache.
(Vă amintiți linia myRequire.cache = module;
? Când a fost găsită pentru prima dată de compilator, ea indica un obiect { exports: {} }
fictiv; acum conține modulul dvs.)
NOTA. Din moment ce trecem
myRequire
la funcția wrapper, putem de acum înainte să folosimrequire
în fișierele noastre de test, dar require-ul nostru este apelat. Adăugați un console.log dacă nu aveți încredere în mine 😉
În cele din urmă… myRequire
returnează chestiile export
ed pe care le-ați declarat (pasul 7) și pe care le-am salvat în memoria cache pentru a nu mai fi nevoiți să reevaluăm din nou acest cod.
Considerații finale
Un exemplu al acestui cod poate fi găsit aici, împreună cu câteva jurnale de consolă care explică ce se întâmplă.
Ideea acestui articol vine din explicația acestei funcții de la capitolul 10 (Module). Cartea (Eloquent Javascript) este excelentă, dar am simțit nevoia să înțeleg mai bine și să încerc cu un debugger ceea ce nu puteam înțelege doar cu mintea.
Ar trebui neapărat să citiți cartea dacă vreți să înțelegeți mai bine javascript.
Taguri: javascript – nodejs
.