Exploração de tecnologia front-end - Princípio de implementação da especificação CommonJS do Nodejs | JD Logistics Technology Team

Aprenda sobre Node.js

Node.js é um ambiente de execução de JavaScript baseado no mecanismo ChromeV8. Ele usa um modelo de E/S sem bloqueio e orientado a eventos para permitir que o JavaScript seja executado no lado do servidor. Ele permite que o JavaScript se torne um serviço com PHP, Python, Perl, Ruby, etc. Uma linguagem de script equivalente às linguagens de terminal. Muitos módulos integrados foram adicionados ao Node para fornecer uma variedade de funções, e muitos módulos de terceiros também são fornecidos.

problema do módulo

Por que ter módulos?

Projetos front-end complexos precisam ser dispostos em camadas e divididos em módulos de acordo com funções, negócios e componentes. Projetos modulares têm pelo menos as seguintes vantagens:

  1. Facilita testes unitários
  2. Facilite a colaboração entre colegas
  3. Extrair métodos públicos torna o desenvolvimento mais rápido
  4. Carga sob demanda, excelente desempenho
  5. Alta coesão e baixo acoplamento
  6. Evite conflitos de variáveis
  7. Facilite a manutenção do projeto de código

Várias especificações modulares

  • CMD (SeaJS implementa CMD)
  • AMD (RequireJS implementa AMD)
  • UMD (suporta AMD e CMD)
  • IIFE (função autoexecutável)
  • CommonJS (o nó usa CommonJS)
  • Especificação do Módulo ES (solução de modularização oficial JS)

Módulos no nó

Especificação CommonJS adotada no Node

Princípio de implementação:

O Node lerá o arquivo, obterá o conteúdo para implementar a modularização e usará o método Require para referenciá-lo de forma síncrona.

Dicas: Qualquer arquivo js no Node é um módulo e todo arquivo é um módulo.

Tipos de módulo no Node

  1. Módulos integrados são módulos principais e não precisam ser instalados. Eles não requerem referências de caminho relativas no projeto. O próprio Node os fornece.
  2. Módulo de arquivo, um módulo de arquivo js escrito pelo próprio programador.
  3. Módulos de terceiros precisam ser instalados e não há necessidade de adicionar um caminho após a instalação.

Módulos integrados no Node

sistema de arquivos fs

Este módulo é necessário para operar arquivos

const path = require('path'); // 处理路径
const fs = require('fs'); // file system
// // 同步读取
let content = fs.readFileSync(path.resolve(__dirname, 'test.js'), 'utf8');
console.log(content);

let exists = fs.existsSync(path.resolve(__dirname, 'test1.js'));
console.log(exists);

processamento de caminho

const path = require('path'); // 处理路径


// join / resolve 用的时候可以混用

console.log(path.join('a', 'b', 'c', '..', '/'))

// 根据已经有的路径来解析绝对路径, 可以用他来解析配置文件
console.log(path.resolve('a', 'b', '/')); // resolve 不支持/ 会解析成根路径

console.log(path.join(__dirname, 'a'))
console.log(path.extname('1.js'))
console.log(path.dirname(__dirname)); // 解析父目录

vm executa código

Como uma string pode ser transformada em JS para execução?

1. avaliação

O escopo quando o código em eval é executado é o escopo atual. Ele pode acessar variáveis ​​locais em funções.

let test = 'global scope'
global.test1 = '123'
function b(){
  test = 'fn scope'
  eval('console.log(test)'); //local scope
  new Function('console.log(test1)')() // 123
  new Function('console.log(test)')() //global scope
}
b()

2.nova função

Quando new Function() cria uma função, ela não faz referência ao ambiente léxico atual, mas ao ambiente global. As variáveis ​​usadas nas expressões em Function são passadas em parâmetros ou valores globais.

A função pode obter variáveis ​​globais, portanto ainda pode ter poluição variável.

function getFn() {
  let value = "test"
  let fn = new Function('console.log(value)')
  return fn
}

getFn()()

global.a = 100 // 挂在到全局对象global上
new Function("console.log(a)")() // 100

3.vm

Nos dois métodos anteriores sempre enfatizamos um conceito, que é a poluição de variáveis.

