Módulos de JavaScript Parte 2: Agrupación de módulos

En la Parte I de esta publicación, hablé sobre qué son los módulos, por qué los desarrolladores los usan y las diversas formas de incorporarlos a sus programas.

En esta segunda parte, abordaré qué significa exactamente "agrupar" módulos: por qué agrupamos módulos, las diferentes formas de hacerlo y el futuro de los módulos en el desarrollo web.

¿Qué es la agrupación de módulos?

En un nivel alto, la agrupación de módulos es simplemente el proceso de unir un grupo de módulos (y sus dependencias) en un solo archivo (o grupo de archivos) en el orden correcto.

Como con todos los aspectos del desarrollo web, el diablo está en los detalles. :)

¿Por qué agrupar módulos?

Cuando divide su programa en módulos, normalmente organiza esos módulos en diferentes archivos y carpetas. Lo más probable es que también tenga un grupo de módulos para las bibliotecas que está utilizando, como Underscore o React.

Como resultado, cada uno de esos archivos debe incluirse en su archivo HTML principal en un pt> etiqueta, que luego es cargada por el navegador cuando un usuario visita su página de inicio. Tener etiquetas & l t; script> separadas para cada archivo significa que el navegador tiene que cargar cada archivo individualmente: uno ... por ... uno.

… Lo cual es una mala noticia para el tiempo de carga de la página.

Para solucionar este problema, agrupamos o “concatenamos” todos nuestros archivos en un archivo grande (o un par de archivos, según sea el caso) para reducir el número de solicitudes. Cuando escuchas a los desarrolladores hablar sobre el "paso de compilación" o el "proceso de compilación", de esto es de lo que están hablando.

Otro enfoque común para acelerar la operación de agrupación es "minificar" el código agrupado. La minificación es el proceso de eliminar caracteres innecesarios del código fuente (por ejemplo, espacios en blanco, comentarios, caracteres de nueva línea, etc.) para reducir el tamaño general del contenido sin cambiar la funcionalidad del código.

Menos datos significa menos tiempo de procesamiento del navegador, lo que a su vez reduce el tiempo necesario para descargar archivos. Si alguna vez ha visto un archivo que tiene una extensión "min" como "subrayado-min.js", probablemente haya notado que la versión minificada es bastante pequeña (e ilegible) en comparación con la versión completa.

Los ejecutores de tareas como Gulp y Grunt hacen que la concatenación y la minificación sean sencillas para los desarrolladores, lo que garantiza que el código legible por humanos permanezca expuesto para los desarrolladores mientras que el código optimizado para máquinas se incluye para los navegadores.

¿Cuáles son las diferentes formas de agrupar módulos?

Concatenar y minificar sus archivos funciona muy bien cuando usa uno de los patrones de módulo estándar (discutidos en la publicación anterior) para definir sus módulos. Todo lo que estás haciendo en realidad es combinar un montón de código JavaScript simple.

Sin embargo, si se está adhiriendo a sistemas de módulos no nativos que los navegadores no pueden interpretar como CommonJS o AMD (o incluso formatos de módulos nativos de ES6), necesitará utilizar una herramienta especializada para convertir sus módulos en un navegador ordenado correctamente. -código amigable. Ahí es donde entran en juego Browserify, RequireJS, Webpack y otros "paquetes de módulos" o "cargadores de módulos".

Además de empaquetar y / o cargar sus módulos, los empaquetadores de módulos ofrecen un montón de características adicionales como la recompilación automática de código cuando realiza un cambio o la producción de mapas fuente para depurar.

Veamos algunos métodos comunes de agrupación de módulos:

Empaquetado CommonJS

Como sabe de la Parte 1, CommonJS carga módulos de forma sincrónica, lo que estaría bien, excepto que no es práctico para los navegadores. Mencioné que había una solución a esto: una de ellas es un paquete de módulos llamado Browserify. Browserify es una herramienta que compila módulos CommonJS para el navegador.

Por ejemplo, digamos que tiene este archivo main.js que importa un módulo para calcular el promedio de una matriz de números:

Entonces, en este caso, tenemos una dependencia (myDependency). Con el comando a continuación, Browserify agrupa de forma recursiva todos los módulos necesarios a partir de main.js en un solo archivo llamado bundle.js:

