Cuando usamos Nodejs para el desarrollo diario, a menudo usamos require para importar dos tipos de módulos. Uno es el módulo que escribimos nosotros mismos o el módulo de terceros instalado usando npm. Este tipo de módulo se llama文件模块
en Node; Es el módulo integrado de Node que se nos proporciona para su uso, como os
, fs
y otros módulos. Estos módulos se denominan核心模块
.
Cabe señalar que la diferencia entre el módulo de archivo y el módulo principal radica no solo en si está integrado por Node, sino también en la ubicación del archivo, el proceso de compilación y ejecución del módulo. Existen diferencias obvias entre los dos. . No solo eso, los módulos de archivos también se pueden subdividir en módulos de archivos ordinarios, módulos personalizados o módulos de extensión C/C++, etc. Los diferentes módulos también tienen muchos detalles que difieren en el posicionamiento de los archivos, la compilación y otros procesos.
Este artículo abordará estos problemas y aclarará los conceptos de módulos de archivos y módulos principales, así como sus procesos y detalles específicos a los que se debe prestar atención en la ubicación, compilación o ejecución de archivos. Espero que le resulte útil.
Comencemos con el módulo de archivos.
¿Qué es un módulo de archivo?
En Node, los módulos que se requieren mediante identificadores de módulo que comienzan con .、.. 或/
(es decir, que utilizan rutas relativas o absolutas) se tratarán como módulos de archivos. Además, existe un tipo especial de módulo. Aunque no contiene una ruta relativa ni una ruta absoluta y no es un módulo principal, apunta a un paquete. Cuando Node localice este tipo de módulo, utilizará模块路径
para buscar el módulo uno por uno. Este tipo de módulo se denomina módulo personalizado.
Por lo tanto, los módulos de archivos incluyen dos tipos: uno son módulos de archivos normales con rutas y el otro son módulos personalizados sin rutas.
El módulo de archivo se carga dinámicamente en tiempo de ejecución, lo que requiere una ubicación completa del archivo, un proceso de compilación y ejecución, y es más lento que el módulo principal.
Para el posicionamiento de archivos, Node maneja estos dos tipos de módulos de archivos de manera diferente. Echemos un vistazo más de cerca a los procesos de búsqueda de estos dos tipos de módulos de archivos.
Para los módulos de archivos ordinarios, dado que la ruta que llevan es muy clara, la búsqueda no llevará mucho tiempo, por lo que la eficiencia de la búsqueda es mayor que la del módulo personalizado que se presenta a continuación. Sin embargo, aún quedan dos puntos por señalar.
Primero, en circunstancias normales, cuando se usa require para introducir un módulo de archivo, la extensión del archivo generalmente no se especifica, por ejemplo:
const math = require("math");
dado que no se especifica la extensión, Node no puede determinar el archivo final. En este caso, Node completará las extensiones en el orden de .js、.json、.node
y las probará una por una. Este proceso se denomina文件扩展名分析
.
También debe tenerse en cuenta que en el desarrollo real, además de requerir un archivo específico, generalmente también especificamos un directorio, como por ejemplo:
const
axios = require("../network");
análisis de extensión. Si no se encuentra el archivo correspondiente, pero se obtiene un directorio, Node tratará el directorio como un paquete.
Específicamente, Node devolverá el archivo señalado por el campo main
de package.json
en el directorio como resultado de la búsqueda. Si el archivo al que apunta main es incorrecto o el archivo package.json
no existe en absoluto, Node usará index
como nombre de archivo predeterminado y luego usará .js
y .node
para realizar un análisis de extensión y buscar el archivo de destino. uno por uno. Si no se encuentra, arrojará un error.
(Por supuesto, dado que Node tiene dos tipos de sistemas de módulos, CJS y ESM, además de buscar el campo principal, Node también utilizará otros métodos. Dado que está fuera del alcance de este artículo, no entraré en detalles. )
Se acaba de mencionar. Cuando Node busca módulos personalizados, utilizará la ruta del módulo. Entonces, ¿cuál es la ruta del módulo?
Los amigos que estén familiarizados con el análisis de módulos deben saber que la ruta del módulo es una matriz compuesta de rutas. El valor específico se puede ver en el siguiente ejemplo:
// example.js. console.log(module.paths);
imprimir resultados:
Como puede ver, el módulo en Node tiene una matriz de ruta de módulo, que se almacena en module.paths
y se usa para especificar cómo Node encuentra el módulo personalizado al que hace referencia el módulo actual.
Específicamente, Node atravesará la matriz de rutas del módulo, probará cada ruta una por una y descubrirá si hay un módulo personalizado específico en el directorio node_modules
correspondiente a la ruta. De lo contrario, se repetirá paso a paso hasta llegar a la ruta. directorio node_modules
en el directorio raíz Hasta que se encuentre el módulo de destino, se generará un error si no se encuentra.
Se puede ver que buscar recursivamente node_modules
paso a paso es la estrategia de Node para encontrar módulos personalizados, y la ruta del módulo es la implementación específica de esta estrategia.
Al mismo tiempo, también llegamos a la conclusión de que al buscar módulos personalizados, cuanto más profundo sea el nivel, más tiempo llevará la búsqueda correspondiente. Por lo tanto, en comparación con los módulos principales y los módulos de archivos normales, la velocidad de carga de los módulos personalizados es la más lenta.
Por supuesto, lo que se encuentra según la ruta del módulo es solo un directorio, no un archivo específico. Después de encontrar el directorio, Node también buscará de acuerdo con el proceso de procesamiento del paquete descrito anteriormente. El proceso específico no se describirá nuevamente.
Lo anterior es el proceso de posicionamiento de archivos y los detalles a los que se debe prestar atención para los módulos de archivos ordinarios y los módulos personalizados. A continuación, veamos cómo se compilan y ejecutan los dos tipos de módulos.
Cuandoy se localiza el archivo señalado por require, el identificador del módulo generalmente no tiene una extensión. Según el análisis de extensión de archivo mencionado anteriormente, podemos saber que Node admite la compilación y ejecución de archivos con. tres extensiones.:
archivo JavaScript. El archivo se lee sincrónicamente a través del módulo fs
y luego se compila y ejecuta. A excepción de los archivos .node
y .json
, los demás archivos se cargarán como archivos .js
.
Archivo .node
, que es un archivo de extensión compilado y generado después de escribir en C/C++. Node carga el archivo a través del método process.dlopen()
.
json, después de leer el archivo sincrónicamente a través del módulo fs
, use JSON.parse()
para analizar y devolver el resultado.
Antes de compilar y ejecutar el módulo de archivo, Node lo empaquetará usando un contenedor de módulo como se muestra a continuación:
(función(exportaciones, require, módulo, __filename, __dirname) { // Código del módulo});
se puede ver que a través del contenedor del módulo, Node empaqueta el módulo en el alcance de la función y lo aísla de otros alcances para evitar problemas como conflictos de nombres de variables y contaminación del alcance global. tiempo, al pasar los parámetros exports y require permiten que el módulo tenga las capacidades de importación y exportación necesarias. Esta es la implementación de módulos de Node.
Después de comprender el contenedor del módulo, primero veamos el proceso de compilación y ejecución del archivo json.
La compilación y ejecución de archivos json es la más sencilla. Después de leer sincrónicamente el contenido del archivo JSON a través del módulo fs
, Node usará JSON.parse() para analizar el objeto JavaScript, luego lo asignará al objeto de exportación del módulo y finalmente lo devolverá al módulo que hace referencia a él. El proceso es muy simple y tosco.
. Después de usar el contenedor del módulo para empaquetar los archivos JavaScript, el código empaquetado se ejecutará a través runInThisContext()
(similar a eval) del módulo vm
, devolviendo un objeto de función.
Luego, las exportaciones, los requisitos, el módulo y otros parámetros del módulo JavaScript se pasan a esta función para su ejecución. Después de la ejecución, el atributo de exportación del módulo se devuelve a la persona que llama. Este es el proceso de compilación y ejecución del archivo JavaScript.
Antes de explicar la compilación y ejecución de módulos de extensión C/C++, primero introduzcamos qué es un módulo de extensión C/C++.
Los módulos de extensión C/C++ pertenecen a una categoría de módulos de archivos. Como sugiere el nombre, estos módulos están escritos en C/C++. La diferencia con los módulos JavaScript es que no es necesario compilarlos después de cargarlos. después de ejecutarse directamente, por lo que se cargan un poco más rápido que los módulos de JavaScript. En comparación con los módulos de archivos escritos en JS, los módulos de extensión C/C++ tienen ventajas de rendimiento obvias. Para funciones que no pueden ser cubiertas por el módulo principal de Node o que tienen requisitos de rendimiento específicos, los usuarios pueden escribir módulos de extensión C/C++ para lograr sus objetivos.
Entonces, ¿qué es un archivo .node
y qué tiene que ver con los módulos de extensión C/C++?
De hecho, después de compilar el módulo de extensión C/C++ escrito, se genera un archivo .node
. En otras palabras, como usuarios del módulo, no introducimos directamente el código fuente del módulo de extensión C/C++, sino el archivo binario compilado del módulo de extensión C/C++. Por lo tanto, no es necesario compilar el archivo .node
. Una vez que Node encuentra el archivo .node
, solo necesita cargarlo y ejecutarlo. Durante la ejecución, el objeto de exportaciones del módulo se completa y se devuelve a la persona que llama.
Vale la pena señalar que los archivos .node
generados al compilar módulos de extensión C/C++ tienen diferentes formas en diferentes plataformas: en sistemas *nix
, los módulos de extensión C/C++ se compilan en archivos de objetos compartidos de enlace dinámico mediante compiladores como g++/gcc. La extensión es .so
; en Windows
el compilador de Visual C++ lo compila en un archivo de biblioteca de vínculos dinámicos y la extensión es .dll
. Pero la extensión que usamos en el uso real es .node
. De hecho, la extensión de .node
es solo para parecer más natural. De hecho, es un archivo .dll en Windows
y un archivo .dll
en archivos *nix
.so
.
Después de que Node encuentra el archivo .node
que necesita, llama process.dlopen()
para cargar y ejecutar el archivo. Dado que los archivos .node
tienen diferentes formatos de archivo en diferentes plataformas, para lograr una implementación multiplataforma, dlopen()
tiene diferentes implementaciones en las plataformas Windows
y *nix
, y luego se encapsula a través de libuv
. La siguiente figura muestra el proceso de compilación y carga de módulos de extensión C/C++ en diferentes plataformas:
El módulo principal se compila en un archivo ejecutable binario durante el proceso de compilación del código fuente de Node. Cuando se inicia el proceso del Nodo, algunos módulos principales se cargan directamente en la memoria. Por lo tanto, cuando se introducen estos módulos principales, los dos pasos de ubicación del archivo y compilación y ejecución se pueden omitir y se juzgarán antes que el módulo de archivo en la ruta. análisis. Por lo que su velocidad de carga es la más rápida.
El módulo principal en realidad está dividido en dos partes escritas en C/C++ y JavaScript. Los archivos C/C++ se almacenan en el directorio src del proyecto Node y los archivos JavaScript se almacenan en el directorio lib. Obviamente, los procesos de compilación y ejecución de estas dos partes de módulos son diferentes.
Para la compilación de módulos principales de JavaScript, durante el proceso de compilación del código fuente de Node, Node utilizará la herramienta js2c.py que viene con V8 para convertir todos los códigos JavaScript integrados, incluidos los módulos principales de JavaScript. en C++, el código JavaScript se almacena en el espacio de nombres del nodo como cadenas. Al iniciar el proceso de Nodo, el código JavaScript se carga directamente en la memoria.
Cuando se introduce un módulo central de JavaScript, Node llamará a process.binding()
para localizar su ubicación en la memoria mediante el análisis del identificador del módulo y recuperarlo. Después de ser eliminado, el módulo principal de JavaScript también será empaquetado por el contenedor del módulo, luego ejecutado, el objeto de exportación se exportará y se devolverá a la persona que llama.
en el módulo principal. Algunos módulos están todos escritos en C/C++, algunos módulos tienen la parte principal completada por C/C++ y otras partes están empaquetadas o exportadas por JavaScript para cumplir con los requisitos de rendimiento. Los módulos como buffer
, fs
, os
, etc. están escritos parcialmente en C/C++. Este modelo en el que el módulo C++ implementa el núcleo dentro de la parte principal y el módulo JavaScript implementa la encapsulación fuera de la parte principal es una forma común para que Node mejore el rendimiento.
Las partes del módulo principal escritas en C/C++ puro se denominan módulos integrados, como node_fs
, node_os
, etc. Por lo general, los usuarios no los llaman directamente, sino que dependen directamente del módulo principal de JavaScript. Por lo tanto, en el proceso de introducción del módulo principal de Node, existe una cadena de referencia de este tipo:
Entonces, ¿cómo carga el módulo principal de JavaScript el módulo integrado?
¿Recuerda process.binding()
? Node elimina el módulo principal de JavaScript de la memoria llamando a este método. Este método también se aplica a los módulos principales de JavaScript para ayudar a cargar los módulos integrados.
Específicamente para la implementación de este método, al cargar un módulo integrado, primero cree un objeto de exportación vacío, luego llame get_builtin_module()
para extraer el objeto del módulo integrado, complete el objeto de exportación ejecutando register_func()
, y finalmente devuélvalo a la persona que llama para completar la exportación. Este es el proceso de carga y ejecución del módulo integrado.
A través del análisis anterior, para la introducción de una cadena de referencia como el módulo central, tomando el módulo os como ejemplo, el proceso general es el siguiente:
En resumen, el proceso de introducción del módulo del sistema operativo implica la introducción del módulo de archivo JavaScript, la carga y ejecución del módulo central de JavaScript y la carga y ejecución del módulo integrado. El proceso es muy engorroso y complicado, pero. para la persona que llama al módulo, debido al blindaje del subyacente. Para implementaciones y detalles complejos, todo el módulo se puede importar simplemente a través de require (), que es muy simple. amigable.
Este artículo presenta los conceptos básicos de los módulos de archivos y los módulos principales, así como sus procesos y detalles específicos a los que se debe prestar atención en la ubicación, compilación o ejecución de archivos. Específicamente:
los módulos de archivos se pueden dividir en módulos de archivos ordinarios y módulos personalizados de acuerdo con los diferentes procesos de posicionamiento de archivos. Los módulos de archivos ordinarios se pueden ubicar directamente debido a sus rutas claras, lo que a veces implica el proceso de análisis de extensión de archivo y análisis de directorio. Los módulos personalizados buscarán según la ruta del módulo y, después de una búsqueda exitosa, la ubicación final del archivo se realizará mediante el análisis de directorio; .
Los módulos de archivos se pueden dividir en módulos JavaScript y módulos de extensión C/C++ según diferentes procesos de compilación y ejecución. Después de que el contenedor del módulo empaqueta el módulo JavaScript, se ejecuta a través del método runInThisContext
del módulo vm
, dado que el módulo de extensión C/C++ ya es un archivo ejecutable generado después de la compilación, se puede ejecutar directamente y se devuelve el objeto exportado; a la persona que llama.
El módulo principal se divide en módulo principal de JavaScript y módulo integrado. El módulo principal de JavaScript se carga en la memoria cuando se inicia el proceso del Nodo. Se puede extraer y luego ejecutar a través del método process.binding()
; la compilación y ejecución del módulo incorporado pasará por process.binding()
. Procesamiento de funciones get_builtin_module()
y register_func()
.
Además, también encontramos la cadena de referencia de Node para introducir módulos principales, es decir, módulo de archivo -> módulo principal de JavaScript -> módulo incorporado. También aprendimos que el módulo C ++ completa el núcleo internamente y JavaScript. El módulo implementa la encapsulación externamente.