A característica da VM é que ela não é afetada pelo ambiente, podendo-se dizer também que é um ambiente sandbox.

No Node, variáveis ​​globais são compartilhadas entre vários módulos, então tente não definir propriedades em globais.

Portanto, vm.runInThisContext pode acessar variáveis ​​globais em global , mas não pode acessar variáveis ​​personalizadas. vm.runInNewContext não pode acessar variáveis ​​globais ou personalizadas. Ele existe em um novo contexto de execução .

const vm = require('vm')
global.a = 1
// vm.runInThisContext("console.log(a)")
vm.runInThisContext("a = 100") // 沙箱,独立的环境
console.log(a) // 1
vm.runInNewContext('console.log(a)')
console.log(a) // a is not defined

Implementação modular de nó

O Node possui seu próprio mecanismo de modularização, cada arquivo é um módulo separado e segue a especificação CommonJS, ou seja, utiliza require para importar módulos e exportar módulos através de module.export.

O mecanismo de execução do módulo do nó também é muito simples. Na verdade, cada módulo é envolvido por uma camada de funções. Com o empacotamento das funções, o isolamento do escopo entre os códigos pode ser alcançado.

Primeiro imprimimos os argumentos diretamente em um arquivo js. O resultado é mostrado na figura abaixo. Vamos primeiro lembrar desses parâmetros.

console.log(arguments) // exports, require, module, __filename, __dirname

No Node, ele é exportado através de module.export e introduzido por require. Entre eles, require depende do módulo fs no nó para carregar o arquivo do módulo, e o que é lido através de fs.readFile é uma string.

Em javascrpt, você pode usar eval ou new Function para converter uma string em código js para execução. Mas como mencionado anteriormente, todos eles têm um problema fatal, que é a poluição variável .

Implementar o carregador de módulo necessário

Primeiro importe o módulo dependente path , fs , vm , e crie uma função Require , que recebe um parâmetro modulePath , indicando o caminho do arquivo a ser importado.

const path = require('path');
const fs = require('fs');
const vm = require('vm');
// 定义导入类,参数为模块路径
function Require(modulePath) {
   ...
}

Obtenha o caminho absoluto do módulo em Require, use fs para carregar o módulo, aqui use new Module para abstrair o conteúdo do módulo, use tryModuleLoad para carregar o conteúdo do módulo, Module e tryModuleLoad serão implementados posteriormente, o valor de retorno de Require deve ser o conteúdo do módulo, que é module.exports.

// 定义导入类,参数为模块路径
function Require(modulePath) {
    // 获取当前要加载的绝对路径
    let absPathname = path.resolve(__dirname, modulePath);
    // 创建模块,新建Module实例
    const module = new Module(absPathname);
    // 加载当前模块
    tryModuleLoad(module);
    // 返回exports对象
    return module.exports;
}

A implementação do Módulo consiste em criar um objeto de exportação para o módulo. Quando tryModuleLoad é executado, o conteúdo é adicionado às exportações . O id é o caminho absoluto do módulo.

// 定义模块, 添加文件id标识和exports属性
function Module(id) {
    this.id = id;
    // 读取到的文件内容会放在exports中
    this.exports = {};
}

O módulo do nó é executado em uma função. Aqui, o wrapper de atributo estático é montado no Módulo, que define a string desta função. O wrapper é uma matriz. O primeiro elemento da matriz é a parte do parâmetro da função, que inclui exports, module, Require, __dirname, __filename, são variáveis ​​globais comumente usadas em módulos.

O segundo parâmetro é o fim da função. Ambas as partes são strings. Ao usar, basta envolvê-las fora da string do módulo.

// 定义包裹模块内容的函数
Module.wrapper = [
    "(function(exports, module, Require, __dirname, __filename) {",
    "})"
]

_extensions é usado para usar diferentes métodos de carregamento para diferentes extensões de módulo.Por exemplo, os métodos de carregamento JSON e javascript são definitivamente diferentes. JSON usa JSON.parse para ser executado.

