Quando usamos Nodejs文件模块
desenvolvimento diário, geralmente usamos require para importar dois tipos de módulos. Um é o módulo que escrevemos ou o módulo de terceiros instalado usando npm. é É o módulo integrado do Node que é fornecido para uso, como os
, fs
e outros módulos. Esses módulos são chamados de核心模块
.
Deve-se notar que a diferença entre o módulo de arquivo e o módulo principal não reside apenas no fato de ele ser integrado pelo Node, mas também na localização do arquivo, no processo de compilação e execução do módulo. . Além disso, os módulos de arquivo também podem ser subdivididos em módulos de arquivo comuns, módulos personalizados ou módulos de extensão C/C++, etc. Diferentes módulos também possuem muitos detalhes que diferem no posicionamento de arquivos, compilação e outros processos.
Este artigo abordará essas questões e esclarecerá os conceitos de módulos de arquivo e módulos principais, bem como seus processos e detalhes específicos que precisam ser observados na localização, compilação ou execução de arquivos. Espero que seja útil para você.
Vamos começar com o módulo de arquivo.
O que é um módulo de arquivo?
No Node, os módulos que são necessários usando identificadores de módulo começando com .、.. 或/
(ou seja, usando caminhos relativos ou caminhos absolutos) serão tratados como módulos de arquivo. Além disso, existe um tipo especial de módulo, embora não contenha um caminho relativo ou absoluto e não seja um módulo principal, ele aponta para um模块路径
.模块路径
para procurar o módulo um por um. Este tipo de módulo é chamado de módulo personalizado.
Portanto, os módulos de arquivo incluem dois tipos, um são módulos de arquivo comuns com caminhos e o outro são módulos personalizados sem caminhos.
O módulo de arquivo é carregado dinamicamente em tempo de execução, o que requer um processo completo de localização, compilação e execução do arquivo e é mais lento que o módulo principal.
Para posicionamento de arquivos, o Node lida com esses dois tipos de módulos de arquivo de maneira diferente. Vamos dar uma olhada mais de perto nos processos de pesquisa para esses dois tipos de módulos de arquivo.
Para módulos de arquivos comuns, como o caminho que eles carregam é muito claro, a pesquisa não demorará muito, portanto a eficiência da pesquisa é maior do que o módulo personalizado apresentado a seguir. No entanto, ainda há dois pontos a serem observados.
Primeiro, em circunstâncias normais, ao usar require para introduzir um módulo de arquivo, a extensão do arquivo geralmente não é especificada, por exemplo:
const
math = require("math");
Neste caso, o Node irá completar as extensões na ordem de .js、.json、.node
e experimentá-las uma por uma. Este processo é chamado de文件扩展名分析
.
um
diretório, como:
const axios = require("../network");
análise de extensão. Se o arquivo correspondente não for encontrado, mas um diretório for obtido, o Node tratará o diretório como um pacote.
Especificamente, o Node retornará o arquivo apontado pelo campo main
de package.json
no diretório como resultado da pesquisa. Se o arquivo apontado por main estiver errado ou o arquivo package.json
não existir, o Node usará index
como o nome de arquivo padrão e, em seguida, usará .js
e .node
para realizar a análise de extensão e procurar o arquivo de destino um por um. Se não for encontrado, ocorrerá um erro.
(Claro, como o Node possui dois tipos de sistemas de módulos, CJS e ESM, além de pesquisar o campo principal, o Node também utilizará outros métodos. Por estar fora do escopo deste artigo, não entrarei em detalhes. )
acabou de ser mencionada. Quando o Node procura por módulos customizados, ele usará o caminho do módulo.
Amigos familiarizados com a análise de módulos devem saber que o caminho do módulo é uma matriz composta de caminhos. O valor específico pode ser visto no exemplo a seguir:
// example.js. console.log(module.paths);
imprimir resultados:
Como você pode ver, o módulo no Node possui uma matriz de caminho de módulo, que é armazenada em module.paths
e é usada para especificar como o Node encontra o módulo personalizado referenciado pelo módulo atual.
Especificamente, o Node percorrerá a matriz de caminhos do módulo, tentará cada caminho um por um e descobrirá se existe um módulo personalizado especificado no diretório node_modules
correspondente ao caminho. Caso contrário, ele irá subir passo a passo até atingir o caminho. diretório node_modules
no diretório raiz Até que o módulo de destino seja encontrado, um erro será gerado se ele não for encontrado.
Pode-se observar que a busca recursiva node_modules
passo a passo é a estratégia do Node para encontrar módulos customizados, e o caminho do módulo é a implementação específica dessa estratégia.
Ao mesmo tempo, também chegamos à conclusão de que na busca por módulos customizados, quanto mais profundo o nível, mais demorada será a busca correspondente. Portanto, em comparação com módulos principais e módulos de arquivo comuns, a velocidade de carregamento dos módulos personalizados é a mais lenta.
Claro, o que é encontrado com base no caminho do módulo é apenas um diretório, não um arquivo específico. Depois de encontrar o diretório, o Node também pesquisará de acordo com o processo de processamento do pacote descrito acima.
A descrição acima é o processo de posicionamento de arquivo e os detalhes que precisam ser observados para módulos de arquivo comuns e módulos personalizados. A seguir, vamos ver como os dois tipos de módulos são compilados e executados.
Quandoe o arquivo apontado por require é localizado, o identificador do módulo geralmente não possui extensão. De acordo com a análise de extensão de arquivo mencionada acima, podemos saber que o Node suporta a compilação e execução de arquivos com. três extensões:
arquivo JavaScript. O arquivo é lido de forma síncrona através do módulo fs
e então compilado e executado. Exceto os arquivos .node
e .json
, outros arquivos serão carregados como arquivos .js
.
Arquivo .node
, que é um arquivo de extensão compilado e gerado após a gravação em C/C++. O Node carrega o arquivo por meio do método process.dlopen()
.
json, após ler o arquivo de forma síncrona por meio do módulo fs
, use JSON.parse()
para analisar e retornar o resultado.
Antes de compilar e executar o módulo de arquivo, o Node irá envolvê-lo usando um wrapper de módulo conforme mostrado abaixo:
(function(exports, require, module, __filename, __dirname) { //Código do módulo});
Pode-se observar que através do wrapper do módulo, o Node empacota o módulo no escopo da função e o isola de outros escopos para evitar problemas como conflitos de nomenclatura de variáveis e contaminação do escopo global. vez, passando os parâmetros exports e require permitem que o módulo tenha os recursos necessários de importação e exportação. Esta é a implementação de módulos do Node.
Depois de entender o wrapper do módulo, vamos primeiro dar uma olhada no processo de compilação e execução do arquivo JSON.
A compilação e execução de arquivos json é a mais simples. Depois de ler de forma síncrona o conteúdo do arquivo JSON por meio do módulo fs
, o Node usará JSON.parse() para analisar o objeto JavaScript, em seguida, atribuí-lo ao objeto de exportação do módulo e, finalmente, devolvê-lo ao módulo que o referencia. . O processo é muito simples e rudimentar.
Após usar o wrapper do módulo para encapsular os arquivos JavaScript, o código encapsulado será executado através runInThisContext()
(semelhante ao eval) do módulo vm
, retornando um objeto de função.
Em seguida, os parâmetros exports, require, module e outros parâmetros do módulo JavaScript são passados para esta função para execução. Após a execução, o atributo exports do módulo é retornado ao chamador. Este é o processo de compilação e execução do arquivo JavaScript.
Antes de explicar a compilação e execução de módulos de extensão C/C++, vamos primeiro apresentar o que é um módulo de extensão C/C++.
Os módulos de extensão C/C++ pertencem a uma categoria de módulos de arquivo. Como o nome sugere, esses módulos são escritos em C/C++. A diferença dos módulos JavaScript é que eles não precisam ser compilados após serem carregados. depois de serem executados diretamente, eles são carregados um pouco mais rápido que os módulos JavaScript. Comparados aos módulos de arquivo escritos em JS, os módulos de extensão C/C++ têm vantagens óbvias de desempenho. Para funções que não podem ser cobertas pelo módulo principal do Node ou têm requisitos específicos de desempenho, os usuários podem escrever módulos de extensão C/C++ para atingir seus objetivos.
Então, o que é um arquivo .node
e o que ele tem a ver com módulos de extensão C/C++?
Na verdade, após a compilação do módulo de extensão C/C++ escrito, um arquivo .node
é gerado. Em outras palavras, como usuários do módulo, não apresentamos diretamente o código-fonte do módulo de extensão C/C++, mas o arquivo binário compilado do módulo de extensão C/C++. Portanto, o arquivo .node
não precisa ser compilado. Após o Node encontrar o arquivo .node
, ele só precisa carregar e executar o arquivo. Durante a execução, o objeto de exportação do módulo é preenchido e retornado ao chamador.
É importante notar que os arquivos .node
gerados pela compilação de módulos de extensão C/C++ têm diferentes formas em diferentes plataformas: em sistemas *nix
, os módulos de extensão C/C++ são compilados em arquivos de objeto compartilhado de link dinâmico por compiladores como g++/gcc. A extensão é .so
; no Windows
ela é compilada em um arquivo de biblioteca de vínculo dinâmico pelo compilador Visual C++ e a extensão é .dll
. Mas a extensão que usamos na verdade é .node
. Na verdade, a extensão de .node
é apenas para parecer mais natural, é um arquivo .dll no Windows
e um arquivo .dll
em arquivos *nix
.so
. .
Depois que o Node encontra o arquivo .node
necessário, ele chama process.dlopen()
para carregar e executar o arquivo. Como os arquivos .node
têm diferentes formatos de arquivo em diferentes plataformas, para obter implementação multiplataforma, dlopen()
tem diferentes implementações nas plataformas Windows
e *nix
e é então encapsulado por meio libuv
. A figura a seguir mostra o processo de compilação e carregamento de módulos de extensão C/C++ em diferentes plataformas:
O módulo principal é compilado em um arquivo executável binário durante o processo de compilação do código-fonte do Node. Quando o processo do Node é iniciado, alguns módulos principais são carregados diretamente na memória. Portanto, quando esses módulos principais são introduzidos, as duas etapas de localização do arquivo, compilação e execução podem ser omitidas e serão julgadas antes do módulo de arquivo no caminho. análise. Portanto, sua velocidade de carregamento é a mais rápida.
O módulo principal é na verdade dividido em duas partes escritas em C/C++ e JavaScript. Os arquivos C/C++ são armazenados no diretório src do projeto Node e os arquivos JavaScript são armazenados no diretório lib. Obviamente, os processos de compilação e execução destas duas partes dos módulos são diferentes.
Para a compilação de módulos principais JavaScript, durante o processo de compilação do código-fonte do Node, o Node usará a ferramenta js2c.py que vem com o V8 para converter todos os códigos JavaScript integrados, incluindo módulos principais JavaScript, em matrizes C++, o código JavaScript é armazenado no namespace do nó como strings. Ao iniciar o processo do Node, o código JavaScript é carregado diretamente na memória.
Quando um módulo principal JavaScript é introduzido, o Node chamará process.binding()
para localizar sua localização na memória por meio da análise do identificador do módulo e recuperá-lo. Depois de retirado, o módulo principal do JavaScript também será encapsulado pelo wrapper do módulo e, em seguida, executado, o objeto de exportação será exportado e retornado ao chamador.
no módulo principal. Alguns módulos são todos escritos em C/C++, alguns módulos têm a parte principal concluída em C/C++ e outras partes são empacotadas ou exportadas por JavaScript para atender aos requisitos de desempenho. Módulos como buffer
, fs
, os
, etc. são parcialmente escritos em C/C++. Este modelo em que o módulo C++ implementa o núcleo dentro da parte principal e o módulo JavaScript implementa o encapsulamento fora da parte principal é uma forma comum do Node melhorar o desempenho.
As partes do módulo principal escritas em C/C++ puro são chamadas de módulos integrados, como node_fs
, node_os
, etc. Eles geralmente não são chamados diretamente pelos usuários, mas dependem diretamente do módulo principal do JavaScript. Portanto, no processo de introdução do módulo principal do Node, existe uma tal cadeia de referência:
Então, como o módulo principal do JavaScript carrega o módulo integrado?
Lembra process.binding()
? O Node remove o módulo principal do JavaScript da memória chamando este método. Este método também se aplica aos módulos principais do JavaScript para auxiliar no carregamento de módulos integrados.
Específico para a implementação deste método, ao carregar um módulo integrado, primeiro crie um objeto de exportação vazio e, em seguida, chame get_builtin_module()
para retirar o objeto de módulo integrado, preencha o objeto de exportação executando register_func()
, e finalmente devolvê-lo ao chamador para concluir a exportação. Este é o processo de carregamento e execução do módulo integrado.
Através da análise acima, para a introdução de uma cadeia de referência como o módulo principal, tomando o módulo os como exemplo, o processo geral é o seguinte:
Em resumo, o processo de introdução do módulo os envolve a introdução do módulo de arquivo JavaScript, o carregamento e execução do módulo principal JavaScript e o carregamento e execução do módulo integrado. O processo é muito complicado e complicado, mas. para o chamador do módulo, devido à blindagem do subjacente. Para implementações e detalhes complexos, o módulo inteiro pode ser importado simplesmente através de require(), o que é muito simples. amigável.
Este artigo apresenta os conceitos básicos de módulos de arquivo e módulos principais, bem como seus processos específicos e detalhes que precisam ser observados na localização, compilação ou execução de arquivos. Especificamente:
os módulos de arquivo podem ser divididos em módulos de arquivo comuns e módulos personalizados de acordo com os diferentes processos de posicionamento de arquivos. Módulos de arquivo comuns podem ser localizados diretamente por causa de seus caminhos claros, às vezes envolvendo o processo de análise de extensão de arquivo e análise de diretório, os módulos personalizados pesquisarão com base no caminho do módulo e, após a pesquisa bem-sucedida, a localização final do arquivo será realizada por meio de análise de diretório; .
Os módulos de arquivo podem ser divididos em módulos JavaScript e módulos de extensão C/C++ de acordo com diferentes processos de compilação e execução. Após o módulo JavaScript ser empacotado pelo wrapper do módulo, ele é executado através do método runInThisContext
do módulo vm
, pois o módulo de extensão C/C++ já é um arquivo executável gerado após a compilação, ele pode ser executado diretamente e o objeto exportado é retornado; para o chamador.
O módulo principal é dividido em módulo principal JavaScript e módulo integrado. O módulo principal do JavaScript é carregado na memória quando o processo do Node é iniciado. Ele pode ser retirado e executado por meio do método process.binding()
process.binding()
; processamento das funções get_builtin_module()
e register_func()
.
Além disso, também encontramos a cadeia de referência para o Node introduzir módulos principais, ou seja, módulo de arquivo -> módulo principal JavaScript -> módulo integrado. Também aprendemos que o módulo C++ completa o núcleo internamente e o JavaScript. módulo implementa encapsulamento método de escrita de módulo.