Você deve saber que Javascript (melhor dizer EcmaScript) não especifica nenhuma função para ler e escrever arquivos.
Na verdade, Javascript é apenas a linguagem usada por muitos ambientes (o browser, ou NodeJS, são exemplos de ambientes) que oferecem mais objetos e funções para trabalhar.
Node foi o primeiro ambiente a oferecer uma forma de organizar código em módulos usando uma função especial chamada require()
. Como funciona? Vamos tentar implementá-lo de zero.
Aqui está um exemplo de require
no trabalho:
//test.jsmodule.exports = "Hello World";
//main.jsconst test = require("./test.js"); console.log(test)
Vamos escrever que require
função.
O que deve uma função require() fazer
a require
function is expected to:
>
- ler o conteúdo de um ficheiro javascript numa string
- avaliar esse código
- guardar a função/objecto exportado numa cache para uso posterior (só ler ficheiros uma vez)
Disclaimer
Não reconstruiremos o NodeJS inteiro num único post. Na verdade, eu não vou implementar muitas verificações e risadas do NodeJS, nós só estamos interessados em entender como as coisas funcionam.
Nós ainda vamos precisar da função real require
para carregar o módulo fs
. Eu não estou trapaceando, é só que este post tem que terminar mais cedo ou mais tarde 🙂
myRequire() function
here’s the code:
//file setup.jsconst fs = require('fs');myRequire.cache = Object.create(null); //(1)function myRequire(name) { if (!(name in myRequire.cache)) { let code = fs.readFileSync(name, 'utf8'); //(2) let module = {exports: {}}; //(3) myRequire.cache = module; //(4) let wrapper = Function("require, exports, module", code); //(5) wrapper(myRequire, module.exports, module); //(6) } return myRequire.cache.exports; //(7)}...
Did you forget to declare myRequire variable?
No. Em Javascript, funções declaradas com function
palavra-chave são avaliadas antes de qualquer outro código (funções são “hoisted”) para que possam ser referenciadas mesmo antes de serem declaradas.
Também, funções podem ter propriedades (isto é javascript!) para que você possa adicionar a propriedade cache
à função myRequire
(passo 1).
Finalmente estamos criando a propriedade cache
com Object.create
. Com esta função podemos especificar o protótipo do objeto, nós escolhemos não especificar um protótipo. Porquê? Desta forma não mexemos com outras funções ou propriedades declaradas pelo tempo de execução. Aqui está uma explicação.
Vamos voltar a myRequire
. Se o arquivo que estamos importando não está em cache, lemos o arquivo do disco (passo 2).
Então declaramos um objeto vazio module
com apenas uma propriedade, exports
(passo 3).
Adicionamos este módulo vazio ao cache, usando o nome do ficheiro como chave, e depois a magia acontece (passo 4).
O construtor de funções
No JS podemos avaliar um código js string de duas maneiras. A primeira maneira é via eval()
function, que é um pouco perigoso (desorganiza o escopo) então é altamente desencorajado usá-lo.
A segunda maneira de avaliar o código que temos em uma string é via o construtor Function
. Este construtor pega uma string com os argumentos e uma string com o código. Desta forma tudo tem seu próprio escopo e não confunde as coisas para os outros.
Então, basicamente estamos criando uma nova função com estas variáveis (passo 5): require
, exports
, e module
. Vamos pensar por um momento no primeiro exemplo deste post, o arquivo test.js
: torna-se
function(require, exports, module) { module.exports = "Hello World" }
e o segundo arquivo, main.js
:
function(require, exports, module) { const test = require("./test.js"); console.log(test) }
Variáveis que pareciam “globais” em arquivos são de fato passadas como argumentos de função.
Último passo: executando a função
Criamos (passo 6) uma variável wrapper
que contém uma função, mas a função nunca é executada. Fazemos isso na linha:
wrapper(myRequire, module.exports, module);
Nota que a segunda variável (que deveria ser exports
) é apenas um handle para module.exports
; os criadores do NodeJS pensaram que isso poderia ter ajudado a escrever menos código…
Quando o Node executa a função, tudo que foi “exportado” (sua API pública) fica ligado ao cache.
(Lembra-se da linha myRequire.cache = module;
? Quando foi encontrada pela primeira vez pelo compilador foi apontada para um objecto dummy { exports: {} }
; agora contém o seu módulo.)
NOTE. Como passamos
myRequire
para a função wrapper, podemos a partir de agora usarrequire
em nossos arquivos de teste, mas nossa necessidade é chamada. Adicione um console.log se você não confiar em mim 😉
Finally… myRequire
retorna o material export
ed que você declarou (passo 7), e que nós salvamos no cache para não termos que reavaliar este código novamente.
Considerações finais
Um exemplo deste código pode ser encontrado aqui, juntamente com alguns logs de console que explicam o que está acontecendo.
A idéia deste artigo vem da explicação desta função no capítulo 10 (Módulos). O livro (Javascript Eloquente) é excelente, mas eu tive a vontade de entender melhor, e tentar com um debugger, o que eu não consegui entender só com a minha mente.
Você definitivamente deveria ler o livro se você quiser entender melhor o javascript.
Tags: javascript – nodejs