JavaScript usa vm.runInThisContext para ser executado. Você pode ver que fs.readFileSync passa em module.id. Ou seja, quando o módulo é definido, o id armazena o caminho absoluto do módulo. O conteúdo lido é uma string. Use Module.wrapper Wrapping é equivalente a envolver outra função fora deste módulo, realizando assim um escopo privado.

Use call para executar a função fn. O primeiro parâmetro altera a execução de this e passa-o para module.exports. Os parâmetros subsequentes são os parâmetros exports, module, Require, __dirname, __filename envolvidos na função. /

// 定义扩展名,不同的扩展名,加载方式不同,实现js和json
Module._extensions = {
    '.js'(module) {
        const content = fs.readFileSync(module.id, 'utf8');
        const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
        const fn = vm.runInThisContext(fnStr);
        fn.call(module.exports, module.exports, module, Require,__filename,__dirname);
    },
    '.json'(module) {
        const json = fs.readFileSync(module.id, 'utf8');
        module.exports = JSON.parse(json); // 把文件的结果放在exports属性上
    }
}

A função tryModuleLoad recebe o objeto do módulo, obtém o nome do sufixo do módulo por meio de path.extname e, em seguida, usa Module._extensions para carregar o módulo.

// 定义模块加载方法
function tryModuleLoad(module) {
    // 获取扩展名
    const extension = path.extname(module.id);
    // 通过后缀加载当前模块
    Module._extensions[extension](module); // 策略模式???
}

Neste ponto, o mecanismo de carregamento Require está basicamente concluído. Quando Require carrega um módulo, passe o nome do módulo e use path.resolve(__dirname, modulePath) no método Require para obter o caminho absoluto do arquivo. Em seguida, crie o objeto do módulo instanciando o novo Módulo, armazene o caminho absoluto do módulo no atributo id do módulo e crie o atributo exports no módulo como um objeto JSON.

Use o método tryModuleLoad para carregar o módulo, use path.extname em tryModuleLoad para obter a extensão do arquivo e, em seguida, execute o mecanismo de carregamento do módulo correspondente com base na extensão.

O módulo que eventualmente será carregado é montado em module.exports. Após a execução de tryModuleLoad, module.exports já existe, então basta retorná-lo diretamente.

A seguir, adicionamos cache ao módulo. Ou seja, quando o arquivo é carregado, o arquivo é colocado no cache. Ao carregar o módulo, primeiro verifique se ele existe no cache. Se existir, use-o diretamente. Se não existir, recarregue-o e depois coloque-o no cache após o carregamento.

// 定义导入类,参数为模块路径
function Require(modulePath) {
  // 获取当前要加载的绝对路径
  let absPathname = path.resolve(__dirname, modulePath);
  // 从缓存中读取,如果存在,直接返回结果
  if (Module._cache[absPathname]) {
      return Module._cache[absPathname].exports;
  }
  // 创建模块,新建Module实例
  const module = new Module(absPathname);
  // 添加缓存
  Module._cache[absPathname] = module;
  // 加载当前模块
  tryModuleLoad(module);
  // 返回exports对象
  return module.exports;
}

Função adicionada: omitir o nome do sufixo do módulo.

Adicione automaticamente um nome de sufixo ao módulo para carregar o módulo sem o nome do sufixo. Na verdade, se o arquivo não tiver um nome de sufixo, ele percorrerá todos os nomes de sufixo para ver se o arquivo existe.

// 定义导入类,参数为模块路径
function Require(modulePath) {
  // 获取当前要加载的绝对路径
  let absPathname = path.resolve(__dirname, modulePath);
  // 获取所有后缀名
  const extNames = Object.keys(Module._extensions);
  let index = 0;

  // 存储原始文件路径
  const oldPath = absPathname;
  function findExt(absPathname) {
      if (index === extNames.length) {
         return throw new Error('文件不存在');
      }
      try {
          fs.accessSync(absPathname);
          return absPathname;
      } catch(e) {
          const ext = extNames[index++];
          findExt(oldPath + ext);
      }
  }
  
  // 递归追加后缀名,判断文件是否存在
  absPathname = findExt(absPathname);
  // 从缓存中读取,如果存在,直接返回结果
  if (Module._cache[absPathname]) {
      return Module._cache[absPathname].exports;
  }
  // 创建模块,新建Module实例
  const module = new Module(absPathname);
  // 添加缓存
  Module._cache[absPathname] = module;
  // 加载当前模块
  tryModuleLoad(module);
  // 返回exports对象
  return module.exports;
}

