Curso de introducción rápida a Node.js: entra para aprender
Hace dos años escribí un artículo que presenta el sistema de módulos: Comprensión del concepto de módulos front-end: CommonJs y ES6Module. El conocimiento de este artículo está dirigido a principiantes y es relativamente simple. Aquí también corregimos algunos errores del artículo:
[Módulo] y [Sistema de módulo] son dos cosas diferentes. Un módulo es una unidad de software y un sistema de módulos es un conjunto de sintaxis o herramientas. El sistema de módulos permite a los desarrolladores definir y utilizar módulos en proyectos.
La abreviatura de ECMAScript Module es ESM, o ESModule, no ES6Module.
El conocimiento básico sobre el sistema de módulos está casi cubierto en el artículo anterior, por lo que este artículo se centrará en los principios internos del sistema de módulos y una introducción más completa a las diferencias entre diferentes sistemas de módulos. El contenido del artículo anterior se encuentra en Este. No se volverá a repetir.
No todos los lenguajes de programación tienen un sistema de módulos incorporado, y JavaScript no tuvo un sistema de módulos durante mucho tiempo después de su nacimiento.
En el entorno del navegador, solo puede usar la etiqueta para introducir archivos de código no utilizados. Este método comparte un alcance global, que se puede decir que está lleno de problemas junto con el rápido desarrollo de la interfaz. ya no satisface las necesidades actuales. Antes de que apareciera el sistema de módulos oficial, la comunidad front-end creó su propio sistema de módulos de terceros. Los más utilizados son: definición de módulo asíncrono AMD , definición de módulo universal UMD , etc. Por supuesto, el más famoso es CommonJS .
Dado que Node.js es un entorno de ejecución de JavaScript, puede acceder directamente al sistema de archivos subyacente. Entonces los desarrolladores lo adoptaron e implementaron un sistema de módulos de acuerdo con las especificaciones de CommonJS.
Al principio, CommonJS solo se podía usar en la plataforma Node.js. Con la aparición de herramientas de empaquetado de módulos como Browserify y Webpack, CommonJS finalmente se puede ejecutar en el lado del navegador.
No fue hasta el lanzamiento de la especificación ECMAScript6 en 2015 que hubo un estándar formal para el sistema de módulos. El sistema de módulos construido de acuerdo con este estándar se llamó módulo ECMAScript (ESM). A partir de entonces, ESM comenzó a unificarse. el entorno Node.js y el entorno del navegador. Por supuesto, ECMAScript6 solo proporciona sintaxis y semántica. En cuanto a la implementación, depende de varios proveedores de servicios de navegador y desarrolladores de Node trabajar duro. Es por eso que tenemos el artefacto Babel que es la envidia de otros lenguajes de programación. Implementar un sistema de módulos no es una tarea fácil. Node.js solo tiene soporte relativamente estable para ESM en la versión 13.2.
Pero pase lo que pase, ESM es el "hijo" de JavaScript y ¡no hay nada de malo en aprenderlo!
En la era de la agricultura de tala y quema, JavaScript se usaba para desarrollar aplicaciones y los archivos de script solo podían introducirse a través de etiquetas de script. Uno de los problemas más graves es la falta de un mecanismo de espacio de nombres, lo que significa que cada script comparte el mismo alcance. Hay una mejor solución a este problema en la comunidad: módulo revelador
const miMódulo = (() => { const _privateFn = () => {} const_privateAttr = 1 devolver { función pública: () => {}, publicAttr: 2 } })() console.log(miMódulo) console.log(myModule.publicFn, myModule._privateFn)
Los resultados de ejecución son los siguientes:
Este patrón es muy simple: use IIFE para crear un alcance privado y use variables de retorno para exponerlo. No se puede acceder a las variables internas (como _privateFn, _privateAttr) desde el ámbito externo.
[módulo revelador] aprovecha estas características para ocultar información privada y exportar API que deberían estar expuestas al mundo exterior. El sistema de módulos posterior también se desarrolla basándose en esta idea.
Con base en las ideas anteriores, desarrolle un cargador de módulos.
Primero escriba una función que cargue el contenido del módulo, envuelva esta función en un ámbito privado y luego evalúela mediante eval() para ejecutar la función:
función loadModule (nombre de archivo, módulo, requerir) { constante envueltaSrc = `(función (módulo, exportaciones, requerir) { ${fs.readFileSync(nombre de archivo, 'utf8)} }(módulo, módulo.exportaciones, requerir)` evaluación (envueltoSrc) }
Al igual que [módulo revelador], el código fuente del módulo está incluido en una función. La diferencia es que también se pasa una serie de variables (módulo, módulo.exportaciones, requerir).
Vale la pena señalar que el contenido del módulo se lee mediante [readFileSync]. En términos generales, no debe utilizar la versión sincronizada al llamar a API que involucran el sistema de archivos. Pero esta vez es diferente, porque la carga de módulos a través del propio sistema CommonJs debe implementarse como una operación sincrónica para garantizar que se puedan introducir varios módulos en el orden de dependencia correcto.
Luego simule la función require(), cuya función principal es cargar el módulo.
función requerir (nombre del módulo) { id constante = require.resolve(nombredelmódulo) si (require.cache[id]) { devolver require.cache[id].exportaciones } // Módulo constante de metadatos del módulo = { exportaciones: {}, IDENTIFICACIÓN } //Actualizar caché require.cache[id] = módulo //Cargar módulo loadModule(id, módulo, require) // Devuelve las variables exportadas return module.exports } requerir.cache = {} require.resolve = (nombre del módulo) => { // Analiza la identificación completa del módulo según el nombre del módulo }
(1) Después de que la función recibe el nombre del módulo, primero analiza la ruta completa del módulo y la asigna a la identificación.
(2) Si cache[id]
es verdadero, significa que el módulo se ha cargado y el resultado del caché se devolverá directamente. (3) De lo contrario, se configurará un entorno para la primera carga. Específicamente, cree un objeto de módulo, que incluya exportaciones (es decir, contenido exportado) e identificación (la función es la anterior)
(4) Almacene en caché el módulo cargado por primera vez (5) Lea el código fuente del archivo fuente del módulo a través de loadModule (6) Finalmente, return module.exports
devuelve el contenido que desea exportar.
Al simular la función require, hay un detalle muy importante: la función require debe ser síncrona . Su función es solo devolver directamente el contenido del módulo y no utiliza el mecanismo de devolución de llamada. Lo mismo ocurre con require en Node.js. Por lo tanto, la operación de asignación para module.exports también debe ser sincrónica. Si se utiliza asíncrono, se producirán problemas:
// Algo salió mal setTimeout(() => { módulo.exportaciones = función () {} }, 1000)
El hecho de que require sea una función sincrónica tiene un impacto muy importante en la forma de definir módulos, porque nos obliga a usar solo código sincrónico al definir módulos, por lo que Node.js proporciona versiones sincrónicas de la mayoría de las API asincrónicas para este propósito.
Los primeros Node.js tenían una versión asincrónica de la función require, pero se eliminó rápidamente porque complicaría mucho la función.
ESM es parte de la especificación ECMAScript2015, que especifica un sistema de módulos oficial para que el lenguaje JavaScript se adapte a diversos entornos de ejecución.
De forma predeterminada, Node.js trata los archivos con un sufijo .js como si estuvieran escritos utilizando la sintaxis CommonJS. Si utiliza la sintaxis ESM directamente en el archivo .js, el intérprete informará un error.
Hay tres formas de convertir el intérprete de Node.js a la sintaxis de ESM:
1. Cambie la extensión del archivo a .mjs;
2. Agregue un campo de tipo al último archivo package.json con el valor "módulo";
3. La cadena se pasa a --eval
como parámetro o se transmite al nodo a través de la tubería STDIN con la bandera --input-type=module
Por ejemplo:
nodo --input-type=module --eval "importar {sep} desde 'nodo:ruta'; console.log(sep);"
ESM se puede analizar y almacenar en caché como una URL (lo que también significa que los caracteres especiales deben estar codificados en porcentaje). Admite protocolos URL como file:
node:
y data:
archivo: URL
El módulo se carga varias veces si el especificador de importación utilizado para resolver el módulo tiene diferentes consultas o fragmentos
// Se consideran dos módulos diferentes import './foo.mjs?query=1'; importar './foo.mjs?query=2';
datos: URL
Admite la importación utilizando tipos MIME:
text/javascript
para módulos ES
application/json
para JSON
application/wasm
para Wasm
importar 'datos:text/javascript,console.log("¡hola!");'; importar _ desde 'datos:aplicación/json,"mundo!"' afirmar {tipo: 'json' };
data:URL
solo analiza especificadores simples y absolutos para módulos integrados. El análisis de especificadores relativos no funciona porque data:
no es un protocolo especial y no tiene concepto de análisis relativo.
Aserción de importación
Este atributo agrega sintaxis en línea a la declaración de importación del módulo para pasar más información junto al especificador del módulo.
importar fooData desde './foo.json' afirmar {tipo: 'json'}; const {predeterminado: barData } = await import('./bar.json', { afirmar: {tipo: 'json' } });
Actualmente, solo se admite el módulo JSON y assert { type: 'json' }
es obligatoria.
Importación de módulos Wash
La importación de módulos WebAssembly se admite bajo el indicador --experimental-wasm-modules
, lo que permite importar cualquier archivo .wasm como un módulo normal y, al mismo tiempo, admite la importación de sus módulos.
// index.mjs importar * como M desde './module.wasm'; consola.log(M)
Utilice el siguiente comando para ejecutar:
nodo --experimental-wasm-modules index.mjs
La palabra clave await se puede utilizar en el nivel superior de ESM.
// a.mjs exportar constante cinco = esperar Promise.resolve(5) // b.mjs importar {cinco} desde './a.mjs' console.log(cinco) // 5
Como se mencionó anteriormente, la resolución de dependencias de módulos de la declaración de importación es estática, por lo que tiene dos limitaciones famosas:
Los identificadores de módulo no pueden esperar hasta el tiempo de ejecución para construirlos;
Las declaraciones de importación del módulo deben escribirse en la parte superior del archivo y no pueden anidarse en declaraciones de flujo de control;
Sin embargo, en algunas situaciones, estas dos restricciones son sin duda demasiado estrictas. Por ejemplo, existe un requisito relativamente común: carga diferida :
Cuando se encuentra con un módulo grande, solo desea cargar este módulo enorme cuando realmente necesita usar una determinada función en el módulo.
Para ello, ESM proporciona un mecanismo de introducción asíncrono. Esta operación de introducción se puede lograr mediante import()
cuando el programa se está ejecutando. Desde un punto de vista sintáctico, es equivalente a una función que recibe un identificador de módulo como parámetro y devuelve una Promesa. Una vez resuelta la Promesa, se puede obtener el objeto del módulo analizado.
Utilice un ejemplo de dependencia circular para ilustrar el proceso de carga de ESM:
// index.js importar * como foo desde './foo.js'; importar * como barra desde './bar.js'; consola.log(foo); consola.log(barra); // foo.js importar * como barra desde './bar.js' exportar dejar cargado = falso; exportar barra constante = Barra; cargado = verdadero; //bar.js importar * como Foo desde './foo.js'; exportar dejar cargado = falso; exportar const foo = Foo; cargado = verdadero
Primero echemos un vistazo a los resultados en ejecución:
Se puede observar al cargar que ambos módulos foo y bar pueden registrar la información completa del módulo cargado. Pero CommonJS es diferente. Debe haber un módulo que no puede imprimir su apariencia después de estar completamente cargado.
Profundicemos en el proceso de carga para ver por qué ocurre este resultado.
El proceso de carga se puede dividir en tres etapas:
La primera etapa: análisis.
Segunda etapa: declaración
La tercera etapa: ejecución.
Etapa de análisis:
El intérprete comienza desde el archivo de entrada (es decir, index.js), analiza las dependencias entre módulos y las muestra en forma de gráfico. Este gráfico también se denomina gráfico de dependencia.
En esta etapa, solo nos enfocamos en las declaraciones de importación y cargamos el código fuente correspondiente a los módulos que estas declaraciones quieren introducir. Y obtenga el gráfico de dependencia final mediante un análisis en profundidad. Tome el ejemplo anterior para ilustrar:
1. Comenzando desde index.js, busque import * as foo from './foo.js'
y vaya al archivo foo.js.
2. Continúe analizando desde el archivo foo.js y busque import * as Bar from './bar.js'
, yendo así a bar.js.
3. Continúe analizando desde bar.js y busque import * as Foo from './foo.js'
, que forma una dependencia circular. Sin embargo, dado que el intérprete ya está procesando el módulo foo.js, no ingresará en él. nuevamente y luego continúe. Analice el módulo de barra.
4. Después de analizar el módulo de barra, se descubre que no hay una declaración de importación, por lo que regresa a foo.js y continúa analizando. La declaración de importación no se volvió a encontrar por completo y se devolvió index.js.
5. import * as bar from './bar.js'
se encuentra en index.js, pero como bar.js ya ha sido analizado, se omite y continúa la ejecución.
Finalmente, el gráfico de dependencia se muestra completamente mediante un enfoque de profundidad primero:
Fase de declaración:
El intérprete comienza desde el gráfico de dependencia obtenido y declara cada módulo en orden de abajo hacia arriba. Específicamente, cada vez que se llega a un módulo, se buscan todas las propiedades que el módulo exportará y se declaran en la memoria los identificadores de los valores exportados. Tenga en cuenta que en esta etapa solo se realizan declaraciones y no se realizan operaciones de asignación.
1. El intérprete comienza desde el módulo bar.js y declara los identificadores de cargado y foo.
2. Vuelva a rastrear el módulo foo.js y declare los identificadores cargados y de barra.
3. Llegamos al módulo index.js, pero este módulo no tiene una declaración de exportación, por lo que no se declara ningún identificador.
Después de declarar todos los identificadores de exportación, recorra nuevamente el gráfico de dependencia para conectar la relación entre importación y exportación.
Se puede ver que se establece una relación vinculante similar a constante entre el módulo introducido por la importación y el valor exportado por la exportación. El lado importador solo puede leer pero no escribir. Además, el módulo de barra leído en index.js y el módulo de barra leído en foo.js son esencialmente la misma instancia.
Esta es la razón por la que los resultados completos del análisis se muestran en los resultados de este ejemplo.
Esto es fundamentalmente diferente del enfoque utilizado por el sistema CommonJS. Si un módulo importa un módulo CommonJS, el sistema copiará todo el objeto de exportación de este último y copiará su contenido al módulo actual. En este caso, si el módulo importado modifica su propia variable de copia, entonces el usuario no puede ver el nuevo valor. .
Fase de ejecución:
En esta etapa, el motor ejecutará el código del módulo. Todavía se accede al gráfico de dependencia en orden de abajo hacia arriba y los archivos a los que se accede se ejecutan uno por uno. La ejecución comienza desde el archivo bar.js, hasta foo.js y finalmente hasta index.js. En este proceso se va mejorando progresivamente el valor del identificador en la tabla de exportación.
Este proceso no parece ser muy diferente de CommonJS, pero en realidad existen diferencias importantes. Dado que CommonJS es dinámico, analiza el gráfico de dependencia mientras ejecuta archivos relacionados. Entonces, siempre que vea una declaración requerida, puede estar seguro de que cuando el programa llegue a esta declaración, se habrán ejecutado todos los códigos anteriores. Por lo tanto, la declaración require no necesariamente tiene que aparecer al principio del archivo, sino que puede aparecer en cualquier lugar, y los identificadores de módulo también se pueden construir a partir de variables.
Pero ESM es diferente. En ESM, las tres etapas anteriores están separadas entre sí. Primero debe construir completamente el gráfico de dependencia antes de poder ejecutar el código. Por lo tanto, las operaciones de introducción y exportación de módulos deben ser estáticas. No espere hasta que se ejecute el código.
Además de las diversas diferencias mencionadas anteriormente, existen algunas diferencias que vale la pena señalar:
Cuando se utiliza la palabra clave import en ESM para resolver especificadores relativos o absolutos, se debe proporcionar la extensión del archivo y se debe especificar completamente el índice del directorio ('./path/index.js'). La función require de CommonJS permite omitir esta extensión.
ESM se ejecuta en modo estricto de forma predeterminada y este modo estricto no se puede desactivar. Por lo tanto, no puede utilizar variables no declaradas ni funciones que solo están disponibles en modo no estricto (como con).
CommonJS proporciona algunas variables globales. Estas variables no se pueden usar en ESM. Si intenta usar estas variables, se producirá un error de referencia. incluir
require
exports
module.exports
__filename
__dirname
Entre ellos, __filename
se refiere a la ruta absoluta del archivo del módulo actual y __dirname
es la ruta absoluta de la carpeta donde se encuentra el archivo. Estas dos variables son muy útiles al construir la ruta relativa del archivo actual, por lo que ESM proporciona algunos métodos para implementar las funciones de las dos variables.
En ESM, puede utilizar el objeto import.meta
para obtener una referencia, que hace referencia a la URL del archivo actual. Específicamente, la ruta del archivo del módulo actual se obtiene a través de import.meta.url
. El formato de esta ruta es similar a file:///path/to/current_module.js
. Con base en esta ruta, se construye la ruta absoluta expresada por __filename
y __dirname
:
importar {fileURLToPath} desde 'url' importar {nombredirección} desde 'ruta' const __nombre de archivo = fileURLToPath(import.meta.url) const __dirname = dirname(__nombre de archivo)
También puede simular la función require() en CommonJS.
importar {createRequire} desde 'módulo' const requiere = crearRequire(import.meta.url)
En el ámbito global de ESM, esto no está definido, pero en el sistema de módulos CommonJS, es una referencia a las exportaciones:
//ESM console.log(this) // indefinido // comúnJS console.log(esto === exporta) // verdadero
Como se mencionó anteriormente, la función CommonJS require() se puede simular en ESM para cargar módulos CommonJS. Además, también puede utilizar la sintaxis de importación estándar para introducir módulos CommonJS, pero este método de importación solo puede importar elementos exportados de forma predeterminada:
importar paqueteMain desde 'commonjs-package' // Es completamente posible importar {método} desde 'commonjs-package' // Error
El requisito del módulo CommonJS siempre trata los archivos a los que hace referencia como CommonJS. No se admite la carga de módulos ES mediante require porque los módulos ES tienen ejecución asincrónica. Pero puedes usar import()
para cargar módulos ES desde módulos CommonJS.
Aunque ESM se lanzó durante 7 años, node.js también lo admite de manera estable. Cuando desarrollamos bibliotecas de componentes, solo podemos admitir ESM. Pero para que sea compatible con proyectos antiguos, el soporte para CommonJS también es esencial. Hay dos métodos ampliamente utilizados para hacer que una biblioteca de componentes admita exportaciones desde ambos sistemas de módulos.
Escriba paquetes en CommonJS o convierta el código fuente del módulo ES a CommonJS y cree archivos contenedores de módulos ES que definan exportaciones con nombre. Utilice la exportación condicional, la importación utiliza el contenedor del módulo ES y require utiliza el punto de entrada CommonJS. Por ejemplo, en el módulo de ejemplo
// paquete.json { "tipo": "módulo", "exportaciones": { "importar": "./wrapper.mjs", "requerir": "./index.cjs" } }
Utilice las extensiones de visualización .cjs
y .mjs
, porque al usar solo .js
el valor predeterminado será CommonJS o "type": "module"
hará que estos archivos se traten como módulos ES.
// ./index.cjs exportar.nombre = 'nombre'; // ./wrapper.mjs importar cjsModule desde './index.cjs' exportar nombre constante = cjsModule.name;
En este ejemplo:
// Usa ESM para introducir import {nombre} del 'ejemplo' // Usa CommonJS para introducir const {nombre} = require('ejemplo')
El nombre introducido en ambos sentidos es el mismo singleton.
El archivo package.json puede definir directamente puntos de entrada de módulos CommonJS y ES separados:
// paquete.json { "tipo": "módulo", "exportaciones": { "importar": "./index.mjs", "requerir": "./index.cjs" } }
Esto se puede hacer si las versiones CommonJS y ESM del paquete son equivalentes, por ejemplo, porque una es una salida transpilada de la otra y la administración del estado del paquete está cuidadosamente aislada (o el paquete no tiene estado);
El motivo por el que el estado es un problema se debe a que se pueden usar las versiones CommonJS y ESM del paquete en la aplicación; por ejemplo, el código de referencia del usuario puede importar la versión ESM, mientras que la dependencia requiere la versión CommonJS. Si esto sucede, se cargarán dos copias del paquete en la memoria, por lo que se producirán dos estados diferentes. Esto puede provocar errores difíciles de resolver.
Además de escribir paquetes sin estado (por ejemplo, si Math de JavaScript fuera un paquete, sería sin estado porque todos sus métodos son estáticos), hay formas de aislar el estado para que pueda usarse en CommonJS y ESM potencialmente cargados. Compartirlo entre instancias de paquetes:
Si es posible, incluya todos los estados en el objeto instanciado. Por ejemplo, se debe crear una instancia de la fecha de JavaScript para que contenga el estado; si es un paquete, se usará así:
importar fecha desde 'fecha'; const algunaFecha = nueva Fecha(); // alguna fecha contiene estado; la fecha no
La nueva palabra clave no es obligatoria; las funciones del paquete pueden devolver nuevos objetos o modificar objetos pasados para mantener el estado fuera del paquete.
Aislar el estado en uno o más archivos CommonJS compartidos entre las versiones CommonJS y ESM de un paquete. Por ejemplo, los puntos de entrada para CommonJS y ESM son index.cjs e index.mjs respectivamente:
// index.cjs estado constante = requerir('./state.cjs') módulo.exportaciones.estado = estado; // index.mjs importar estado desde './state.cjs' exportar { estado }
Incluso si el ejemplo se usa en una aplicación mediante require e import, cada referencia al ejemplo contiene el mismo estado y los cambios en el estado realizados por cualquiera de los sistemas de módulos se aplicarán a ambos;
Si este artículo te resulta útil, dale un me gusta y apóyalo. Tu "me gusta" es la motivación para seguir creando.
Este artículo cita la siguiente información:
documentación oficial de node.js
Patrones de diseño de Node.js