Aprendamos cómo funcionan los paquetes de módulos y luego escribamos uno nosotros mismos

¡Hola! Bienvenidos, bienvenidos, ¡es genial tenerte aquí! Hoy vamos a crear un paquete de módulos JavaScript realmente simple.

Antes de empezar, quiero dar algunos agradecimientos. Este artículo se basa en gran medida en los siguientes recursos:

  • Desagregando el paquete de módulos JavaScript - Luciano Mammino
  • Minipack - Ronen Amiel

Bien, comencemos con lo que es realmente un paquete de módulos.

¿Qué es un paquete de módulos?

Un empaquetador de módulos es una herramienta que toma partes de JavaScript y sus dependencias y las agrupa en un solo archivo, generalmente para su uso en el navegador. Es posible que haya utilizado herramientas como Browserify, Webpack, Rollup o una de muchas otras.

Por lo general, comienza con un archivo de entrada y, a partir de ahí, agrupa todo el código necesario para ese archivo de entrada.

Hay dos etapas principales de un agrupador:

  1. Resolución de dependencia
  2. Embalaje

A partir de un punto de entrada (como el app.jsanterior), el objetivo de la resolución de dependencias es buscar todas las dependencias de su código (otras piezas de código que necesita para funcionar) y construir un gráfico (llamado gráfico de dependencia).

Una vez hecho esto, puede empaquetar o convertir su gráfico de dependencia en un solo archivo que la aplicación puede usar.

Comencemos nuestro código con algunas importaciones (explicaré el motivo más adelante).

Resolución de dependencia

Lo primero que tenemos que hacer es pensar cómo queremos representar un módulo durante la fase de resolución de dependencias.

Representación del módulo

Vamos a necesitar cuatro cosas:

  • El nombre y un identificador del archivo.
  • De dónde vino el archivo (en el sistema de archivos)
  • El código en el archivo
  • Qué dependencias necesita ese archivo

La estructura del gráfico se construye mediante la verificación recursiva de las dependencias dentro de cada archivo.

En JavaScript, la forma más sencilla de representar tal conjunto de datos sería un objeto.

Mirando la createModuleObjectfunción anterior, la parte notable es la llamada a una función llamada detective.

Detective es una biblioteca que puede " encontrar todas las llamadas a require () sin importar cuán profundamente anidadas " , y su uso significa que podemos evitar hacer nuestro propio recorrido AST.

Una cosa a tener en cuenta (y esto es lo mismo en casi todos los paquetes de módulos) es que si intenta hacer algo extraño como:

const libName = 'lodash'const lib = require(libName)

No podrá encontrarlo (porque eso significaría ejecutar el código).

Entonces, ¿qué proporciona ejecutar esta función desde la ruta de un módulo?

¿Que sigue? Resolución de dependencia.

De acuerdo, todavía no. Primero, quiero hablar sobre algo llamado mapa de módulos.

Mapa del módulo

Cuando importa módulos en Node, puede realizar importaciones relativas, como require('./utils'). Entonces, cuando su código llama a esto, ¿cómo sabe el empaquetador cuál es el ./utilsarchivo correcto cuando todo está empaquetado?

Ese es el problema que resuelve el mapa de módulos.

Nuestro objeto de módulo tiene una idclave única que será nuestra "fuente de verdad". Entonces, cuando estemos haciendo nuestra resolución de dependencia, para cada módulo, mantendremos una lista de los nombres de lo que se requiere junto con su identificación. De esta manera, podemos obtener el módulo correcto en tiempo de ejecución.

Esto también significa que podemos almacenar todos los módulos en un objeto no anidado, usando el id como clave.

Resolución de dependencia

Bien, entonces hay una buena cantidad de cosas en la getModulesfunción. Su propósito principal es comenzar en el módulo raíz / entrada y buscar y resolver dependencias de forma recursiva.

