Lorsque nous utilisons Nodejs pour le développement quotidien, nous utilisons souvent require pour importer deux types de modules. L'un est le module que nous avons écrit nous-mêmes ou le module tiers installé à l'aide de npm. Ce type de module est appelé文件模块
dans Node ; C'est le module intégré de Node qui nous est fourni, comme os
, fs
et d'autres modules. Ces modules sont appelés核心模块
.
Il convient de noter que la différence entre le module de fichier et le module principal réside non seulement dans le fait qu'il soit intégré par Node, mais également dans l'emplacement du fichier, la compilation et le processus d'exécution du module. Il existe des différences évidentes entre les deux. . De plus, les modules de fichiers peuvent également être subdivisés en modules de fichiers ordinaires, modules personnalisés ou modules d'extension C/C++, etc. Différents modules comportent également de nombreux détails qui diffèrent dans le positionnement des fichiers, la compilation et d'autres processus.
Cet article abordera ces problèmes et clarifiera les concepts de modules de fichiers et de modules de base ainsi que leurs processus et détails spécifiques auxquels il faut prêter attention lors de l'emplacement, de la compilation ou de l'exécution des fichiers. J'espère qu'il vous sera utile.
Commençons par le module de fichiers.
Qu'est-ce qu'un module de fichiers ?
Dans Node, les modules requis à l'aide d'identifiants de module commençant par .、.. 或/
(c'est-à-dire utilisant des chemins relatifs ou absolus) seront traités comme des modules de fichiers. De plus, il existe un type spécial de module. Bien qu'il ne contienne pas de chemin relatif ou de chemin absolu, et qu'il ne s'agisse pas d'un module principal, il pointe vers un package. Lorsque Node localise ce type de module, il模块路径
pour rechercher le module un par un. Ce type de module est appelé module personnalisé.
Par conséquent, les modules de fichiers incluent deux types, l'un étant des modules de fichiers ordinaires avec des chemins et l'autre des modules personnalisés sans chemins.
Le module de fichiers est chargé dynamiquement au moment de l'exécution, ce qui nécessite un processus complet d'emplacement, de compilation et d'exécution du fichier, et est plus lent que le module principal.
Pour le positionnement des fichiers, Node gère différemment ces deux types de modules de fichiers. Examinons de plus près les processus de recherche pour ces deux types de modules de fichiers.
Pour les modules de fichiers ordinaires, puisque le chemin qu'ils transportent est très clair, la recherche ne prendra pas longtemps, donc l'efficacité de la recherche est supérieure à celle du module personnalisé présenté ci-dessous. Il reste cependant deux points à noter.
Premièrement, dans des circonstances normales, lors de l'utilisation de require pour introduire un module de fichier, l'extension du fichier n'est généralement pas spécifiée, par exemple :
const math = require("math");
Puisque l'extension n'est pas spécifiée, Node ne peut pas déterminer le fichier final. Dans ce cas, Node complétera les extensions dans l'ordre .js、.json、.node
et les essaiera une par une. Ce processus est appelé文件扩展名分析
.
Il convient également de noter que dans le développement réel, en plus d'exiger un fichier spécifique, nous spécifions généralement également un répertoire, tel que :
const axios = require("../network");
Dans ce cas, Node exécutera d'abord le fichier ; Analyse de l'extension. Si le fichier correspondant n'est pas trouvé, mais qu'un répertoire est obtenu, Node traitera le répertoire comme un package.
Plus précisément, Node renverra le fichier pointé par le champ main
de package.json
dans le répertoire comme résultat de la recherche. Si le fichier pointé par main est erroné ou si le fichier package.json
n'existe pas du tout, Node utilisera index
comme nom de fichier par défaut, puis utilisera .js
et .node
pour effectuer une analyse d'extension et rechercher le fichier cible. un par un. S'il n'est pas trouvé, une erreur sera générée.
(Bien sûr, puisque Node dispose de deux types de systèmes de modules, CJS et ESM, en plus de rechercher le champ principal, Node utilisera également d'autres méthodes. Comme cela sort du cadre de cet article, je n'entrerai pas dans les détails. )
vient d'être mentionnée. Lorsque Node recherche des modules personnalisés, il utilisera le chemin du module. Alors, quel est le chemin du module ?
Les amis qui connaissent l'analyse des modules doivent savoir que le chemin du module est un tableau composé de chemins. La valeur spécifique peut être vue dans l'exemple suivant :
// exemple.js. console.log(module.paths);
imprimer les résultats :
Comme vous pouvez le voir, le module dans Node possède un tableau de chemins de module, qui est stocké dans module.paths
et est utilisé pour spécifier comment Node trouve le module personnalisé référencé par le module actuel.
Plus précisément, Node parcourra le tableau des chemins du module, essaiera chaque chemin un par un et découvrira s'il existe un module personnalisé spécifié dans le répertoire node_modules
correspondant au chemin. Sinon, il remontera étape par étape jusqu'à ce qu'il atteigne le chemin. Répertoire node_modules
dans le répertoire racine Jusqu'à ce que le module cible soit trouvé, une erreur sera générée s'il n'est pas trouvé.
On peut voir que la recherche récursive node_modules
étape par étape est la stratégie de Node pour trouver des modules personnalisés, et le chemin du module est l'implémentation spécifique de cette stratégie.
Dans le même temps, nous sommes également arrivés à la conclusion que lors de la recherche de modules personnalisés, plus le niveau est profond, plus la recherche correspondante prendra du temps. Par conséquent, par rapport aux modules de base et aux modules de fichiers ordinaires, la vitesse de chargement des modules personnalisés est la plus lente.
Bien sûr, ce qui est trouvé en fonction du chemin du module n'est qu'un répertoire, pas un fichier spécifique. Après avoir trouvé le répertoire, Node effectuera également une recherche selon le processus de traitement du package décrit ci-dessus. Le processus spécifique ne sera pas décrit à nouveau.
Ce qui précède concerne le processus de positionnement des fichiers et les détails auxquels il faut prêter attention pour les modules de fichiers ordinaires et les modules personnalisés. Voyons ensuite comment les deux types de modules sont compilés et exécutés.
Lorsqueet que le fichier pointé par require est localisé, l'identifiant du module n'a généralement pas d'extension. Selon l'analyse des extensions de fichier mentionnée ci-dessus, nous pouvons savoir que Node prend en charge la compilation et l'exécution de fichiers avec. trois extensions. :
fichier JavaScript. Le fichier est lu de manière synchrone via le module fs
puis compilé et exécuté. À l'exception des fichiers .node
et .json
, les autres fichiers seront chargés en tant que fichiers .js
.
Le fichier .node
, qui est un fichier d'extension compilé et généré après l'écriture en C/C++, Node charge le fichier via la méthode process.dlopen()
.
json, après avoir lu le fichier de manière synchrone via le module fs
, utilisez JSON.parse()
pour analyser et renvoyer le résultat.
Avant de compiler et d'exécuter le module de fichier, Node l'encapsulera à l'aide d'un wrapper de module comme indiqué ci-dessous :
(function(exports, require, module, __filename, __dirname) { //Code du module});
On peut voir que grâce au wrapper du module, Node emballe le module dans la portée de la fonction et l'isole des autres portées pour éviter des problèmes tels que les conflits de noms de variables et la contamination de la portée globale. temps, en passant les paramètres exports et require permettent au module de disposer des capacités d'import et d'export nécessaires. Il s'agit de l'implémentation des modules de Node.
Après avoir compris le wrapper du module, examinons d'abord le processus de compilation et d'exécution du fichier json.
La compilation et l'exécution de fichiers json sont les plus simples. Après avoir lu de manière synchrone le contenu du fichier JSON via le module fs
, Node utilisera JSON.parse() pour analyser l'objet JavaScript, puis l'attribuera à l'objet exports du module, et enfin le renverra au module qui le référence. . Le processus est très simple et grossier.
. Après avoir utilisé le wrapper de module pour envelopper les fichiers JavaScript, le code encapsulé sera exécuté via runInThisContext()
(similaire à eval) du module vm
, renvoyant un objet fonction.
Ensuite, les paramètres exports, require, module et autres du module JavaScript sont transmis à cette fonction pour exécution. Après exécution, l'attribut exports du module est renvoyé à l'appelant. Il s'agit du processus de compilation et d'exécution du fichier JavaScript.
Avant d'expliquer la compilation et l'exécution de modules d'extension C/C++, présentons d'abord ce qu'est un module d'extension C/C++.
Les modules d'extension C/C++ appartiennent à une catégorie de modules de fichiers. Comme leur nom l'indique, ces modules sont écrits en C/C++. La différence avec les modules JavaScript est qu'ils n'ont pas besoin d'être compilés après avoir été chargés. après avoir été exécutés directement, ils sont donc chargés légèrement plus rapidement que les modules JavaScript. Par rapport aux modules de fichiers écrits en JS, les modules d'extension C/C++ présentent des avantages évidents en termes de performances. Pour les fonctions qui ne peuvent pas être couvertes par le module principal Node ou qui ont des exigences de performances spécifiques, les utilisateurs peuvent écrire des modules d'extension C/C++ pour atteindre leurs objectifs.
Alors, qu'est-ce qu'un fichier .node
et qu'est-ce qu'il a à voir avec les modules d'extension C/C++ ?
En fait, une fois le module d'extension C/C++ écrit compilé, un fichier .node
est généré. Autrement dit, en tant qu'utilisateurs du module, nous n'introduisons pas directement le code source du module d'extension C/C++, mais le fichier binaire compilé du module d'extension C/C++. Par conséquent, le fichier .node
n'a pas besoin d'être compilé. Une fois que Node a trouvé le fichier .node
, il lui suffit de charger et d'exécuter le fichier. Lors de l'exécution, l'objet exports du module est renseigné et renvoyé à l'appelant.
Il est à noter que les fichiers .node
générés par la compilation des modules d'extension C/C++ ont différentes formes sous différentes plates-formes : sous les systèmes *nix
, les modules d'extension C/C++ sont compilés en fichiers objets partagés de liens dynamiques par des compilateurs tels que g++/gcc. L'extension est .so
; sous Windows
elle est compilée dans un fichier de bibliothèque de liens dynamiques par le compilateur Visual C++ et l'extension est .dll
. Mais l'extension que nous utilisons en utilisation réelle est .node
. En fait, l'extension de .node
est juste pour paraître plus naturelle. En fait, il s'agit d'un fichier .dll sous Windows
et d'un fichier .dll
sous les fichiers *nix
.so
. .
Une fois que Node a trouvé le fichier .node
requis, il appelle process.dlopen()
pour charger et exécuter le fichier. Étant donné que les fichiers .node
ont différentes formes de fichiers sous différentes plates-formes, afin de réaliser une implémentation multiplateforme, dlopen()
a différentes implémentations sous les plates-formes Windows
et *nix
, et est ensuite encapsulée via libuv
. La figure suivante montre le processus de compilation et de chargement des modules d'extension C/C++ sous différentes plates-formes :
Le module principal est compilé dans un fichier exécutable binaire pendant le processus de compilation du code source de Node. Lorsque le processus Node démarre, certains modules de base sont chargés directement dans la mémoire. Par conséquent, lorsque ces modules de base sont introduits, les deux étapes de localisation, de compilation et d'exécution du fichier peuvent être omises et seront jugées avant le module de fichier dans le chemin. analyse Sa vitesse de chargement est donc la plus rapide.
Le module principal est en fait divisé en deux parties écrites en C/C++ et JavaScript. Les fichiers C/C++ sont stockés dans le répertoire src du projet Node, et les fichiers JavaScript sont stockés dans le répertoire lib. Evidemment, les processus de compilation et d'exécution de ces deux parties de modules sont différents.
Pour la compilation des modules de base JavaScript, pendant le processus de compilation du code source de Node, Node utilisera l'outil js2c.py fourni avec la V8 pour convertir tous les codes JavaScript intégrés, y compris les modules de base JavaScript, dans les tableaux C++, le code JavaScript est stocké dans l'espace de noms du nœud sous forme de chaînes. Lors du démarrage du processus Node, le code JavaScript est chargé directement en mémoire.
Lorsqu'un module principal JavaScript est introduit, Node appellera process.binding()
pour localiser son emplacement en mémoire grâce à l'analyse de l'identifiant du module et le récupérer. Après avoir été supprimé, le module principal JavaScript sera également encapsulé par le wrapper de module, puis exécuté, l'objet exports sera exporté et renvoyé à l'appelant.
dans le module principal. Certains modules sont tous écrits en C/C++, certains modules ont la partie principale complétée par C/C++, et d'autres parties sont empaquetées ou exportées par JavaScript pour répondre aux exigences de performances. . Les modules comme buffer
, fs
, os
, etc. sont en partie écrits en C/C++. Ce modèle dans lequel le module C++ implémente le noyau à l'intérieur de la partie principale et le module JavaScript implémente l'encapsulation en dehors de la partie principale est un moyen courant pour Node d'améliorer les performances.
Les parties du module principal écrites en C/C++ pur sont appelées modules intégrés, tels que node_fs
, node_os
, etc. Ils ne sont généralement pas appelés directement par les utilisateurs, mais dépendent directement du module principal JavaScript. Par conséquent, dans le processus d'introduction du module principal de Node, il existe une telle chaîne de référence :
Alors, comment le module principal JavaScript charge-t-il le module intégré ?
Vous vous souvenez process.binding()
? Node supprime le module principal JavaScript de la mémoire en appelant cette méthode. Cette méthode s'applique également aux modules de base JavaScript pour faciliter le chargement des modules intégrés.
Spécifique à l'implémentation de cette méthode, lors du chargement d'un module intégré, créez d'abord un objet exports vide, puis appelez get_builtin_module()
pour retirer l'objet module intégré, remplissez l'objet exports en exécutant register_func()
, et enfin renvoyez-le à l'appelant pour terminer l'exportation. Il s'agit du processus de chargement et d'exécution du module intégré.
Grâce à l'analyse ci-dessus, pour l'introduction d'une chaîne de référence telle que le module de base, en prenant le module os comme exemple, le processus général est le suivant :
En résumé, le processus d'introduction du module os implique l'introduction du module de fichier JavaScript, le chargement et l'exécution du module principal JavaScript, ainsi que le chargement et l'exécution du module intégré. Le processus est très lourd et compliqué, mais. pour l'appelant du module, en raison du blindage du sous-jacent. Pour des implémentations et des détails complexes, le module entier peut être importé simplement via require(), ce qui est très simple. amical.
Cet article présente les concepts de base des modules de fichiers et des modules de base ainsi que leurs processus et détails spécifiques auxquels il faut prêter attention lors de l'emplacement, de la compilation ou de l'exécution des fichiers. Plus précisément :
les modules de fichiers peuvent être divisés en modules de fichiers ordinaires et modules personnalisés en fonction des différents processus de positionnement des fichiers. Les modules de fichiers ordinaires peuvent être localisés directement en raison de leurs chemins clairs, impliquant parfois le processus d'analyse des extensions de fichiers et d'analyse de répertoire ; les modules personnalisés effectueront une recherche en fonction du chemin du module, et après une recherche réussie, l'emplacement final du fichier sera effectué via une analyse de répertoire ; .
Les modules de fichiers peuvent être divisés en modules JavaScript et modules d'extension C/C++ selon différents processus de compilation et d'exécution. Une fois le module JavaScript empaqueté par le wrapper de module, il est exécuté via la méthode runInThisContext
du module vm
puisque le module d'extension C/C++ est déjà un fichier exécutable généré après la compilation, il peut être exécuté directement et l'objet exporté est renvoyé ; à l'appelant.
Le module principal est divisé en module principal JavaScript et module intégré. Le module principal JavaScript est chargé en mémoire au démarrage du processus Node. Il peut être retiré puis exécuté via la méthode process.binding()
; la compilation et l'exécution du module intégré passeront par process.binding()
, Traitement des fonctions get_builtin_module()
et register_func()
.
De plus, nous avons également trouvé la chaîne de référence pour Node pour introduire les modules de base, c'est-à-dire le module de fichier -> module principal JavaScript -> module intégré. Nous avons également appris que le module C++ complète le noyau en interne et le JavaScript. Le module implémente l'encapsulation en externe.