Exploración de la tecnología front-end: principio de implementación de la especificación CommonJS de Nodejs | Equipo de tecnología de JD Logistics

Más información sobre Node.js

Node.js es un entorno de ejecución de JavaScript basado en el motor ChromeV8. Utiliza un modelo de E/S sin bloqueo y controlado por eventos para permitir que JavaScript se ejecute en el lado del servidor. Permite que JavaScript se convierta en un servicio con PHP, Python, Perl, Ruby, etc. Un lenguaje de scripting a la par de los lenguajes de terminal. Se han agregado muchos módulos integrados a Node para proporcionar una variedad de funciones, y también se proporcionan muchos módulos de terceros.

problema del módulo

¿Por qué tener módulos?

Los proyectos front-end complejos deben superponerse en capas y dividirse en módulos según funciones, negocios y componentes. Los proyectos modulares tienen al menos las siguientes ventajas:

  1. Facilita las pruebas unitarias
  2. Facilitar la colaboración entre colegas.
  3. Extraer métodos públicos acelera el desarrollo
  4. Carga bajo demanda, excelente rendimiento
  5. Alta cohesión y bajo acoplamiento.
  6. Prevenir conflictos variables
  7. Facilitar el mantenimiento del proyecto de código.

Varias especificaciones modulares

  • CMD (SeaJS implementa CMD)
  • AMD (RequireJS implementa AMD)
  • UMD (compatible con AMD y CMD)
  • IIFE (función autoejecutable)
  • CommonJS (El nodo usa CommonJS)
  • Especificación del módulo ES (solución de modularización oficial de JS)

Módulos en nodo

Especificación CommonJS adoptada en Node

Principio de implementación:

Node leerá el archivo, obtendrá el contenido para implementar la modularización y utilizará el método Require para hacer referencia a él de forma sincrónica.

Consejos: cualquier archivo js en Node es un módulo y cada archivo es un módulo.

Tipos de módulos en Node

  1. Los módulos integrados son módulos centrales y no necesitan ser instalados. No requieren referencias de rutas relativas en el proyecto. El propio Node los proporciona.
  2. Módulo de archivo, un módulo de archivo js escrito por el propio programador.
  3. Es necesario instalar módulos de terceros y no es necesario agregar una ruta después de la instalación.

Módulos integrados en Node

sistema de archivos fs

Este módulo es necesario para operar archivos.

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);

procesamiento de ruta de ruta

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 ejecuta código

¿Cómo se puede convertir una cadena en JS para su ejecución?

1.evaluación

El alcance cuando se ejecuta el código en eval es el alcance actual. Puede acceder a variables locales en funciones.

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.nueva función

Cuando new Function() crea una función, no hace referencia al entorno léxico actual, sino al entorno global. Las variables utilizadas en las expresiones en Function se pasan en parámetros o valores globales.

La función puede obtener variables globales, por lo que aún puede tener contaminación variable.

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

En los dos métodos anteriores, siempre hemos enfatizado un concepto, que es la contaminación de variables.

La característica de VM es que no se ve afectada por el entorno, también se puede decir que es un entorno sandbox.

En Node, las variables globales se comparten entre varios módulos, así que trate de no definir propiedades en global.

Por lo tanto, vm.runInThisContext puede acceder a variables globales en global , pero no puede acceder a variables personalizadas. vm.runInNewContext no puede acceder a variables globales o personalizadas. Existe en un contexto de ejecución completamente nuevo .

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

Implementación modular de nodo

Node tiene su propio mecanismo de modularización: cada archivo es un módulo separado y sigue la especificación CommonJS, es decir, utiliza require para importar módulos y exportar módulos a través de module.export.

El mecanismo de ejecución del módulo de nodo también es muy simple: de hecho, cada módulo está envuelto con una capa de funciones, y con la envoltura de funciones se puede lograr el aislamiento del alcance entre códigos.

Primero imprimimos los argumentos directamente en un archivo js. El resultado es como se muestra en la siguiente figura. Primero recordemos estos parámetros.

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