Browserify hace esto saltando para analizar el AST para cada llamada requerida con el fin de recorrer todo el gráfico de dependencia de su proyecto. Una vez que ha averiguado cómo están estructuradas sus dependencias, las agrupa todas en el orden correcto en un solo archivo. En ese momento, todo lo que tiene que hacer es insertar un solo pt> etiqueta con su archivo "bundle.js" en su html para asegurarse de que todo su código fuente se descargue en una solicitud HTTP. ¡Bam! Lote para llevar.

De manera similar, si tiene varios archivos con múltiples dependencias, simplemente dígale a Browserify cuál es su archivo de entrada y siéntese mientras hace su magia.

El producto final: archivos empaquetados preparados y listos para herramientas como Minify-JS para minimizar el código empaquetado.

Agrupación de AMD

Si está usando AMD, querrá usar un cargador AMD como RequireJS o Curl. Un cargador de módulos (frente a un agrupador) carga dinámicamente los módulos que su programa necesita para ejecutarse.

Como recordatorio, una de las principales diferencias de AMD sobre CommonJS es que carga módulos de forma asincrónica. En este sentido, con AMD, técnicamente no necesita un paso de compilación en el que agrupe sus módulos en un archivo, ya que está cargando sus módulos de forma asincrónica, lo que significa que está descargando progresivamente solo aquellos archivos que son estrictamente necesarios para ejecutar el programa en lugar de descargar todos los archivos a la vez cuando el usuario visita la página por primera vez.

En realidad, sin embargo, la sobrecarga de solicitudes de gran volumen a lo largo del tiempo para cada acción del usuario no tiene mucho sentido en producción. La mayoría de los desarrolladores web todavía usan herramientas de compilación para agrupar y minimizar sus módulos AMD para un rendimiento óptimo, utilizando herramientas como el optimizador RequireJS, r.js, por ejemplo.

En general, la diferencia entre AMD y CommonJS cuando se trata de empaquetar es la siguiente: durante el desarrollo, las aplicaciones de AMD pueden escapar sin un paso de compilación. Al menos, hasta que envíe el código en vivo, momento en el que los optimizadores como r.js pueden intervenir para manejarlo.

Para una discusión interesante sobre CommonJS vs. AMD, consulte esta publicación en el blog de Tom Dale :)

Webpack

En lo que respecta a los paquetes, Webpack es el nuevo chico del barrio. Fue diseñado para ser independiente del sistema de módulos que usa, lo que permite a los desarrolladores usar CommonJS, AMD o ES6 según corresponda.

Quizás se pregunte por qué necesitamos Webpack cuando ya tenemos otros paquetes como Browserify y RequireJS que hacen el trabajo y lo hacen bastante bien. Bueno, por un lado, Webpack proporciona algunas características útiles como "división de código", una forma de dividir su base de código en "fragmentos" que se cargan a pedido.

Por ejemplo, si tiene una aplicación web con bloques de código que solo se requieren en determinadas circunstancias, es posible que no sea eficiente poner todo el código base en un solo archivo empaquetado masivo. En este caso, puede usar la división de código para extraer el código en fragmentos agrupados que se pueden cargar a pedido, evitando problemas con grandes cargas útiles iniciales cuando la mayoría de los usuarios solo necesitan el núcleo de su aplicación.

La división de código es solo una de las muchas características atractivas que ofrece Webpack, e Internet está lleno de artículos de opinión sólidos sobre si Webpack o Browserify es mejor. Estas son solo algunas de las discusiones más sensatas que encontré útiles para entender el tema:

  • //gist.github.com/substack/68f8d502be42d5cd4942
  • //mattdesl.svbtle.com/browserify-vs-webpack
  • //blog.namangoel.com/browserify-vs-webpack-js-drama

Módulos ES6

¿Ya regresaste? ¡Bueno! Porque a continuación quiero hablar sobre los módulos ES6, que de alguna manera podrían reducir la necesidad de paquetes en el futuro. (verá lo que quiero decir momentáneamente). Primero, entendamos cómo se cargan los módulos ES6.

