Tudnod kell, hogy a Javascript (jobban mondva az EcmaScript) nem ad meg semmilyen függvényt a fájlok írására és olvasására.
A Javascript valójában csak a nyelv, amelyet számos olyan környezet használ (a böngésző, vagy a NodeJS példák a környezetekre), amelyek több objektumot és funkciót kínálnak a munkához.
A Node volt az első környezet, amely módot kínált a kód modulokba szervezésére egy speciális, require()
nevű függvény segítségével. Hogyan működik? Próbáljuk meg a nulláról megvalósítani.
Itt egy példa a require
működésére:
//test.jsmodule.exports = "Hello World";
//main.jsconst test = require("./test.js"); console.log(test)
Írjuk meg ezt a require
függvényt.
Mit kell tennie egy require() függvénynek
Egy require
függvénytől elvárják, hogy:
- beolvassa egy javascript fájl tartalmát egy sztringben
- kiértékeli ezt a kódot
- a kivitt függvényt/objektumot egy gyorsítótárba menti későbbi használatra (csak egyszer olvassa be a fájlokat)
Disclaimer
Nem fogjuk az egész NodeJS-t egyetlen bejegyzésben újraépíteni. Valójában nem fogok sok NodeJS ellenőrzést és kacagást implementálni, minket csak az érdekel, hogy megértsük, hogyan működnek a dolgok.
Az igazi require
függvényre továbbra is szükségünk lesz a fs
modul betöltéséhez. Nem csalok, csak ennek a posztnak előbb-utóbb véget kell érnie 🙂
myRequire() függvény
Itt a kód:
//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)}...
Elfelejtetted deklarálni a myRequire változót?
Nem. A Javascriptben a function
kulcsszóval deklarált függvények minden más kód előtt kiértékelődnek (a függvényeket “felemelik”), így már a deklarálásuk előtt is lehet rájuk hivatkozni.
A függvényeknek lehetnek tulajdonságaik is (ez a javascript!), így a myRequire
függvényhez (1. lépés) hozzáadhatjuk a cache
tulajdonságot.
Végül létrehozzuk a cache
tulajdonságot a Object.create
-vel. Ezzel a függvénnyel megadhatjuk az objektum prototípusát, mi úgy döntöttünk, hogy nem adunk meg prototípust. Hogy miért? Mert így nem keverjük össze a futásidő által deklarált más függvényeket vagy tulajdonságokat. Íme egy magyarázat.
Menjünk vissza a myRequire
-hez. Ha az importálandó fájl nincs a gyorsítótárban, akkor beolvassuk a fájlt a lemezről (2. lépés).
Ezután deklarálunk egy üres module
objektumot egyetlen tulajdonsággal, a exports
-vel (3. lépés).
Ezt az üres modult hozzáadjuk a gyorsítótárhoz, a fájlnevet használjuk kulcsként, és máris megtörténik a varázslat (4. lépés).
A függvénykonstruktor
A JS-ben kétféleképpen értékelhetünk ki egy string js kódot. Az első mód a eval()
függvényen keresztül, ez egy kicsit veszélyes (összezavarja a hatókörünket), ezért erősen ellenjavallt a használata.
A második módja a stringben lévő kódunk kiértékelésének a Function
konstruktoron keresztül. Ez a konstruktor egy sztringet vesz fel az argumentumokkal és egy sztringet a kóddal. Így mindennek megvan a saját hatósugara, és nem rontja el a többiek dolgát.
Így lényegében egy új függvényt hozunk létre ezekkel a változókkal (5. lépés): require
, exports
és module
. Gondoljunk egy pillanatra a bejegyzés első példájára, a test.js
fájlra:
function(require, exports, module) { module.exports = "Hello World" }
és a második fájlra, main.js
:
function(require, exports, module) { const test = require("./test.js"); console.log(test) }
A fájlokban “globálisnak” tűnő változókat valóban függvényargumentumként adjuk át.
Az utolsó lépés: a függvény végrehajtása
Elkészítettünk (6. lépés) egy wrapper
változót, amely egy függvényt tartalmaz, de a függvény soha nem kerül végrehajtásra. Ezt a következő sorban tesszük:
wrapper(myRequire, module.exports, module);
Megjegyezzük, hogy a második változó (amelynek exports
-nek kellene lennie) csak egy fogantyú a module.exports
-hoz; a NodeJS alkotói úgy gondolták, hogy ez segíthetett volna kevesebb kód írásában…
Amikor a Node végrehajtja a függvényt, minden, amit “exportált” (a nyilvános API-d) a cache-be kerül.
(Emlékszel a myRequire.cache = module;
sorra? Amikor a fordító először találta meg, egy dummy { exports: {} }
objektumra mutatott; most a te modulodat tartalmazza.)
MEGJEGYZÉS. Mivel a
myRequire
-t átadjuk a wrapper függvénynek, innentől kezdve használhatjuk arequire
-t a tesztfájljainkban, de a require-unk meghívást kap. Adj hozzá egy console.log-ot, ha nem bízol bennem 😉
Végre… myRequire
visszaadja a export
deklarált dolgokat (7. lépés), és amit elmentettünk a gyorsítótárba, így nem kell újra kiértékelnünk ezt a kódot.
Végső megfontolások
Egy példa erre a kódra itt található, néhány konzolnaplóval együtt, amelyek elmagyarázzák, mi történik.
A cikk ötlete a 10. fejezetben (Modulok) található magyarázatból származik. A könyv (Eloquent Javascript) kiváló, de késztetést éreztem arra, hogy jobban megértsem, és debuggerrel próbáljam ki azt, amit pusztán az eszemmel nem tudtam megérteni.
A könyvet mindenképpen el kell olvasnod, ha jobban meg akarod érteni a javascriptet.
Tagek: javascript – nodejs