¿Qué quiero decir con "resolver dependencias"? En Node hay una cosa llamada require.resolve, y así es como Node descubre dónde está el archivo que está solicitando. Esto se debe a que podemos importar relativamente o desde una node_modulescarpeta.

Por suerte para nosotros, hay un módulo npm llamado resolveque implementa este algoritmo para nosotros. Solo tenemos que pasar la dependencia y los argumentos de la URL base, y hará todo el trabajo duro por nosotros.

Necesitamos llevar a cabo esta resolución para cada dependencia de cada módulo del proyecto.

También estamos creando el mapa de módulos llamado mapque mencioné anteriormente.

Al final de la función, nos queda una matriz nombrada modulesque contendrá objetos de módulo para cada módulo / dependencia en nuestro proyecto.

Ahora que lo tenemos, podemos pasar al paso final: ¡empacar!

Embalaje

En el navegador, no existen los módulos (más o menos). Pero esto significa que no hay función requerida, y no module.exports. Entonces, aunque tenemos todas nuestras dependencias, actualmente no tenemos forma de usarlas como módulos.

Función de fábrica del módulo

Ingrese a la función de fábrica.

Una función de fábrica es una función (que no es un constructor) que devuelve un objeto. Es un patrón de la programación orientada a objetos, y uno de sus usos es hacer encapsulación e inyección de dependencias.

¿Suena bien?

Usando una función de fábrica, podemos inyectar nuestra propia requirefunción y module.exportsobjeto que se pueden usar en nuestro código empaquetado y darle al módulo su propio alcance.

Embalaje

La siguiente es la función de empaque que se utiliza para empaquetar.

La mayor parte son solo plantillas literales de JavaScript, así que analicemos lo que está haciendo.

Primero es modulesSource. Aquí, estamos revisando cada uno de los módulos y transformándolos en una cadena de fuentes.

Entonces, ¿cómo es la salida para un objeto de módulo?

Ahora es un poco difícil de leer, pero puede ver que la fuente está encapsulada. Estamos proporcionando modulesy requireutilizando la función de fábrica como mencioné antes.

También estamos incluyendo el mapa de módulos que construimos durante la resolución de dependencia.

A continuación, en la función, unimos todos estos para crear un gran objeto de todas las dependencias.

La siguiente cadena de código es un IIFE, lo que significa que cuando ejecuta ese código en el navegador (o en cualquier otro lugar), la función se ejecutará inmediatamente. IIFE es otro patrón para encapsular el alcance, y se usa aquí para no contaminar el alcance global con nuestros requiremódulos y.

Puede ver que estamos definiendo dos funciones obligatorias requirey localRequire.

Require acepta la identificación de un objeto de módulo, pero, por supuesto, el código fuente no se escribe con identificadores. En cambio, estamos usando la otra función localRequirepara tomar cualquier argumento que requieran los módulos y convertirlos al id correcto. Esto está usando esos mapas de módulos.

Después de esto, estamos definiendo un module objectque el módulo puede poblar, y pasando ambas funciones a la fábrica, luego de lo cual regresamos module.exports.

Por último, llamamos require(0)para requerir el módulo con una identificación de 0, que es nuestro archivo de entrada.

¡Y eso es! ¡Nuestro paquete de módulos está 100% completo!

¡Felicidades! ?

Entonces ahora tenemos un paquete de módulos en funcionamiento.

Esto probablemente no debería usarse en producción, porque le faltan muchas características (como administrar dependencias circulares, asegurarse de que cada archivo se analice solo una vez, es-modules, etc.) pero es de esperar que esto le haya dado una buena idea de cómo los paquetes de módulos realmente funcionan.

De hecho, este funciona en aproximadamente 60 líneas si elimina todo el código fuente.

Gracias por leer y espero que haya disfrutado de un vistazo al funcionamiento de nuestro sencillo paquete de módulos. Si lo hizo, asegúrese de aplaudir. y compartir.

Este artículo se publicó originalmente en mi blog.

Consulte la fuente //github.com/adamisntdead/wbpck-bundler