La diferencia más importante entre los formatos de módulo JS actuales (CommonJS, AMD) y los módulos ES6 es que los módulos ES6 están diseñados teniendo en cuenta el análisis estático. Lo que esto significa es que cuando importa módulos, la importación se resuelve en tiempo de compilación, es decir, antes de que el script comience a ejecutarse. Esto nos permite eliminar las exportaciones que no utilizan otros módulos antes de ejecutar el programa. La eliminación de las exportaciones no utilizadas puede generar importantes ahorros de espacio, lo que reduce el estrés en el navegador.

Una pregunta común que surge es: ¿en qué se diferencia esto de la eliminación del código muerto que ocurre cuando usas algo como UglifyJS para minimizar tu código? La respuesta es, como siempre, "depende".

(NOTA: la eliminación del código muerto es un paso de optimización que elimina el código y las variables no utilizados; considérelo como eliminar el exceso de equipaje que su programa incluido no necesita ejecutar, * después * de que se haya incluido).

A veces, la eliminación del código muerto podría funcionar exactamente igual entre los módulos UglifyJS y ES6, y otras veces no. Hay un ejemplo genial en la wiki de Rollup) si quieres verlo.

Lo que hace que los módulos ES6 sean diferentes es el enfoque diferente para la eliminación de código muerto, llamado "sacudida de árboles". Sacudir árboles es esencialmente la eliminación del código muerto al revés. Solo incluye el código que su paquete necesita para ejecutar, en lugar de excluir el código que su paquete no necesita. Veamos un ejemplo de sacudidas de árboles:

Digamos que tenemos un archivo utils.js con las siguientes funciones, cada una de las cuales exportamos usando la sintaxis ES6:

A continuación, digamos que no sabemos qué funciones de utils queremos usar en nuestro programa, así que seguimos adelante e importamos todos los módulos en main.js así:

Y luego terminamos usando solo cada función:

La versión de "árbol sacudido" de nuestro archivo main.js se vería así una vez que se hayan cargado los módulos:

Observe cómo las únicas exportaciones incluidas son las que usamos: cada una .

Mientras tanto, si decidimos usar la función de filtro en lugar de cada función, terminamos viendo algo como esto:

La versión de árbol sacudido se ve así:

Observe cómo esta vez se incluyen tanto cada uno como el filtro . Esto se debe a que el filtro está definido para usar cada uno , por lo que necesitamos ambas exportaciones para que el módulo funcione.

Bastante astuto, ¿eh?

Te desafío a que juegues y explores el movimiento de árboles en la demostración y el editor en vivo de Rollup.js.

Construyendo módulos ES6

Bien, sabemos que los módulos ES6 se cargan de manera diferente a otros formatos de módulo, pero aún no hemos hablado sobre el paso de compilación para cuando usa módulos ES6.

Desafortunadamente, los módulos ES6 aún requieren un trabajo adicional, ya que aún no existe una implementación nativa de cómo los navegadores cargan los módulos ES6.

Aquí hay un par de opciones para construir / convertir módulos ES6 para que funcionen en el navegador, siendo el # 1 el enfoque más común en la actualidad:

  1. Utilice un transpilador (por ejemplo, Babel o Traceur) para transpilar su código ES6 a código ES5 en formato CommonJS, AMD o UMD. Luego, canalice el código transpilado a través de un paquete de módulos como Browserify o Webpack para crear uno o más archivos empaquetados.
  2. Use Rollup.js, que es muy similar a la opción n. ° 1, excepto que Rollup aprovecha la potencia de los módulos ES6 para analizar estáticamente su código ES6 y sus dependencias antes de empaquetar. Utiliza "sacudidas de árboles" para incluir lo mínimo en su paquete. En general, el principal beneficio de Rollup.js sobre Browserify o Webpack cuando usa módulos ES6 es que la agitación de árboles podría hacer que sus paquetes sean más pequeños. La advertencia es que Rollup proporciona varios formatos para agrupar su código, incluidos ES6, CommonJS, AMD, UMD o IIFE. Los paquetes IIFE y UMD funcionarían en su navegador tal como están, pero si elige empaquetarlos en AMD, CommonJS o ES6, necesita encontrar otros métodos para convertir ese código en un formato que el navegador entienda (por ejemplo, usando Browserify, Webpack, RequireJS, etc.).

Saltando a través de aros

Como desarrolladores web, tenemos que pasar por muchos obstáculos. No siempre es fácil convertir nuestros hermosos módulos ES6 en algo que los navegadores puedan interpretar.