En Node, se exporta a través de module.export y se introduce mediante require. Entre ellos, require depende del módulo fs en el nodo para cargar el archivo del módulo, y lo que se lee a través de fs.readFile es una cadena.

En javascrpt, puede usar eval o new Función para convertir una cadena en código js para ejecutar. Pero como se mencionó antes, todos tienen un problema fatal que es la contaminación variable .

Implementar el cargador de módulos requerido

Primero importe la ruta del módulo dependiente , fs , vm y cree una función Requerir . Esta función recibe un parámetro modulePath , que indica la ruta del archivo que se importará.

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

Obtenga la ruta absoluta del módulo en Require, use fs para cargar el módulo, aquí use new Module para abstraer el contenido del módulo, use tryModuleLoad para cargar el contenido del módulo, Module y tryModuleLoad se implementarán más adelante, el valor de retorno de Require debe ser el contenido del módulo, es decir module.exports.

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

La implementación del Módulo es crear un objeto de exportación para el módulo. Cuando se ejecuta tryModuleLoad , el contenido se agrega a las exportaciones . La identificación es la ruta absoluta del módulo.

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

El módulo de nodo se ejecuta en una función. Aquí, el contenedor de atributos estáticos se monta en el módulo, que define la cadena de esta función. El contenedor es una matriz. El primer elemento de la matriz es la parte del parámetro de la función, que incluye exportaciones, módulo, Requerir, __dirname, __filename, son todas variables globales de uso común en los módulos.

El segundo parámetro es el final de la función. Ambas partes son cadenas. Cuando las use, simplemente envuélvalas fuera de la cadena del módulo.

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

_extensions se utiliza para utilizar diferentes métodos de carga para diferentes extensiones de módulo. Por ejemplo, los métodos de carga JSON y JavaScript son definitivamente diferentes. JSON usa JSON.parse para ejecutarse.

JavaScript usa vm.runInThisContext para ejecutarse. Puede ver que fs.readFileSync pasa en module.id. Es decir, cuando se define el módulo, la identificación almacena la ruta absoluta del módulo. El contenido leído es una cadena. Utilice Module.wrapper Envolverlo equivale a envolver otra función fuera de este módulo, logrando así un alcance privado.

Utilice llamada para ejecutar la función fn. El primer parámetro cambia la ejecución de esto y lo pasa a module.exports. Los parámetros siguientes son los parámetros exports, module, Require, __dirname, __filename envueltos alrededor de la función. /

// 定义扩展名,不同的扩展名,加载方式不同,实现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属性上
    }
}

La función tryModuleLoad recibe el objeto del módulo, obtiene el nombre del sufijo del módulo a través de path.extname y luego usa Module._extensions para cargar el módulo.

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

En este punto, el mecanismo de carga Requerir está básicamente terminado. Cuando Require carga un módulo, pase el nombre del módulo y use path.resolve(__dirname, modulePath) en el método Require para obtener la ruta absoluta del archivo. Luego cree el objeto del módulo creando una instancia del nuevo Módulo, almacene la ruta absoluta del módulo en el atributo de identificación del módulo y cree el atributo de exportaciones en el módulo como un objeto json.

Utilice el método tryModuleLoad para cargar el módulo, utilice path.extname en tryModuleLoad para obtener la extensión del archivo y luego ejecute el mecanismo de carga del módulo correspondiente según la extensión.

El módulo que finalmente se cargará está montado en module.exports. Después de ejecutar tryModuleLoad, module.exports ya existe, así que devuélvalo directamente.

A continuación, agregamos almacenamiento en caché al módulo. Es decir, cuando se carga el archivo, el archivo se coloca en el caché. Al cargar el módulo, primero verifique si existe en el caché. Si existe, úselo directamente. Si no existe, vuelva a cargarlo y luego póngalo en el caché después de cargarlo.

// 定义导入类,参数为模块路径
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;
}

Función agregada: omitir el nombre del sufijo del módulo.

