Sie sollten wissen, dass Javascript (besser gesagt EcmaScript) keine Funktion zum Lesen und Schreiben von Dateien vorsieht.
In der Tat ist Javascript nur die Sprache, die von vielen Umgebungen verwendet wird (der Browser oder NodeJS sind Beispiele für Umgebungen), die mehr Objekte und Funktionen zum Arbeiten anbieten.
Node war die erste Umgebung, die eine Möglichkeit bot, Code in Modulen zu organisieren, indem sie eine spezielle Funktion namens require()
verwendete. Wie funktioniert das? Versuchen wir, sie von Null an zu implementieren.
Hier ist ein Beispiel für require
bei der Arbeit:
//test.jsmodule.exports = "Hello World";
//main.jsconst test = require("./test.js"); console.log(test)
Lassen Sie uns diese require
Funktion schreiben.
Was soll eine require()-Funktion tun
Eine require
Funktion soll das:
- den Inhalt einer Javascript-Datei in einem String lesen
- den Code auswerten
- die exportierte Funktion/das exportierte Objekt in einem Cache zur späteren Verwendung speichern (Dateien nur einmal lesen)
Haftungsausschluss
Wir werden nicht das gesamte NodeJS in einem einzigen Beitrag neu aufbauen. In der Tat werde ich nicht viele NodeJS Checks und Kicherer implementieren, wir sind nur daran interessiert zu verstehen, wie die Dinge funktionieren.
Wir werden immer noch die echte require
Funktion benötigen, um das fs
Modul zu laden. Ich schummle nicht, es ist nur so, dass dieser Beitrag früher oder später enden muss 🙂
myRequire() Funktion
Hier ist der Code:
//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)}...
Hast du vergessen, die myRequire Variable zu deklarieren?
Nein. In Javascript werden Funktionen, die mit dem Schlüsselwort function
deklariert werden, vor jedem anderen Code ausgewertet (Funktionen werden „gehievt“), so dass sie referenziert werden können, noch bevor sie deklariert werden.
Außerdem können Funktionen Eigenschaften haben (das ist Javascript!), so dass du die cache
-Eigenschaft zur myRequire
-Funktion hinzufügen kannst (Schritt 1).
Schließlich erstellen wir die cache
-Eigenschaft mit Object.create
. Mit dieser Funktion können wir den Objektprototyp angeben, wir haben uns dafür entschieden, keinen Prototyp anzugeben. Warum? Auf diese Weise kommen wir nicht mit anderen Funktionen oder Eigenschaften in Konflikt, die von der Laufzeit deklariert wurden. Hier ist eine Erklärung.
Zurück zu myRequire
. Wenn die Datei, die wir importieren, nicht im Cache ist, lesen wir die Datei von der Festplatte (Schritt 2).
Dann deklarieren wir ein leeres module
Objekt mit nur einer Eigenschaft, exports
(Schritt 3).
Wir fügen dieses leere Modul dem Cache hinzu, wobei wir den Dateinamen als Schlüssel verwenden, und dann passiert die Magie (Schritt 4).
Der Funktionskonstruktor
In JS können wir einen String js-Code auf zwei Arten auswerten. Der erste Weg ist über die eval()
Funktion, die ein wenig gefährlich ist (sie bringt den Bereich durcheinander), so dass von ihrer Verwendung dringend abgeraten wird.
Der zweite Weg, den Code in einem String auszuwerten, ist über den Function
Konstruktor. Dieser Konstruktor nimmt einen String mit den Argumenten und einen String mit dem Code. Auf diese Weise hat alles seinen eigenen Geltungsbereich und bringt andere nicht durcheinander.
Wir erstellen also im Grunde eine neue Funktion mit diesen Variablen (Schritt 5): require
, exports
, und module
. Denken wir einen Moment an das erste Beispiel dieses Beitrags, die Datei test.js
: Sie wird
function(require, exports, module) { module.exports = "Hello World" }
und die zweite Datei, main.js
:
function(require, exports, module) { const test = require("./test.js"); console.log(test) }
Variablen, die in Dateien „global“ zu sein schienen, werden tatsächlich als Funktionsargumente übergeben.
Letzter Schritt: Ausführen der Funktion
Wir haben (Schritt 6) eine wrapper
-Variable erstellt, die eine Funktion enthält, aber die Funktion wird nie ausgeführt. Wir tun dies in der Zeile:
wrapper(myRequire, module.exports, module);
Beachten Sie, dass die zweite Variable (die exports
sein sollte) nur ein Handle zu module.exports
ist; die NodeJS-Schöpfer dachten, dass dies beim Schreiben von weniger Code helfen könnte…
Wenn Node die Funktion ausführt, wird alles, was „exportiert“ wurde (Ihre öffentliche API), mit dem Cache verknüpft.
(Erinnern Sie sich an die Zeile myRequire.cache = module;
? Als sie zum ersten Mal vom Compiler gefunden wurde, zeigte sie auf ein Dummy-Objekt { exports: {} }
; jetzt enthält sie Ihr Modul.)
Hinweis. Da wir
myRequire
an die Wrapper-Funktion übergeben, können wir von nun anrequire
in unseren Testdateien verwenden, aber unser require wird aufgerufen. Füge ein console.log hinzu, wenn du mir nicht vertraust 😉
Schließlich… myRequire
gibt das export
deklarierte Zeug zurück, das du deklariert hast (Schritt 7), und das wir im Cache gespeichert haben, damit wir diesen Code nicht noch einmal auswerten müssen.
Abschließende Überlegungen
Ein Beispiel für diesen Code finden Sie hier, zusammen mit einigen Konsolenprotokollen, die erklären, was vor sich geht.
Die Idee zu diesem Artikel stammt aus der Erklärung dieser Funktion in Kapitel 10 (Module). Das Buch (Eloquent Javascript) ist hervorragend, aber ich hatte den Drang, besser zu verstehen und mit einem Debugger auszuprobieren, was ich mit meinem Verstand allein nicht verstehen konnte.
Das Buch sollte man unbedingt lesen, wenn man Javascript besser verstehen will.
Tags: javascript – nodejs