La pregunta es, ¿cuándo se ejecutarán los módulos ES6 en el navegador sin toda esta sobrecarga?

La respuesta, afortunadamente, "más temprano que tarde".

ECMAScript tiene actualmente una especificación para una solución llamada API de cargador de módulo ECMAScript 6. En resumen, esta es una API programática basada en Promise que se supone que carga dinámicamente sus módulos y los almacena en caché para que las importaciones posteriores no recarguen una nueva versión del módulo.

Se verá algo como esto:

myModule.js

main.js

Alternativamente, también puede definir módulos especificando "tipo = módulo" directamente en la etiqueta de secuencia de comandos, así:

Si aún no ha revisado el repositorio para el polyfill de la API del cargador de módulos, le recomiendo encarecidamente que al menos eche un vistazo.

Además, si desea probar este enfoque, consulte SystemJS, que está construido sobre el polyfill del cargador de módulos ES6. SystemJS carga dinámicamente cualquier formato de módulo (módulos ES6, AMD, CommonJS y / o scripts globales) en el navegador y en Node. Realiza un seguimiento de todos los módulos cargados en un "registro de módulo" para evitar volver a cargar los módulos que se cargaron anteriormente. Sin mencionar que también transpila automáticamente los módulos ES6 (si simplemente configura una opción) y tiene la capacidad de cargar cualquier tipo de módulo de cualquier otro tipo. Con buena pinta.

¿Seguiremos necesitando paquetes ahora que tenemos módulos nativos de ES6?

La creciente popularidad de los módulos ES6 tiene algunas consecuencias interesantes:

¿HTTP / 2 hará que los paquetes de módulos sean obsoletos?

Con HTTP / 1, solo se nos permite una solicitud por conexión TCP. Es por eso que para cargar múltiples recursos se requieren múltiples solicitudes. Con HTTP / 2, todo cambia. HTTP / 2 está completamente multiplexado, lo que significa que pueden ocurrir múltiples solicitudes y respuestas en paralelo. Como resultado, podemos atender múltiples solicitudes simultáneamente con una sola conexión.

Dado que el costo por solicitud HTTP es significativamente más bajo que HTTP / 1, cargar un montón de módulos no será un gran problema de rendimiento a largo plazo. Algunos argumentan que esto significa que la agrupación de módulos ya no será necesaria. Ciertamente es posible, pero realmente depende de la situación.

Por un lado, la agrupación de módulos ofrece beneficios que HTTP / 2 no tiene en cuenta, como eliminar las exportaciones no utilizadas para ahorrar espacio. Si está creando un sitio web en el que cada pequeña parte del rendimiento importa, la agrupación puede brindarle ventajas incrementales a largo plazo. Dicho esto, si sus necesidades de rendimiento no son tan extremas, podría ahorrar tiempo a un costo mínimo al omitir el paso de compilación por completo.

En general, todavía estamos bastante lejos de que la mayoría de los sitios web sirvan su código a través de HTTP / 2. Me inclino a predecir que el proceso de construcción está aquí para quedarse, al menos a corto plazo.

PD: También hay otras diferencias con HTTP / 2, y si tienes curiosidad, aquí tienes un gran recurso.

¿Se volverán obsoletos CommonJS, AMD y UMD?

Una vez que ES6 se convierta en el estándar del módulo, ¿realmente necesitamos otros formatos de módulo no nativos?

Lo dudo.

El desarrollo web se beneficia enormemente de seguir un único método estandarizado para importar y exportar módulos en JavaScript, sin pasos intermedios. ¿Cuánto tiempo se tarda en llegar al punto en el que ES6 es el estándar del módulo?

Lo más probable es que, bastante tiempo;)

Además, hay muchas personas a las que les gusta tener "sabores" para elegir, por lo que el "enfoque único y veraz" puede que nunca se convierta en una realidad.

Conclusión

Espero que esta publicación de dos partes haya ayudado a aclarar parte de la jerga que usan los desarrolladores cuando hablan sobre módulos y agrupación de módulos. Continúe y consulte la parte I si encuentra confuso alguno de los términos anteriores.

Como siempre, háblame en los comentarios y no dudes en hacer preguntas.

Feliz empaque :)