Wenn wir Nodejs für die tägliche Entwicklung verwenden, importieren wir häufig zwei Arten von Modulen. Das eine ist das Modul, das wir selbst geschrieben haben, oder das文件模块
eines Drittanbieters, das mit npm installiert wird Es ist das integrierte Modul von Node, das uns zur Verfügung gestellt wird, z. B. os
, fs
und andere Module. Diese Module werden核心模块
bezeichnet.
Es ist zu beachten, dass der Unterschied zwischen dem Dateimodul und dem Kernmodul nicht nur darin liegt, ob es von Node integriert wird, sondern auch im Dateispeicherort, der Kompilierung und dem Ausführungsprozess des Moduls. Es gibt offensichtliche Unterschiede zwischen den beiden . Darüber hinaus können Dateimodule in gewöhnliche Dateimodule, benutzerdefinierte Module oder C/C++-Erweiterungsmodule usw. unterteilt werden. Verschiedene Module weisen auch viele Details auf, die sich in der Dateipositionierung, Kompilierung und anderen Prozessen unterscheiden.
Dieser Artikel geht auf diese Probleme ein und erläutert die Konzepte von Dateimodulen und Kernmodulen sowie deren spezifische Prozesse und Details, auf die beim Dateispeicherort, bei der Kompilierung oder bei der Ausführung geachtet werden muss. Ich hoffe, dass er für Sie hilfreich ist.
Beginnen wir mit dem Dateimodul.
Was ist ein Dateimodul?
In Node werden Module, die Modulkennungen verwenden müssen, die mit .、.. 或/
beginnen (d. h. relative oder absolute Pfade verwenden), als Dateimodule behandelt. Darüber hinaus gibt es einen speziellen Modultyp, obwohl er keinen relativen oder absoluten Pfad enthält und kein Kernmodul ist. Wenn der Knoten diesen Modultyp findet, wird er模块路径
um nacheinander nach dem Modul zu suchen. Dieser Modultyp wird als benutzerdefiniertes Modul bezeichnet.
Daher gibt es zwei Arten von Dateimodulen: zum einen normale Dateimodule mit Pfaden und zum anderen benutzerdefinierte Module ohne Pfade.
Das Dateimodul wird zur Laufzeit dynamisch geladen, was einen vollständigen Dateispeicherungs-, Kompilierungs- und Ausführungsprozess erfordert und langsamer als das Kernmodul ist.
Für die Dateipositionierung behandelt Node diese beiden Arten von Dateimodulen unterschiedlich. Schauen wir uns die Suchprozesse für diese beiden Arten von Dateimodulen genauer an.
Da der Pfad, den sie tragen, bei gewöhnlichen Dateimodulen sehr klar ist, dauert die Suche nicht lange, sodass die Sucheffizienz höher ist als bei dem unten vorgestellten benutzerdefinierten Modul. Allerdings gibt es noch zwei Punkte zu beachten.
Erstens wird unter normalen Umständen bei Verwendung von require zum Einführen eines Dateimoduls die Dateierweiterung im Allgemeinen nicht angegeben, z. B.:
const math = require("math");
Da die Erweiterung nicht angegeben ist, kann Node die endgültige Datei nicht ermitteln. In diesem Fall vervollständigt Node die Erweiterungen in der Reihenfolge .js、.json、.node
und probiert sie einzeln aus. Dieser Vorgang wird als文件扩展名分析
bezeichnet.
Es ist auch zu beachten, dass wir in der tatsächlichen Entwicklung neben der Anforderung einer bestimmten Datei normalerweise auch ein Verzeichnis angeben, z. B.:
const axios = require("../network");
In diesem Fall führt Node die Datei zuerst aus Erweiterungsanalyse: Wenn die entsprechende Datei nicht gefunden wird, aber ein Verzeichnis abgerufen wird, behandelt Node das Verzeichnis als Paket.
Insbesondere gibt Node als Suchergebnis die Datei zurück, auf die das main
von package.json
im Verzeichnis verweist. Wenn die Datei, auf die main verweist, falsch ist oder die Datei package.json
überhaupt nicht existiert, verwendet Node index
als Standarddateinamen und verwendet dann .js
und .node
um eine Erweiterungsanalyse durchzuführen und nach der Zieldatei zu suchen Wenn es nicht gefunden wird, wird ein Fehler ausgegeben.
(Da Node natürlich über zwei Arten von Modulsystemen verfügt, CJS und ESM, verwendet Node neben der Suche nach dem Hauptfeld natürlich auch andere Methoden. Da dies außerhalb des Rahmens dieses Artikels liegt, werde ich nicht näher darauf eingehen. )
wurde gerade erwähnt. Wenn Node nach benutzerdefinierten Modulen sucht, wird der Modulpfad verwendet. Wie lautet also der Modulpfad?
Freunde, die mit dem Parsen von Modulen vertraut sind, sollten wissen, dass der Modulpfad ein Array aus Pfaden ist. Der spezifische Wert ist im folgenden Beispiel zu sehen:
// example.js console.log(module.paths);
Druckergebnisse:
Wie Sie sehen können, verfügt das Modul in Node über ein Modulpfad-Array, das in module.paths
gespeichert ist und verwendet wird, um anzugeben, wie Node das vom aktuellen Modul referenzierte benutzerdefinierte Modul findet.
Insbesondere durchläuft Node das Modulpfad-Array, probiert jeden Pfad einzeln aus und findet heraus, ob im Verzeichnis node_modules
ein bestimmtes benutzerdefiniertes Modul vorhanden ist, das dem Pfad entspricht node_modules
-Verzeichnis im Stammverzeichnis, bis das Zielmodul gefunden wird, wird ein Fehler ausgegeben, wenn es nicht gefunden wird.
Es ist ersichtlich, dass das schrittweise rekursive Durchsuchen node_modules
die Strategie von Node zum Auffinden benutzerdefinierter Module ist und der Modulpfad die spezifische Implementierung dieser Strategie darstellt.
Gleichzeitig sind wir auch zu dem Schluss gekommen, dass bei der Suche nach benutzerdefinierten Modulen die entsprechende Suche umso zeitaufwändiger ist, je tiefer das Level liegt. Daher ist die Ladegeschwindigkeit benutzerdefinierter Module im Vergleich zu Kernmodulen und normalen Dateimodulen am langsamsten.
Natürlich wird anhand des Modulpfads nur ein Verzeichnis und keine bestimmte Datei gefunden. Nachdem das Verzeichnis gefunden wurde, sucht Node auch nach dem oben beschriebenen Paketverarbeitungsprozess. Der spezifische Prozess wird nicht erneut beschrieben.
Das Obige ist der Dateipositionierungsprozess und die Details, auf die bei normalen Dateimodulen und benutzerdefinierten Modulen geachtet werden muss. Schauen wir uns als Nächstes an, wie die beiden Modultypen kompiliert und ausgeführt werden.
Wennund die Datei, auf die require zeigt, gefunden wird, hat die Modulkennung normalerweise keine Erweiterung. Gemäß der oben erwähnten Dateierweiterungsanalyse können wir wissen, dass Node die Kompilierung und Ausführung von Dateien unterstützt drei Erweiterungen. :
JavaScript-Datei. Die Datei wird synchron über das fs
Modul gelesen und anschließend kompiliert und ausgeführt. Mit Ausnahme von .node
und .json
Dateien werden andere Dateien als .js
Dateien geladen.
.node
Datei, eine Erweiterungsdatei, die nach dem Schreiben in C/C++ kompiliert und generiert wird. Node lädt die Datei über die Methode process.dlopen()
.
JSON-Datei: Nachdem Sie die Datei synchron über das fs
Modul gelesen haben, verwenden Sie JSON.parse()
um das Ergebnis zu analysieren und zurückzugeben.
Bevor das Dateimodul kompiliert und ausgeführt wird, verpackt Node es wie unten gezeigt mit einem Modul-Wrapper:
(function(exports, require, module, __filename, __dirname) { //Modulcode});
Es ist ersichtlich, dass Node das Modul über den Modul-Wrapper in den Funktionsbereich packt und es von anderen Bereichen isoliert, um Probleme wie Namenskonflikte von Variablen und Kontamination des globalen Bereichs zu vermeiden Zeit, indem Sie die Parameter exports und require übergeben, damit das Modul über die erforderlichen Import- und Exportfunktionen verfügt. Dies ist die Modulimplementierung von Node.
Nachdem wir den Modul-Wrapper verstanden haben, schauen wir uns zunächst den Kompilierungs- und Ausführungsprozess der JSON-Datei an.
Die Kompilierung und Ausführung von JSON-Dateien ist die einfachste. Nach dem synchronen Lesen des Inhalts der JSON-Datei über das fs
-Modul verwendet Node JSON.parse(), um das JavaScript-Objekt zu analysieren, weist es dann dem Exportobjekt des Moduls zu und gibt es schließlich an das Modul zurück, das darauf verweist . Der Prozess ist sehr einfach und grob.
. Nachdem der Modul-Wrapper zum Umschließen der JavaScript-Dateien verwendet wurde, wird der umschlossene Code über runInThisContext()
(ähnlich wie eval) des vm
-Moduls ausgeführt und gibt ein Funktionsobjekt zurück.
Anschließend werden die Export-, Require-, Modul- und andere Parameter des JavaScript-Moduls zur Ausführung an diese Funktion übergeben. Nach der Ausführung wird das Exportattribut des Moduls an den Aufrufer zurückgegeben. Dies ist der Kompilierungs- und Ausführungsprozess der JavaScript-Datei.
Bevor wir die Kompilierung und Ausführung von C/C++-Erweiterungsmodulen erklären, wollen wir zunächst vorstellen, was ein C/C++-Erweiterungsmodul ist.
C/C++-Erweiterungsmodule gehören zu einer Kategorie von Dateimodulen. Der Unterschied zu JavaScript-Modulen besteht darin, dass sie nach dem Laden nicht kompiliert werden müssen Nachdem sie direkt ausgeführt wurden, werden sie etwas schneller geladen als JavaScript-Module. Im Vergleich zu in JS geschriebenen Dateimodulen bieten C/C++-Erweiterungsmodule offensichtliche Leistungsvorteile. Für Funktionen, die nicht vom Node-Kernmodul abgedeckt werden können oder bestimmte Leistungsanforderungen haben, können Benutzer C/C++-Erweiterungsmodule schreiben, um ihre Ziele zu erreichen.
Was ist also eine .node
Datei und was hat sie mit C/C++-Erweiterungsmodulen zu tun?
Tatsächlich wird nach der Kompilierung des geschriebenen C/C++-Erweiterungsmoduls eine .node
Datei generiert. Mit anderen Worten, als Benutzer des Moduls stellen wir nicht direkt den Quellcode des C/C++-Erweiterungsmoduls vor, sondern die kompilierte Binärdatei des C/C++-Erweiterungsmoduls. Daher muss die .node
-Datei nicht kompiliert werden, nachdem Node die .node
Datei gefunden hat, sondern nur die Datei laden und ausführen. Während der Ausführung wird das Exportobjekt des Moduls gefüllt und an den Aufrufer zurückgegeben.
Es ist erwähnenswert, dass die .node
Dateien, die durch das Kompilieren von C/C++-Erweiterungsmodulen generiert werden, auf verschiedenen Plattformen unterschiedliche Formen haben: Unter *nix
-Systemen werden C/C++-Erweiterungsmodule von Compilern wie g++/gcc in Dynamic-Link-Shared-Object-Dateien kompiliert. Die Erweiterung ist .so
; unter Windows
wird sie vom Visual C++-Compiler in eine Dynamic Link Library-Datei kompiliert und die Erweiterung ist .dll
. Aber die Erweiterung, die wir tatsächlich verwenden, .so
.node
. Tatsächlich handelt es sich bei der Erweiterung .node
um eine DLL-Datei unter Windows
und eine .dll
Datei unter *nix
.
Nachdem Node die benötigte .node
Datei gefunden hat, ruft er process.dlopen()
auf, um die Datei zu laden und auszuführen. Da .node
Dateien auf verschiedenen Plattformen unterschiedliche Dateiformen haben, verfügt dlopen()
Methode zur Erzielung einer plattformübergreifenden Implementierung über unterschiedliche Implementierungen auf Windows
und *nix
-Plattformen und wird dann über libuv
Kompatibilitätsschicht gekapselt. Die folgende Abbildung zeigt den Kompilierungs- und Ladevorgang von C/C++-Erweiterungsmodulen auf verschiedenen Plattformen:
Das Kernmodul wird während des Kompilierungsprozesses des Node-Quellcodes in eine binäre ausführbare Datei kompiliert. Wenn der Node-Prozess startet, werden einige Kernmodule direkt in den Speicher geladen. Daher können bei der Einführung dieser Kernmodule die beiden Schritte Dateispeicherung sowie Kompilierung und Ausführung weggelassen werden und werden vor dem Dateimodul im Pfad beurteilt Die Ladegeschwindigkeit ist also am schnellsten.
Das Kernmodul ist tatsächlich in zwei Teile unterteilt, die in C/C++ und JavaScript geschrieben sind. Die C/C++-Dateien werden im src-Verzeichnis des Node-Projekts gespeichert, und die JavaScript-Dateien werden im lib-Verzeichnis gespeichert. Offensichtlich sind die Kompilierungs- und Ausführungsprozesse dieser beiden Modulteile unterschiedlich.
Für die Kompilierung von JavaScript-Kernmodulen verwendet Node während des Kompilierungsprozesses des Node-Quellcodes das mit V8 gelieferte Tool js2c.py, um alle integrierten JavaScript-Codes, einschließlich JavaScript-Kernmodule, zu konvertieren. In C++-Arrays wird JavaScript-Code als Zeichenfolgen im Knoten-Namespace gespeichert. Beim Starten des Node-Prozesses wird der JavaScript-Code direkt in den Speicher geladen.
Wenn ein JavaScript-Kernmodul eingeführt wird, ruft Node process.binding()
auf, um seine Position im Speicher durch Modul-ID-Analyse zu lokalisieren und abzurufen. Nach dem Herausnehmen wird das JavaScript-Kernmodul auch vom Modul-Wrapper umschlossen, dann ausgeführt, das Exportobjekt exportiert und an den Aufrufer zurückgegeben.
im Kernmodul kompiliert und ausgeführt. Einige Module sind alle in C/C++ geschrieben, bei einigen Modulen wird der Kernteil von C/C++ vervollständigt, und andere Teile werden von JavaScript gepackt oder exportiert, um die Leistungsanforderungen zu erfüllen . Module wie buffer
, fs
, os
usw. sind teilweise in C/C++ geschrieben. Dieses Modell, bei dem das C++-Modul den Kern innerhalb des Hauptteils implementiert und das JavaScript-Modul die Kapselung außerhalb des Hauptteils implementiert, ist eine gängige Methode für Node, die Leistung zu verbessern.
Die in reinem C/C++ geschriebenen Teile des Kernmoduls werden als integrierte Module wie node_fs
, node_os
usw. bezeichnet. Sie werden normalerweise nicht direkt von Benutzern aufgerufen, sondern hängen direkt vom JavaScript-Kernmodul ab. Daher gibt es im Einführungsprozess des Node-Kernmoduls eine solche Referenzkette:
Wie lädt das JavaScript-Kernmodul das integrierte Modul?
Erinnern Sie sich an process.binding()
? Node entfernt das JavaScript-Kernmodul aus dem Speicher, indem er diese Methode aufruft. Diese Methode gilt auch für JavaScript-Kernmodule, um das Laden integrierter Module zu unterstützen.
Spezifisch für die Implementierung dieser Methode: Erstellen Sie beim Laden eines integrierten Moduls zunächst ein leeres Exportobjekt, rufen Sie dann get_builtin_module()
auf, um das integrierte Modulobjekt herauszunehmen, und füllen Sie das Exportobjekt durch Ausführen von register_func()
. und geben Sie es schließlich an den Aufrufer zurück, um den Export abzuschließen. Dies ist der Lade- und Ausführungsprozess des integrierten Moduls.
Durch die obige Analyse sieht der allgemeine Prozess für die Einführung einer Referenzkette wie des Kernmoduls am Beispiel des Betriebssystemmoduls wie folgt aus:
Zusammenfassend umfasst der Prozess der Einführung des Betriebssystemmoduls die Einführung des JavaScript-Dateimoduls, das Laden und Ausführen des JavaScript-Kernmoduls sowie das Laden und Ausführen des integrierten Moduls. Der Prozess ist jedoch sehr umständlich und kompliziert Für den Aufrufer des Moduls kann aufgrund der Abschirmung des zugrunde liegenden Moduls bei komplexen Implementierungen und Details das gesamte Modul einfach über require () importiert werden, was sehr einfach ist. freundlich.
In diesem Artikel werden die Grundkonzepte von Dateimodulen und Kernmodulen sowie deren spezifische Prozesse und Details vorgestellt, auf die beim Speicherort, der Kompilierung oder der Ausführung der Datei geachtet werden muss. Konkret:
Dateimodule können entsprechend den unterschiedlichen Dateipositionierungsprozessen in normale Dateimodule und benutzerdefinierte Module unterteilt werden. Gewöhnliche Dateimodule können aufgrund ihrer eindeutigen Pfade direkt gefunden werden. Manchmal ist eine Dateierweiterungsanalyse und eine Verzeichnisanalyse erforderlich. Benutzerdefinierte Module suchen anhand des Modulpfads. Nach erfolgreicher Suche wird der endgültige Dateispeicherort durch eine Verzeichnisanalyse ermittelt .
Dateimodule können je nach Kompilierungs- und Ausführungsprozess in JavaScript-Module und C/C++-Erweiterungsmodule unterteilt werden. Nachdem das JavaScript-Modul vom Modul-Wrapper gepackt wurde, wird es über die runInThisContext
-Methode des vm
-Moduls ausgeführt. Da das C/C++-Erweiterungsmodul bereits nach der Kompilierung eine ausführbare Datei ist, kann es direkt ausgeführt werden und das exportierte Objekt wird zurückgegeben an den Anrufer.
Das Kernmodul ist in ein JavaScript-Kernmodul und ein integriertes Modul unterteilt. Das JavaScript-Kernmodul wird beim Start des Node-Prozesses in den Speicher geladen und kann dann über die Methode process.binding()
ausgeführt werden. Die Kompilierung und Ausführung des integrierten Moduls erfolgt über process.binding()
. Verarbeitung der Funktionen get_builtin_module()
und register_func()
.
Darüber hinaus haben wir auch die Referenzkette für Node gefunden, um Kernmodule einzuführen, nämlich Dateimodul -> JavaScript-Kernmodul -> integriertes Modul. Wir haben auch erfahren , dass das C++-Modul den Kern intern vervollständigt und das JavaScript Das Modul implementiert die Kapselungsmodul -Schreibmethode.