Curso de introdução rápida ao Node.js: entre para aprender
Há dois anos escrevi um artigo apresentando o sistema de módulos: Entendendo o conceito de módulos front-end: CommonJs e ES6Module. O conhecimento deste artigo é voltado para iniciantes e é relativamente simples. Aqui também corrigimos alguns erros no artigo:
[Módulo] e [Sistema de Módulo] são duas coisas diferentes. Um módulo é uma unidade de software e um sistema de módulos é um conjunto de sintaxe ou ferramentas. O sistema de módulos permite que os desenvolvedores definam e usem módulos em projetos.
A abreviatura de Módulo ECMAScript é ESM, ou ESModule, não ES6Module.
O conhecimento básico sobre o sistema de módulos é quase abordado no artigo anterior, portanto, este artigo se concentrará nos princípios internos do sistema de módulos e em uma introdução mais completa às diferenças entre os diferentes sistemas de módulos. não será repetido novamente.
Nem todas as linguagens de programação possuem um sistema de módulos integrado, e o JavaScript não teve um sistema de módulos por muito tempo após seu nascimento.
No ambiente do navegador, você só pode usar a tag <script>
para introduzir arquivos de código não utilizados. Este método compartilha um escopo global, que pode ser considerado cheio de problemas. não atende mais às necessidades atuais. Antes do sistema de módulos oficial aparecer, a comunidade front-end criou seu próprio sistema de módulos de terceiros. Os mais comumente usados são: definição de módulo assíncrono AMD , definição de módulo universal UMD , etc.
Como o Node.js é um ambiente de tempo de execução JavaScript, ele pode acessar diretamente o sistema de arquivos subjacente. Assim, os desenvolvedores o adotaram e implementaram um sistema de módulos de acordo com as especificações CommonJS.
No início, o CommonJS só podia ser usado na plataforma Node.js. Com o surgimento de ferramentas de empacotamento de módulos, como Browserify e Webpack, o CommonJS pode finalmente ser executado no lado do navegador.
Somente com o lançamento da especificação ECMAScript6 em 2015 é que houve um padrão formal para o sistema de módulos. O sistema de módulos construído de acordo com este padrão foi chamado de módulo ECMAScript (ESM), abreviadamente. o ambiente Node.js e o ambiente do navegador. Claro, ECMAScript6 fornece apenas sintaxe e semântica. Quanto à implementação, cabe a vários fornecedores de serviços de navegador e desenvolvedores de Node trabalharem duro. É por isso que temos o artefato babel que causa inveja em outras linguagens de programação. Implementar um sistema de módulos não é uma tarefa fácil. O Node.js só tem suporte relativamente estável para ESM na versão 13.2.
Mas não importa o que aconteça, o ESM é o “filho” do JavaScript e não há nada de errado em aprendê-lo!
Na era da agricultura de corte e queima, o JavaScript era usado para desenvolver aplicativos, e os arquivos de script só podiam ser introduzidos por meio de tags de script. Um dos problemas mais sérios é a falta de um mecanismo de namespace, o que significa que cada script compartilha o mesmo escopo. Existe uma solução melhor para este problema na comunidade: Módulo Revelação
const meuMódulo = (() => { const _privateFn = () => {} const_privateAttr = 1 retornar { publicFn: () => {}, públicoAttr: 2 } })() console.log(meuMódulo) console.log(meuMódulo.publicFn, meuMódulo._privateFn)
Os resultados da execução são os seguintes:
Esse padrão é muito simples, use IIFE para criar um escopo privado e use variáveis de retorno para serem expostas. Variáveis internas (como _privateFn, _privateAttr) não podem ser acessadas do escopo externo.
[módulo revelador] aproveita esses recursos para ocultar informações privadas e exportar APIs que deveriam ser expostas ao mundo exterior. O sistema de módulos subsequente também é desenvolvido com base nesta ideia.
Com base nas ideias acima, desenvolva um carregador de módulo.
Primeiro escreva uma função que carregue o conteúdo do módulo, envolva essa função em um escopo privado e, em seguida, avalie-a por meio de eval() para executar a função:
function loadModule (nome do arquivo, módulo, requer) { const wrapSrc = `(função (módulo, exportações, exigir) { ${fs.readFileSync(nome do arquivo, 'utf8)} }(módulo, módulo.exportações, requer)` avaliação(wrappedSrc) }
Assim como [módulo revelador], o código-fonte do módulo é encapsulado em uma função. A diferença é que uma série de variáveis (módulo, module.exports, require) também são passadas para a função.
Vale ressaltar que o conteúdo do módulo é lido através do [readFileSync]. De modo geral, você não deve usar a versão sincronizada ao chamar APIs que envolvem o sistema de arquivos. Mas desta vez é diferente, porque o carregamento de módulos através do próprio sistema CommonJs deve ser implementado como uma operação síncrona para garantir que vários módulos possam ser introduzidos na ordem de dependência correta.
Em seguida, simule a função require(), cuja função principal é carregar o módulo.
function require(nomedomódulo) { const id = require.resolve(moduleName) if (require.cache[id]) { retornar require.cache[id].exportações } // Metadados do módulo const module = { exportações: {}, EU IA } //Atualizar cache require.cache[id] = módulo //Carrega módulo loadModule(id, módulo, require) // Retorna variáveis exportadas return module.exports } requer.cache = {} require.resolve = (moduleName) => { // analisa o id completo do módulo com base em moduleName }
(1) Depois que a função recebe o moduleName, ela primeiro analisa o caminho completo do módulo e o atribui ao id.
(2) Se cache[id]
for verdadeiro, significa que o módulo foi carregado e o resultado do cache será retornado diretamente. (3) Caso contrário, um ambiente será configurado para o primeiro carregamento. Especificamente, crie um objeto de módulo, incluindo exportações (ou seja, conteúdo exportado) e id (a função é como acima)
(4) Armazene em cache o módulo carregado pela primeira vez (5) Leia o código-fonte do arquivo fonte do módulo por meio de loadModule (6) Finalmente, return module.exports
retorna o conteúdo que você deseja exportar.
Ao simular a função require, há um detalhe muito importante: a função require deve ser síncrona . Sua função é apenas retornar diretamente o conteúdo do módulo, e não utiliza o mecanismo de callback. O mesmo se aplica ao require no Node.js. Portanto, a operação de atribuição para module.exports também deve ser síncrona. Se for usado assíncrono, ocorrerão problemas:
// Algo deu errado setTimeout(() => { módulo.exportações = função () {} }, 1000)
O fato de require ser uma função síncrona tem um impacto muito importante na forma de definir módulos, pois nos obriga a usar apenas código síncrono na definição de módulos, de modo que o Node.js fornece versões síncronas da maioria das APIs assíncronas para esse fim.
Os primeiros Node.js tinham uma versão assíncrona da função require, mas ela foi rapidamente removida porque tornaria a função muito complicada.
ESM faz parte da especificação ECMAScript2015, que especifica um sistema de módulos oficial para a linguagem JavaScript se adaptar a vários ambientes de execução.
Por padrão, o Node.js trata os arquivos com o sufixo .js como sendo escritos usando a sintaxe CommonJS. Se você usar a sintaxe ESM diretamente no arquivo .js, o interpretador reportará um erro.
Existem três maneiras de converter o interpretador Node.js para sintaxe ESM:
1. Altere a extensão do arquivo para .mjs;
2. Adicione um campo de tipo ao arquivo package.json mais recente com o valor "módulo";
3. A string é passada para --eval
como um parâmetro ou transmitida ao nó através do canal STDIN com o sinalizador --input-type=module
por exemplo:
node --input-type=module --eval "importar {setembro} de 'node:caminho'; console.log(setembro);"
O ESM pode ser analisado e armazenado em cache como uma URL (o que também significa que os caracteres especiais devem ser codificados em porcentagem). Suporta protocolos de URL como file:
node:
e data:
arquivo:URL
O módulo será carregado diversas vezes se o especificador de importação usado para resolver o módulo tiver consultas ou fragmentos diferentes
// Considerados dois módulos diferentes import './foo.mjs?query=1'; importar './foo.mjs?query=2';
dados:URL
Suporta importação usando tipos MIME:
text/javascript
para módulos ES
application/json
para JSON
application/wasm
para Wasm
importar 'dados:text/javascript,console.log("olá!");'; import _ from 'data:application/json,"mundo!"' assert { type: 'json' };
data:URL
analisa apenas especificadores simples e absolutos para módulos integrados. A análise de especificadores relativos não funciona porque data:
não é um protocolo especial e não tem conceito de análise relativa.
Asserção de importação <br/>Este atributo adiciona sintaxe embutida à instrução de importação do módulo para passar mais informações ao lado do especificador do módulo.
importar fooData de './foo.json' assert { tipo: 'json' }; const { padrão: barData } = aguarda importação ('./bar.json', { assert: { type: 'json' } });
Atualmente apenas o módulo JSON é suportado e assert { type: 'json' }
é obrigatória.
Importando Módulos Wash <br/>A importação de módulos WebAssembly é suportada pelo sinalizador --experimental-wasm-modules
, permitindo que qualquer arquivo .wasm seja importado como um módulo normal, ao mesmo tempo que suporta a importação de seus módulos.
//index.mjs importar * como M de './module.wasm'; console.log(M)
Use o seguinte comando para executar:
nó --experimental-wasm-modules index.mjs
A palavra-chave await pode ser usada no nível superior no ESM.
//a.mjs exportar const cinco = aguardar Promise.resolve(5) // b.mjs importar {cinco} de './a.mjs' console.log(cinco) // 5
Conforme mencionado anteriormente, a resolução das dependências do módulo na instrução import é estática, portanto, tem duas limitações famosas:
Os identificadores de módulo não podem esperar até o tempo de execução para construí-los;
As instruções de importação de módulo devem ser escritas na parte superior do arquivo e não podem ser aninhadas em instruções de fluxo de controle;
No entanto, para algumas situações, estas duas restrições são, sem dúvida, demasiado rigorosas. Por exemplo, existe um requisito relativamente comum: carregamento lento :
Ao encontrar um módulo grande, você só deseja carregá-lo quando realmente precisar usar uma determinada função no módulo.
Para este propósito, o ESM fornece um mecanismo de introdução assíncrono. Esta operação de introdução pode ser realizada através import()
quando o programa está em execução. Do ponto de vista sintático, é equivalente a uma função que recebe um identificador de módulo como parâmetro e retorna uma Promessa. Depois que a Promessa é resolvida, o objeto do módulo analisado pode ser obtido.
Use um exemplo de dependência circular para ilustrar o processo de carregamento do ESM:
//index.js importar * como foo de './foo.js'; importar * como barra de './bar.js'; console.log(foo); console.log(barra); //foo.js importar * como barra de './bar.js' exportar deixe carregado = falso; exportar const bar = Barra; carregado = verdadeiro; //bar.js importar * como Foo de './foo.js'; exportar deixe carregado = falso; exportar const foo = Foo; carregado = verdadeiro
Vamos dar uma olhada nos resultados em execução primeiro:
Pode-se observar através de carregado que ambos os módulos foo e bar podem registrar as informações completas do módulo carregado. Mas CommonJS é diferente. Deve haver um módulo que não possa imprimir sua aparência depois de totalmente carregado.
Vamos mergulhar no processo de carregamento para ver por que esse resultado ocorre.
O processo de carregamento pode ser dividido em três etapas:
A primeira etapa: análise
Segunda etapa: declaração
A terceira etapa: execução
Estágio de análise:
O interpretador inicia a partir do arquivo de entrada (ou seja, index.js), analisa as dependências entre os módulos e as exibe na forma de um gráfico. Este gráfico também é chamado de gráfico de dependência.
Nesta fase, focamos apenas nas instruções de importação e carregamos o código-fonte correspondente aos módulos que essas instruções desejam introduzir. E obtenha o gráfico de dependência final por meio de uma análise aprofundada. Veja o exemplo acima para ilustrar:
1. Começando em index.js, encontre import * as foo from './foo.js'
e vá para o arquivo foo.js.
2. Continue analisando o arquivo foo.js e encontre import * as Bar from './bar.js'
, indo assim para bar.js.
3. Continue analisando bar.js e encontre import * as Foo from './foo.js'
, que forma uma dependência circular. No entanto, como o interpretador já está processando o módulo foo.js, ele não entrará nele. novamente e continue analisando o módulo da barra.
4. Após analisar o módulo bar, verifica-se que não há instrução de importação, então ele retorna para foo.js e continua a análise. A instrução de importação não foi encontrada novamente e index.js foi retornado.
5. import * as bar from './bar.js'
é encontrado em index.js, mas como bar.js já foi analisado, ele é ignorado e continua a execução.
Finalmente, o gráfico de dependência é completamente exibido por meio da abordagem em profundidade:
Fase de declaração:
O intérprete começa a partir do gráfico de dependência obtido e declara cada módulo em ordem, de baixo para cima. Especificamente, toda vez que um módulo é alcançado, todas as propriedades a serem exportadas pelo módulo são pesquisadas e os identificadores dos valores exportados são declarados na memória. Observe que nesta fase apenas são feitas declarações e nenhuma operação de atribuição é realizada.
1. O interpretador inicia no módulo bar.js e declara os identificadores de carregado e foo.
2. Rastreie até o módulo foo.js e declare os identificadores carregado e de barra.
3. Chegamos ao módulo index.js, mas este módulo não possui instrução de exportação, portanto nenhum identificador é declarado.
Depois de declarar todos os identificadores de exportação, percorra novamente o gráfico de dependência para conectar o relacionamento entre importação e exportação.
Pode-se observar que uma relação vinculativa semelhante a const é estabelecida entre o módulo introduzido pela importação e o valor exportado pela exportação. O lado importador só pode ler, mas não escrever. Além disso, o módulo bar lido em index.js e o módulo bar lido em foo.js são essencialmente a mesma instância.
É por isso que os resultados completos da análise são exibidos nos resultados deste exemplo.
Isto é fundamentalmente diferente da abordagem usada pelo sistema CommonJS. Se um módulo importar um módulo CommonJS, o sistema copiará todo o objeto de exportação deste último e copiará seu conteúdo para o módulo atual. Nesse caso, se o módulo importado modificar sua própria variável de cópia, o usuário não poderá ver o novo valor. .
Fase de execução:
Nesta fase, o motor executará o código do módulo. O gráfico de dependência ainda é acessado de baixo para cima e os arquivos acessados são executados um por um. A execução começa no arquivo bar.js, em foo.js e, finalmente, em index.js. Nesse processo, o valor do identificador na tabela de exportação é aprimorado gradativamente.
Este processo não parece ser muito diferente do CommonJS, mas na verdade existem diferenças importantes. Como CommonJS é dinâmico, ele analisa o gráfico de dependência enquanto executa arquivos relacionados. Portanto, contanto que você veja uma instrução require, você pode ter certeza de que quando o programa chegar a essa instrução, todos os códigos anteriores foram executados. Portanto, a instrução require não precisa necessariamente aparecer no início do arquivo, mas pode aparecer em qualquer lugar, e os identificadores de módulo também podem ser construídos a partir de variáveis.
Mas o ESM é diferente. No ESM, os três estágios acima são separados um do outro. Ele deve primeiro construir completamente o gráfico de dependência antes de poder executar o código. não espere até que o código seja executado.
Além das diversas diferenças mencionadas anteriormente, existem algumas diferenças dignas de nota:
Ao usar a palavra-chave import no ESM para resolver especificadores relativos ou absolutos, a extensão do arquivo deve ser fornecida e o índice do diretório ('./path/index.js') deve ser totalmente especificado. A função require do CommonJS permite que esta extensão seja omitida.
O ESM é executado no modo estrito por padrão e esse modo estrito não pode ser desativado. Portanto, você não pode usar variáveis não declaradas, nem recursos que estão disponíveis apenas no modo não estrito (como with).
CommonJS fornece algumas variáveis globais. Essas variáveis não podem ser usadas no ESM. Se você tentar usar essas variáveis, ocorrerá um ReferenceError. incluir
require
exports
module.exports
__filename
__dirname
Entre eles, __filename
refere-se ao caminho absoluto do arquivo do módulo atual e __dirname
é o caminho absoluto da pasta onde o arquivo está localizado. Essas duas variáveis são muito úteis ao construir o caminho relativo do arquivo atual, portanto o ESM fornece alguns métodos para implementar as funções das duas variáveis.
No ESM, você pode usar o objeto import.meta
para obter uma referência, que se refere ao URL do arquivo atual. Especificamente, o caminho do arquivo do módulo atual é obtido por meio de import.meta.url
. O formato deste caminho é semelhante a file:///path/to/current_module.js
. Com base neste caminho, o caminho absoluto expresso por __filename
e __dirname
é construído:
importar {fileURLToPath} de 'url' importar { nome do diretório } de 'caminho' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename)
Também pode simular a função require() em CommonJS
importar {createRequire} do 'módulo' const require = createRequire(import.meta.url)
No escopo global do ESM, isso é indefinido, mas no sistema do módulo CommonJS, é uma referência às exportações:
//ESM console.log(this) // indefinido //CommonJS console.log(this === exporta) // verdadeiro
Conforme mencionado acima, a função require() do CommonJS pode ser simulada no ESM para carregar módulos CommonJS. Além disso, você também pode usar a sintaxe de importação padrão para introduzir módulos CommonJS, mas este método de importação só pode importar itens exportados por padrão:
import packageMain from 'commonjs-package' // É completamente possível importar { método } de 'commonjs-package' // Erro
O require do módulo CommonJS sempre trata os arquivos aos quais ele faz referência como CommonJS. O carregamento de módulos ES usando require não é suportado porque os módulos ES têm execução assíncrona. Mas você pode usar import()
para carregar módulos ES de módulos CommonJS.
Embora o ESM tenha sido lançado há 7 anos, o node.js também o oferece suporte estável. Quando desenvolvemos bibliotecas de componentes, só podemos oferecer suporte ao ESM. Mas para ser compatível com projetos antigos, o suporte ao CommonJS também é essencial. Existem dois métodos amplamente utilizados para fazer com que uma biblioteca de componentes suporte exportações de ambos os sistemas de módulos.
Escreva pacotes em CommonJS ou converta o código-fonte do módulo ES em CommonJS e crie arquivos wrapper do módulo ES que definem exportações nomeadas. Use exportação condicional, importação usa o wrapper do módulo ES e require usa o ponto de entrada CommonJS. Por exemplo, no módulo de exemplo
//pacote.json { "tipo": "módulo", "exportações": { "importar": "./wrapper.mjs", "require": "./index.cjs" } }
Use extensões de exibição .cjs
e .mjs
, porque usar apenas .js
será o padrão CommonJS ou "type": "module"
fará com que esses arquivos sejam tratados como módulos ES.
// ./index.cjs exportar.nome = 'nome'; // ./wrapper.mjs importar cjsModule de './index.cjs' exportar nome const = cjsModule.name;
Neste exemplo:
// Use ESM para introduzir import { name } from 'example' // Use CommonJS para introduzir const { name } = require('example')
O nome introduzido em ambas as formas é o mesmo singleton.
O arquivo package.json pode definir diretamente pontos de entrada separados dos módulos CommonJS e ES:
//pacote.json { "tipo": "módulo", "exportações": { "importar": "./index.mjs", "require": "./index.cjs" } }
Isso pode ser feito se as versões CommonJS e ESM do pacote forem equivalentes, por exemplo, porque uma é uma saída transpilada da outra e o gerenciamento de estado do pacote é cuidadosamente isolado (ou o pacote não tem estado);
O motivo pelo qual o status é um problema é porque as versões CommonJS e ESM do pacote podem ser usadas no aplicativo. Por exemplo, o código de referência do usuário pode importar a versão ESM, enquanto a dependência requer a versão CommonJS. Se isso acontecer, duas cópias do pacote serão carregadas na memória, portanto ocorrerão dois estados diferentes. Isso pode levar a erros difíceis de resolver.
Além de escrever pacotes sem estado (por exemplo, se o Math do JavaScript fosse um pacote, seria sem estado porque todos os seus métodos são estáticos), existem maneiras de isolar o estado para que ele possa ser usado em CommonJS e ESM potencialmente carregados. instâncias de pacote:
Se possível, inclua todos os estados no objeto instanciado. Por exemplo, a Data do JavaScript precisa ser instanciada para conter o estado, se for um pacote, será usado assim:
importar data de 'data'; const algumaData = new Data(); // someDate contém estado; Date não;
A palavra-chave new não é necessária; as funções do pacote podem retornar novos objetos ou modificar objetos passados para manter o estado fora do pacote.
Isole o estado em um ou mais arquivos CommonJS compartilhados entre as versões CommonJS e ESM de um pacote. Por exemplo, os pontos de entrada para CommonJS e ESM são index.cjs e index.mjs respectivamente:
//index.cjs estado const = require('./state.cjs') module.exportações.state = estado; //index.mjs importar estado de './state.cjs' exportar { estado }
Mesmo que o exemplo seja usado em um aplicativo via require e import, cada referência ao exemplo contém o mesmo estado e as alterações no estado por qualquer sistema de módulo serão aplicadas a ambos;
Se este artigo for útil para você, dê um like e apoie-o. Seu "curtir" é a motivação para continuar criando.
Este artigo cita as seguintes informações:
Documentação oficial do node.js.
Padrões de design Node.js.