Agregue automáticamente un nombre de sufijo al módulo para cargar el módulo sin el nombre del sufijo. De hecho, si el archivo no tiene un nombre de sufijo, recorrerá todos los nombres de sufijo para ver si el archivo 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;
}

Depuración del código fuente

Podemos depurar Node.js a través de VSCode

paso

Crear archivo a.js

module.exports = 'abc'

1.Archivo prueba.js

let r = require('./a')

console.log(r)

1. Configurar la depuración es esencialmente configurar el archivo .vscode/launch.json, y la esencia de este archivo es proporcionar múltiples opciones de entrada de comandos de inicio.

Algunos parámetros comunes son los siguientes:

  • El programa controla la ruta del archivo de inicio (es decir, el archivo de entrada).
  • El nombre que se muestra en el menú desplegable de nombres (el nombre de la entrada correspondiente a este comando)
  • La solicitud se divide en iniciar y adjuntar (el proceso ha comenzado)
  • skipFiles especifica el código que se debe omitir durante la depuración en un solo paso
  • runtimeExecutable establece el archivo ejecutable en tiempo de ejecución. El valor predeterminado es node. Se puede configurar en nodemon, ts-node, npm, etc.

Modifique launch.json, skipFiles especifica el código que se omitirá mediante la depuración en un solo paso

  1. Coloque un punto de interrupción delante de la línea donde se encuentra el método require en el archivo test.js.
  2. Ejecute la depuración e ingrese el método de entrada relacionado con el código fuente

Ordena los pasos del código

1. Primero ingrese el método requerido: Module.prototype.require

2. Depure en el método Module._load, que devuelve module.exports. El método Module._resolveFilename devuelve la dirección del archivo después del procesamiento, cambia el archivo a una dirección absoluta y agrega el sufijo del archivo si el archivo no tiene un sufijo.

3. La clase Módulo se define aquí. id es el nombre del archivo. El atributo de exportaciones está definido en esta clase.

4. Luego depure en el método module.load, que utiliza el modo de estrategia. Module._extensions[extensión](este, nombre de archivo) llama a diferentes métodos según los diferentes nombres de sufijo de archivo pasados.

5. Ingrese el método, vea el código principal, lea el parámetro de dirección del archivo entrante, obtenga el contenido de la cadena en el archivo y ejecute module._compile

6. El método wrapSafe se ejecuta en este método. Agregue sufijos de función antes y después de la cadena, y use el método runInthisContext en el módulo vm en Node para ejecutar la cadena, que ejecutará directamente el contenido de la línea de código console.log en el archivo entrante.

En este punto, se ha depurado todo el código de proceso para implementar el método require en todo el Nodo. ​​Al depurar el código fuente, puede ayudarnos a conocer sus ideas de implementación, estilo de código y especificaciones, ayudarnos a implementar la biblioteca de herramientas y mejorar nuestras ideas de código. Al mismo tiempo, conocemos los principios relevantes y también nos ayuda a resolver los problemas que encontramos en el trabajo de desarrollo diario.

Autor: JD Logística Qiao Panpan

Fuente: Comunidad de desarrolladores de JD Cloud Ziyuanqishuo Tech Indique la fuente al reimprimir

 

OpenAI abre ChatGPT Voice Vite 5 de forma gratuita para todos los usuarios. Se lanza oficialmente . La operación mágica del operador: desconectar la red en segundo plano, desactivar cuentas de banda ancha, obligar a los usuarios a cambiar de módem óptico. Los programadores de Terminal Chat de código abierto de Microsoft manipularon los saldos de ETC y malversó más de 2,6 millones de yuanes al año. Utilizado por el padre de Redis, el código en lenguaje Pure C implementa el marco Telegram Bot. Si usted es un mantenedor de proyectos de código abierto, ¿hasta dónde puede soportar este tipo de respuesta? Microsoft Copilot Web AI se lanzará oficialmente el 1 de diciembre, respaldando la OpenAI china. El ex CEO y presidente Sam Altman y Greg Brockman se unieron a Microsoft. Broadcom anunció la exitosa adquisición de VMware.
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

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