Depuração de código-fonte

Podemos depurar Node.js através do VSCode

etapa

Crie o arquivo a.js

module.exports = 'abc'

1.Arquivo teste.js

let r = require('./a')

console.log(r)

1. Configurar a depuração é essencialmente configurar o arquivo .vscode/launch.json, e a essência desse arquivo é fornecer várias opções de entrada de comando de inicialização.

Alguns parâmetros comuns são os seguintes:

  • o programa controla o caminho do arquivo de inicialização (ou seja, arquivo de entrada)
  • O nome exibido no menu suspenso de nomes (o nome da entrada correspondente a este comando)
  • A solicitação é dividida em lançamento e anexação (o processo foi iniciado)
  • skipFiles especifica o código a ser ignorado durante a depuração em etapa única
  • runtimeExecutable define o arquivo executável do tempo de execução. O padrão é node. Ele pode ser definido como nodemon, ts-node, npm, etc.

Modifique launch.json, skipFiles especifica o código a ser ignorado pela depuração em uma única etapa

  1. Coloque um ponto de interrupção na frente da linha onde o método require no arquivo test.js está localizado.
  2. Execute a depuração e insira o método de entrada relacionado ao código-fonte

Classifique as etapas do código

1. Primeiro insira o método require: Module.prototype.require

2. Depure no método Module._load, que retorna module.exports. O método Module._resolveFilename retorna o endereço do arquivo após o processamento, altera o arquivo para um endereço absoluto e adiciona o sufixo do arquivo se o arquivo não tiver um sufixo.

3. A classe Module é definida aqui. id é o nome do arquivo. O atributo exports é definido nesta classe

4. Em seguida, depure para o método module.load, que usa o modo de estratégia. Module._extensions[extension](este, nome do arquivo) chama métodos diferentes de acordo com os diferentes nomes de sufixos de arquivo passados.

5. Insira o método, veja o código principal, leia o parâmetro de endereço do arquivo de entrada, obtenha o conteúdo da string no arquivo e execute module._compile

6. O método wrapSafe é executado neste método. Adicione sufixos de função antes e depois da string e use o método runInthisContext no módulo vm no Node para executar a string, que executará diretamente o conteúdo da linha de código console.log no arquivo de entrada.

Neste ponto, todo o código do processo para implementar o método require em todo o Node foi depurado. Ao depurar o código-fonte, ele pode nos ajudar a aprender suas idéias de implementação, estilo de código e especificações, ajudar-nos a implementar a biblioteca de ferramentas e melhorar nossas ideias de código., ao mesmo tempo, conhecemos os princípios relevantes e também nos ajuda a resolver os problemas que encontramos no trabalho diário de desenvolvimento.

Autor: JD Logística Qiao Panpan

Fonte: JD Cloud Developer Community Ziyuanqishuo Tech Por favor, indique a fonte ao reimprimir

 

OpenAI abre ChatGPT Voice Vite 5 gratuitamente para todos os usuários. É lançado oficialmente . Operação mágica da operadora: desconectar a rede em segundo plano, desativar contas de banda larga, forçar os usuários a trocar modems ópticos. Programadores de Terminal Chat de código aberto da Microsoft adulteraram saldos ETC e desviou mais de 2,6 milhões de yuans por ano. Usado pelo pai do Redis O código de linguagem Pure C implementa a estrutura do Telegram Bot. Se você é um mantenedor de projeto de código aberto, até onde pode suportar esse tipo de resposta? O Microsoft Copilot Web AI será lançado oficialmente em 1º de dezembro, apoiando o OpenAI chinês. O ex-CEO e presidente Sam Altman e Greg Brockman ingressaram na Microsoft. A Broadcom anunciou a aquisição bem-sucedida da VMware.
{{o.nome}}
{{m.nome}}

Acho que você gosta

Origin my.oschina.net/u/4090830/blog/10150940
Recomendado
Clasificación