ÜberblickDas Aufkommen von Node.js ermöglicht es Front-End-Ingenieuren, clientübergreifend auf dem Server zu arbeiten. Natürlich bringt die Geburt einer neuen Betriebsumgebung auch neue Module, Funktionen oder sogar ideologische Neuerungen mit sich. Dieser Artikel führt die Leser dazu, die Moduldesignideen von Node.js (im Folgenden als Node bezeichnet) zu schätzen und einige Kernquellcodeimplementierungen zu analysieren. CommonJS-SpezifikationNode folgte zunächst der CommonJS-Spezifikation, um sein eigenes Modulsystem zu implementieren, und nahm gleichzeitig einige Anpassungen vor, die von der Spezifikation abwichen. Die CommonJS-Spezifikation ist ein Modulformat, das zur Lösung des Gültigkeitsbereichsproblems von JavaScript definiert wurde und die Ausführung jedes Moduls in seinem eigenen Namespace ermöglicht. Diese Spezifikation betont, dass Module externe Variablen oder Funktionen über module.exports exportieren, die Ausgabe anderer Module über require() in den aktuellen Modulbereich importieren und die folgenden Konventionen befolgen müssen:
Nodes Implementierung der CommonJS-SpezifikationZum Laden von Modulen werden die Funktion module.require innerhalb des Moduls und die globale Funktion require definiert. Im Node-Modulsystem wird jede Datei als separates Modul betrachtet. Wenn ein Modul geladen wird, wird es als Instanz des Modulobjekts initialisiert. Die grundlegende Implementierung und die Eigenschaften des Modulobjekts sind wie folgt: Funktion Modul(id = "", übergeordnetes Element) { // Modul-ID, normalerweise der absolute Pfad des Moduls this.id = id; dieser.Pfad = Pfad.Verzeichnisname(id); dies.exports = {}; //Aktueller Modulaufrufer this.parent = parent; updateChildren(übergeordnet, dies, falsch); dieser.Dateiname = null; // Ist das Modul geladen? this.loaded = false; //Vom aktuellen Modul referenziertes Modul this.children = []; } Jedes Modul stellt sein Exportattribut als Benutzeroberfläche bereit. Modulexporte und -importeIn Node können Sie das Objekt module.exports verwenden, um eine Variable oder Funktion als Ganzes zu exportieren, oder Sie können die zu exportierende Variable oder Funktion in die Attribute des Exportobjekts einbinden. Der Code lautet wie folgt: // 1. Exporte verwenden: Ich verwende es normalerweise, um Tool-Bibliotheksfunktionen oder Konstanten zu exportieren: exports.name = 'xiaoxiang'; exports.add = (a, b) => a + b; // 2. Verwenden Sie module.exports: Exportieren Sie ein ganzes Objekt oder eine einzelne Funktion ... modul.exporte = { hinzufügen, Minus } Auf das Modul wird über die globale require-Funktion verwiesen. Der Modulname, der relative Pfad oder der absolute Pfad können übergeben werden. Wenn das Moduldateisuffix js/json/node lautet, kann das Suffix weggelassen werden, wie im folgenden Code gezeigt: // Referenzmodul const { add, minus } = require('./module'); const a = erfordern('/usr/app/modul'); const http = erfordern('http'); Notiz: Die Exportvariable ist im Dateiebenenbereich des Moduls verfügbar und wird module.exports zugewiesen, bevor das Modul ausgeführt wird. exporte.name = "Test"; konsole.log(modul.exports.name); // Test modul.export.name = "Test"; console.log(exports.name); // Test Wenn „exports“ ein neuer Wert zugewiesen wird, ist es nicht mehr an module.exports gebunden und umgekehrt: Exporte = { Name: 'Test' }; console.log(module.exports.name, exports.name); // undefiniert, Test ]Wenn die Eigenschaft module.exports vollständig durch ein neues Objekt ersetzt wird, ist es normalerweise notwendig, auch die Exporte neu zuzuweisen: modul.exports = Exporte = { Name: 'test' }; console.log(Modul.Exports.Name, Exporte.Name) // Test, Test Modulsystem realisiert AnalysemodulpositionierungNachfolgend sehen Sie die Codeimplementierung der require-Funktion: // Eintragsfunktion erforderlich Module.prototype.require = function(id) { //... erfordernTiefe++; versuchen { return Module._load(id, this, /* isMain */ false); // Modul laden } finally { Tiefe erfordern--; } }; Der obige Code empfängt den angegebenen Modulpfad, wobei requireDepth verwendet wird, um die Tiefe des Modulladens aufzuzeichnen. Die Modulklassenmethode _load implementiert die Hauptlogik der Node-Lademodule. Lassen Sie uns die Quellcodeimplementierung der Funktion Module._load analysieren. Der Einfachheit halber habe ich dem Text Kommentare hinzugefügt. Module._load = Funktion(Anfrage, übergeordnetes Element, isMain) { // Schritt 1: Den vollständigen Pfad des Moduls auflösen const filename = Module._resolveFilename(request, parent, isMain); // Schritt 2: Laden Sie das Modul, das in drei Fälle unterteilt ist. // Fall 1: Wenn ein zwischengespeichertes Modul vorhanden ist, geben Sie direkt die Exporteigenschaft des Moduls zurück const cachedModule = Module._cache[Dateiname]; wenn (cachedModule !== undefiniert) gibt cachedModule.exports zurück; // Fall 2: Integrierte Module laden const mod = loadNativeModule(filename, request); wenn (mod && mod.kannVonBenutzernErforderlichBenötigt Werden) returniere mod.exports; // Fall 3: Modul erstellen laden const module = new Module(Dateiname, übergeordnetes Element); // Nach dem Laden die Modulinstanz zwischenspeichern Module._cache[filename] = module; // Schritt 3: Laden Sie die Moduldatei module.load(filename); // Schritt 4: Geben Sie das Exportobjekt zurück return module.exports; }; LadestrategieDer obige Code enthält viele Informationen. Wir betrachten hauptsächlich die folgenden Probleme: Was ist die Caching-Strategie des Moduls? Bei der Analyse des obigen Codes können wir erkennen, dass die Funktion _load unterschiedliche Ladestrategien für drei Situationen bietet, nämlich:
Wie löst Module._resolveFilename(request, parent, isMain) den Dateinamen auf? Schauen wir uns die Klassenmethode an, die wie folgt definiert ist: Module._resolveFilename = Funktion (Anfrage, übergeordnetes Element, isMain, Optionen) { wenn (NativeModule.canBeRequiredByUsers(Anfrage)) { // Priorisieren Sie das Laden integrierter Module. Return Request; } lass Pfade; // Von der Knotenfunktion require.resolve verwendete Optionen, options.paths wird verwendet, um den Suchpfad anzugeben, wenn (typeof options === "object" && options !== null) { wenn (ArrayIsArray(Optionen.Pfade)) { const istRelative = Anfrage.startsWith("./") || Anfrage.startsWith("../") || (isWindows && request.startsWith(".\\")) || Anfrage.startsWith("..\\"); wenn (istrelativ) { Pfade = Optionen.Pfade; } anders { const fakeParent = neues Modul("", null); Pfade = []; für (let i = 0; i < options.paths.length; i++) { const Pfad = Optionen.Pfade[i]; fakeParent.paths = Module._nodeModulePaths(Pfad); const lookupPaths = Module._resolveLookupPaths(Anfrage, fakeParent); für (let j = 0; j < lookupPaths.length; j++) { wenn (!Pfade.includes(lookupPaths[j])) Pfade.push(lookupPaths[j]); } } } } sonst wenn (Optionen.Pfade === undefiniert) { Pfade = Module._resolveLookupPaths(Anfrage, übergeordnetes Element); } anders { //... } } anders { // Suchen Sie den Modul-Existenzpfad. Pfade = Module._resolveLookupPaths (Anfrage, übergeordnetes Element); } // Suchen Sie den Modulpfad basierend auf dem angegebenen Modul und dem Traversierungsadressarray sowie danach, ob es sich um ein Einstiegsmodul handelt. const filename = Module._findPath(request, paths, isMain); if (!Dateiname) { const erfordernStack = []; für (let cursor = übergeordnetes Element; cursor; cursor = cursor.übergeordnetes Element) { requireStack.push(cursor.dateiname || cursor.id); } // Modul nicht gefunden, Ausnahme auslösen (ist das ein bekannter Fehler?) let message = `Modul ‚${request}‘ kann nicht gefunden werden`; wenn (requireStack.length > 0) { Nachricht = Nachricht + "\nStapel erforderlich:\n- " + requireStack.join("\n- "); } const err = neuer Fehler(Nachricht); err.code = "MODUL NICHT GEFUNDEN"; err.requireStack = erfordernStack; Fehler machen; } //Gib abschließend den vollständigen Pfad einschließlich des Dateinamens zurück return filename; }; Das auffälligste Merkmal des obigen Codes ist die Verwendung der Methoden _resolveLookupPaths und _findPath. _resolveLookupPaths: Gibt ein Array von Durchlaufbereichen zurück, die von _findPath verwendet werden, indem ein Modulname und ein Modulaufrufer akzeptiert werden. // Adressarray-Methode für Moduldateiadressierung Module._resolveLookupPaths = function(request, parent) { wenn (NativeModule.canBeRequiredByUsers(Anfrage)) { debug("suche nach %j in []", Anfrage); gibt null zurück; } // Wenn es kein relativer Pfad ist, wenn ( Anfrage.charAt(0) !== "." || (Anfragelänge > 1 && request.charAt(1) !== "." && request.charAt(1) !== "/" && (!isWindows || request.charAt(1) !== "\\")) ) { /** * Überprüfen Sie den Ordner node_modules * modulePaths ist das Benutzerverzeichnis, die Umgebungsvariable node_path gibt das Verzeichnis an, das globale Knoteninstallationsverzeichnis */ let Pfade = Modulpfade; wenn (übergeordnet != null && übergeordnete.Pfade && übergeordnete.Pfade.Länge) { // Der Modulpfad des übergeordneten Moduls sollte auch zum Modulpfad des untergeordneten Moduls hinzugefügt werden. Anschließend muss die Suche zurückverfolgt werden, um „paths = parent.paths.concat(paths);“ zu finden. } Gib Pfade.Länge > 0 zurück? Pfade: null; } // Bei Verwendung der Repl-Interaktion nacheinander nach ./ ./node_modules und modulePaths suchen wenn (!übergeordnet || !übergeordnet.id || !übergeordnet.dateiname) { const mainPaths = ["."].concat(Module._nodeModulePaths("."), modulePaths); Hauptpfade zurückgeben; } // Wenn es sich um eine relative Pfadeinführung handelt, fügen Sie den übergeordneten Ordnerpfad zum Suchpfad hinzu const parentDir = [path.dirname(parent.filename)]; gibt übergeordnetes Verzeichnis zurück; }; _findPath: Suchen Sie den entsprechenden Dateinamen und geben Sie ihn basierend auf dem Zielmodul und dem von der obigen Funktion gefundenen Bereich zurück. // Finde den tatsächlichen Pfad des Moduls basierend auf dem angegebenen Modul und dem Traversal-Adress-Array sowie basierend darauf, ob es sich um ein Modul der obersten Ebene handelt. Module._findPath = function(request, paths, isMain) { const absoluteRequest = Pfad.isAbsolute(Anfrage); if (absoluteAnforderung) { // Absoluter Pfad, lokalisieren Sie die spezifischen Modulpfade direkt = [""]; } sonst wenn (!Pfade || Pfade.Länge === 0) { gibt false zurück; } Konstant CacheKey = Anfrage + "\x00" + (Pfade.Länge === 1 ? Pfade[0] : Pfade.Join("\x00")); // Cache-Pfad const entry = Module._pathCache[cacheKey]; wenn (Eintrag) Eintrag zurückgeben; lass Erweiterungen; let trailingSlash = Anfragelänge > 0 && request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH; // '/' wenn (!trailingSlash) { trailingSlash = /(?:^|\/)\.?\.$/.test(Anfrage); } // Für jeden Pfad für (lass i = 0; i < Pfade.Länge; i++) { const curPath = Pfade[i]; wenn (curPath und stat(curPath) < 1) fortfahren; const basePath = resolveExports(curPath, request, absoluteRequest); let Dateiname; const rc = stat(Basispfad); wenn (!trailingSlash) { if (rc === 0) { // stat status gibt 0 zurück, dann ist es eine Datei // Datei. wenn (!istHaupt) { if (Symlinks erhalten) { // Weisen Sie den Modullader an, beim Auflösen und Zwischenspeichern von Modulen symbolische Links beizubehalten. Dateiname = Pfad.Auflösen(Basispfad); } anders { // Symbolische Links nicht beibehalten. Dateiname = toRealPath(basePath); } } sonst wenn (SymlinksMain beibehalten) { Dateiname = Pfad.Auflösen(Basispfad); } anders { Dateiname = toRealPath(Basispfad); } } if (!Dateiname) { wenn (exts === undefiniert) exts = ObjectKeys(Module._extensions); // Analysieren Sie das Suffix filename = tryExtensions(basePath, exts, isMain); } } wenn (!Dateiname && rc === 1) { /** * Wenn stat 1 zurückgibt und der Dateiname nicht existiert, wird es als Ordner betrachtet. * Wenn die Dateierweiterung nicht existiert, versuchen Sie, die durch den Haupteintrag in package.json angegebene Datei unter dem Verzeichnis zu laden. * Wenn sie nicht existiert, versuchen Sie es mit der Datei index[.js, .node, .json] */ wenn (exts === undefiniert) exts = ObjectKeys(Modul._extensions); Dateiname = tryPackage(Basispfad, exts, isMain, Anfrage); } if (Dateiname) { // Wenn die Datei existiert, füge den Dateinamen zum Cache hinzu Module._pathCache[cacheKey] = Dateiname; Dateinamen zurückgeben; } } const selfFilename = trySelf(Pfade, Erweiterungen, isMain, nachfolgender Schrägstrich, Anfrage); if (Eigendateiname) { // Pfad-Cache festlegen Module._pathCache[cacheKey] = selfFilename; gib selfFilename zurück; } gibt false zurück; }; Modul wird geladenStandardmodulverarbeitung Nach dem Lesen des obigen Codes stellen wir fest, dass die Logik der tryPackage-Funktion ausgeführt wird, wenn das Modul ein Ordner ist. Nachfolgend finden Sie eine kurze Analyse der spezifischen Implementierung. // Versuchen Sie, eine Standardmodulfunktion zu laden tryPackage(requestPath, exts, isMain, originalPath) { const pkg = readPackageMain(requestPath); wenn (!pkg) { // Wenn kein package.json vorhanden ist, wird „Index“ als Standardeintragsdatei verwendet. return tryExtensions(path.resolve(requestPath, „index“), exts, isMain); } const Dateiname = Pfad.Auflösen(Anforderungspfad, Paket); sei tatsächlich = tryFile(Dateiname, isMain) || tryExtensions(Dateiname, exts, isMain) || tryExtensions(Pfad.resolve(Dateiname, "Index"), exts, isMain); //... tatsächliche Rückgabe; } // Lies das Hauptfeld in package.json function readPackageMain(requestPath) { const pkg = readPackage(Anforderungspfad); Paket zurückgeben? pkg.main: nicht definiert; } Die Funktion readPackage ist für das Lesen und Analysieren des Inhalts der Datei package.json verantwortlich, wie unten beschrieben: Funktion readPackage(requestPath) { const jsonPath = Pfad.resolve(requestPath, "package.json"); const vorhanden = PaketJsonCache.get(jsonPath); wenn (vorhanden !== undefiniert) returniere vorhanden; // Rufen Sie die Ausführungslogik von libuv uv_fs_open auf, lesen Sie die Datei package.json und cachen Sie const json = internalModuleReadJSON(path.toNamespacedPath(jsonPath)); wenn (json === undefiniert) { // Dann die Datei zwischenspeichern packageJsonCache.set(jsonPath, false); gibt false zurück; } //... versuchen { Konstante analysiert = JSONParse(json); const gefiltert = { Name: analysierter.Name, Haupt: analysiert.main, Exporte: analysiert.Exporte, Typ: analysierter Typ }; packageJsonCache.set(jsonPath, gefiltert); gefiltert zurückgeben; } fangen (e) { //... } } Die beiden obigen Codeausschnitte erklären perfekt die Rolle der Datei package.json, den Konfigurationseintrag des Moduls (das Hauptfeld in package.json) und warum die Standarddatei des Moduls index ist. Der spezifische Prozess ist in der folgenden Abbildung dargestellt: ModuldateiverarbeitungWie kann das entsprechende Modul geladen und analysiert werden, nachdem es gefunden wurde? Nachfolgend finden Sie eine spezifische Codeanalyse: Module.prototype.load = Funktion(Dateiname) { // Sicherstellen, dass das Modul nicht geladen wurde assert(!this.loaded); this.filename = Dateiname; // Finde die node_modules des aktuellen Ordners this.paths = Module._nodeModulePaths(Pfad.Verzeichnisname(Dateiname)); const extension = findLongestRegisteredExtension(Dateiname); //... // Führen Sie eine bestimmte Funktion zum Parsen von Dateierweiterungen aus, z. B. js / json / node Module._extensions[Erweiterung](dieser, Dateiname); // Zeigt an, dass das Modul erfolgreich geladen wurde. this.loaded = true; // ... Unterstützung für ESM-Modul weglassen }; SuffixverarbeitungEs ist ersichtlich, dass Node.js für verschiedene Dateisuffixe unterschiedlich geladen wird. Das Folgende ist eine einfache Analyse von .js, .json und .node. Das Lesen von JS-Dateien mit der Endung .js wird hauptsächlich über die in Node integrierte API fs.readFileSync implementiert. Module._extensions[".js"] = Funktion(Modul, Dateiname) { // Dateiinhalt lesen const content = fs.readFileSync(filename, "utf8"); // Code kompilieren und ausführen module._compile(content, filename); }; Die Verarbeitungslogik von JSON-Dateien mit JSON-Suffix ist relativ einfach. Führen Sie nach dem Lesen des Dateiinhalts JSONParse aus, um das Ergebnis zu erhalten. Module._extensions[".json"] = Funktion(Modul, Dateiname) { // Laden Sie die Datei direkt im UTF-8-Format const content = fs.readFileSync(filename, "utf8"); //... versuchen { // Dateiinhalte im JSON-Objektformat exportieren module.exports = JSONParse(stripBOM(content)); } fangen (Fehler) { //... } }; Die .node-Datei mit der Endung .node ist ein natives Modul, das von C/C++ implementiert wird und von der Funktion process.dlopen gelesen wird. Die Funktion process.dlopen ruft tatsächlich die Funktion DLOpen im C++-Code auf, und DLOpen ruft uv_dlopen auf, das die .node-Datei lädt, ähnlich wie das Betriebssystem Systembibliotheksdateien lädt. Module._extensions[".node"] = Funktion(Modul, Dateiname) { //... returniere process.dlopen(Modul, Pfad.zuNamespacedPath(Dateiname)); }; Aus den drei Quellcodes oben können wir erkennen und verstehen, dass am Ende nur das JS-Suffix die Instanzmethode _compile ausführt. Lassen Sie uns einige experimentelle Funktionen und die Debuglogik entfernen, um diesen Code kurz zu analysieren. Kompilieren und AusführenNachdem das Modul geladen wurde, verwendet Node die von der V8-Engine bereitgestellte Methode, um die Sandbox zu erstellen und auszuführen und den Funktionscode auszuführen. Der Code lautet wie folgt: Module.prototype._compile = Funktion(Inhalt, Dateiname) { lass ModulURL; lass Weiterleitungen zu; // Füge die öffentlichen Variablen __dirname / __filename / module / exports / require in das Modul ein und kompiliere die Funktion const compiledWrapper = wrapSafe(filename, content, this); const dirname = Pfad.dirname(Dateiname); const require = makeRequireFunction(diese, Weiterleitungen); lass resultieren; const exports = diese.exports; const thisValue = Exporte; const Modul = dies; wenn (requireDepth === 0) statCache = neue Map(); //... // Führe die Funktion im Modul aus result = compiledWrapper.call( dieserWert, Exporte, erfordern, Modul, Dateiname, Verzeichnisname ); hasLoadedAnyUserCJSModule = true; wenn (requireDepth === 0) statCache = null; Ergebnis zurückgeben; }; //Kernlogik der Variableneinfügung function wrapSafe(filename, content, cjsModuleInstance) { wenn (gepatcht) { const wrapper = Module.wrap(Inhalt); // VM-Sandbox wird ausgeführt und gibt das Ausführungsergebnis direkt zurück, env->SetProtoMethod(script_tmpl, "runInThisContext", RunInThisContext); return vm.runInThisContext(Wrapper, { Dateiname, Zeilenversatz: 0, Anzeigefehler: true, // importModuleDynamically dynamisch laden: async specifier => { const loader = asyncESM.ESMLoader; returniere loader.import(Spezifizierer, normalizeReferrerURL(Dateiname)); } }); } kompilieren lassen; versuchen { kompiliert = Kompilierfunktion( Inhalt, Dateiname, 0, 0, undefiniert, FALSCH, undefiniert, [], ["Exporte", "erfordern", "Modul", "__Dateiname", "__Verzeichnisname"] ); } fangen (Fehler) { //... } const { callbackMap } = internalBinding("module_wrap"); callbackMap.set(kompiliert.cacheKey, { importModuleDynamically: asynchroner Spezifizierer => { const loader = asyncESM.ESMLoader; returniere loader.import(Spezifizierer, normalizeReferrerURL(Dateiname)); } }); kompilierte Funktion zurückgeben; } Im obigen Code können wir sehen, dass die Funktion wrapwrapSafe in der Funktion _compile aufgerufen wird, die Injektion der öffentlichen Variablen __dirname / __filename / module / exports / require durchgeführt wird und die C++-Methode runInThisContext (in der Datei src/node_contextify.cc) aufgerufen wird, um eine Sandbox-Umgebung für die Ausführung des Modulcodes zu erstellen, und das compiledWrapper-Objekt zurückgegeben wird. Schließlich wird das Modul durch die Methode compiledWrapper.call ausgeführt. Oben finden Sie den detaillierten Inhalt der Quellcodeanalyse des NodeJS-Modulsystems. Weitere Informationen zur Quellcodeanalyse des NodeJS-Modulsystems finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM! Das könnte Sie auch interessieren:
|
<<: Eine kurze Diskussion über den MySQL B-Tree-Index und die Indexoptimierungszusammenfassung
>>: Detaillierte Verwendung des Kubernetes-Objektvolumens
Wie der Titel schon sagt, kann andernfalls bei ein...
Inhaltsverzeichnis Abstraktion und Wiederverwendu...
Die Ausführungseffizienz der MySQL-Datenbank hat ...
Zunächst stellen wir vor, wie (1) MySQL 5.7 hat e...
Ich habe das vorliegende Projekt endlich abgeschl...
Code kopieren Der Code lautet wie folgt: <styl...
Inhaltsverzeichnis Herunterladen und Installieren...
Überblick Der Lastenausgleich von Nginx bietet Up...
1. Das Startmenü besteht darin, den Cursor in die...
Klassische Farbkombinationen vermitteln Kraft und ...
<meta http-equiv="x-ua-kompatibel" co...
【Vorwort】 Wenn Sie ORM zum Bedienen von Daten in ...
In diesem Artikel wird der spezifische Code von V...
Linux-Version: CentOS 7 [root@azfdbdfsdf230lqdg1b...
Vorwort Semikolons sind in JavaScript optional un...