Mantener el control: una guía para Webpack y React, pt. 1

Publicado: 2022-03-11

Al iniciar un nuevo proyecto de React, tiene muchas plantillas para elegir: Create React App, react-boilerplate y React Starter Kit, por nombrar algunas.

Estas plantillas, adoptadas por miles de desarrolladores, pueden admitir el desarrollo de aplicaciones a gran escala. Pero dejan la experiencia del desarrollador y la salida del paquete cargada con varios valores predeterminados, lo que puede no ser ideal.

Si desea mantener un mayor grado de control sobre su proceso de compilación, puede optar por invertir en una configuración personalizada de Webpack. Como aprenderá de este tutorial de Webpack, esta tarea no es muy complicada y el conocimiento podría incluso ser útil para solucionar problemas de configuración de otras personas.

Paquete web: Primeros pasos

La forma en que escribimos JavaScript hoy es diferente del código que puede ejecutar el navegador. Con frecuencia confiamos en otros tipos de recursos, idiomas transpilados y funciones experimentales que aún no se admiten en los navegadores modernos. Webpack es un paquete de módulos para JavaScript que puede cerrar esta brecha y producir código compatible con varios navegadores sin costo alguno en lo que respecta a la experiencia del desarrollador.

Antes de comenzar, debe tener en cuenta que todo el código presentado en este tutorial de Webpack también está disponible en forma de un archivo de configuración de ejemplo completo de Webpack/React en GitHub. No dude en consultarlo allí y volver a este artículo si tiene alguna pregunta.

Configuración básica

Desde Legato (versión 4), Webpack no requiere ninguna configuración para ejecutarse. Al elegir un modo de compilación, se aplicará un conjunto de valores predeterminados más adecuados para el entorno de destino. En el espíritu de este artículo, dejaremos de lado esos valores predeterminados e implementaremos una configuración sensata para cada entorno de destino.

Primero, necesitamos instalar webpack y webpack-cli :

 npm install -D webpack webpack-cli

Luego, debemos webpack.config.js con una configuración que presente las siguientes opciones:

  • devtool : Habilita la generación de mapas de origen en el modo de desarrollo.
  • entry : El archivo principal de nuestra aplicación React.
  • output.path : El directorio raíz para almacenar archivos de salida.
  • output.filename : el patrón de nombre de archivo que se usará para los archivos generados.
  • output.publicPath : la ruta al directorio raíz donde se implementarán los archivos en el servidor web.
 const path = require("path"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; const isDevelopment = !isProduction; return { devtool: isDevelopment && "cheap-module-source-map", entry: "./src/index.js", output: { path: path.resolve(__dirname, "dist"), filename: "assets/js/[name].[contenthash:8].js", publicPath: "/" } }; };

La configuración anterior funciona bien para archivos JavaScript sin formato. Pero al usar Webpack y React, necesitaremos realizar transformaciones adicionales antes de enviar el código a nuestros usuarios. En la siguiente sección, usaremos Babel para cambiar la forma en que Webpack carga archivos JavaScript.

Cargador JS

Babel es un compilador de JavaScript con muchos complementos para la transformación de código. En esta sección, lo introduciremos como un cargador en nuestra configuración de Webpack y lo configuraremos para transformar el código JavaScript moderno en uno que sea entendido por los navegadores comunes.

Primero, necesitaremos instalar babel-loader y @babel/core :

 npm install -D @babel/core babel-loader

Luego agregaremos una sección de module a nuestra configuración de Webpack, haciendo que babel-loader sea responsable de cargar archivos JavaScript:

 @@ -11,6 +11,25 @@ module.exports = function(_env, argv) { path: path.resolve(__dirname, "dist"), filename: "assets/js/[name].[contenthash:8].js", publicPath: "/" + }, + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + cacheDirectory: true, + cacheCompression: false, + envName: isProduction ? "production" : "development" + } + } + } + ] + }, + resolve: { + extensions: [".js", ".jsx"] } }; };

Vamos a configurar Babel usando un archivo de configuración separado, babel.config.js . Utilizará las siguientes características:

  • @babel/preset-env : transforma las características modernas de JavaScript en código compatible con versiones anteriores.
  • @babel/preset-react : transforma la sintaxis JSX en llamadas de función JavaScript sencillas.
  • @babel/plugin-transform-runtime : reduce la duplicación de código al extraer ayudantes de Babel en módulos compartidos.
  • @babel/plugin-syntax-dynamic-import : Habilita la sintaxis de importación dinámica import() en navegadores que carecen de compatibilidad nativa con Promise .
  • @babel/plugin-proposal-class-properties : Habilita el soporte para la propuesta de sintaxis de campo de instancia pública, para escribir componentes React basados ​​en clases.

También habilitaremos algunas optimizaciones de producción específicas de React:

  • babel-plugin-transform-react-remove-prop-types elimina los tipos de accesorios innecesarios del código de producción.
  • @babel/plugin-transform-react-inline-elements evalúa React.createElement durante la compilación e inserta el resultado.
  • @babel/plugin-transform-react-constant-elements extrae elementos React estáticos como constantes.

El siguiente comando instalará todas las dependencias necesarias:

 npm install -D @babel/preset-env @babel/preset-react @babel/runtime @babel/plugin-transform-runtime @babel/plugin-syntax-dynamic-import @babel/plugin-proposal-class-properties babel-plugin-transform-react-remove-prop-types @babel/plugin-transform-react-inline-elements @babel/plugin-transform-react-constant-elements

Luego, babel.config.js con esta configuración:

 module.exports = { presets: [ [ "@babel/preset-env", { modules: false } ], "@babel/preset-react" ], plugins: [ "@babel/plugin-transform-runtime", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-class-properties" ], env: { production: { only: ["src"], plugins: [ [ "transform-react-remove-prop-types", { removeImport: true } ], "@babel/plugin-transform-react-inline-elements", "@babel/plugin-transform-react-constant-elements" ] } } };

Esta configuración nos permite escribir JavaScript moderno de manera que sea compatible con todos los navegadores relevantes. Hay otros tipos de recursos que podríamos necesitar en una aplicación React, que cubriremos en las siguientes secciones.

Cargador CSS

Cuando se trata de diseñar aplicaciones React, como mínimo, debemos poder incluir archivos CSS sin formato. Vamos a hacer esto en Webpack usando los siguientes cargadores:

  • css-loader : analiza archivos CSS, resolviendo recursos externos, como imágenes, fuentes e importaciones de estilos adicionales.
  • style-loader : durante el desarrollo, inyecta estilos cargados en el documento en tiempo de ejecución.
  • mini-css-extract-plugin : extrae estilos cargados en archivos separados para uso de producción para aprovechar el almacenamiento en caché del navegador.

Instalemos los cargadores CSS anteriores:

 npm install -D css-loader style-loader mini-css-extract-plugin

Luego agregaremos una nueva regla a la sección module.rules de nuestra configuración de Webpack:

 @@ -1,4 +1,5 @@ const path = require("path"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -25,6 +26,13 @@ module.exports = function(_env, argv) { envName: isProduction ? "production" : "development" } } + }, + { + test: /\.css$/, + use: [ + isProduction ? MiniCssExtractPlugin.loader : "style-loader", + "css-loader" + ] } ] },

También agregaremos MiniCssExtractPlugin a la sección de plugins , que solo habilitaremos en el modo de producción:

 @@ -38,6 +38,13 @@ module.exports = function(_env, argv) { }, resolve: { extensions: [".js", ".jsx"] - } + }, + plugins: [ + isProduction && + new MiniCssExtractPlugin({ + filename: "assets/css/[name].[contenthash:8].css", + chunkFilename: "assets/css/[name].[contenthash:8].chunk.css" + }) + ].filter(Boolean) }; };

Esta configuración funciona para archivos CSS sin formato y se puede ampliar para que funcione con varios procesadores CSS, como Sass y PostCSS, de los que hablaremos en el próximo artículo.

Cargador de imágenes

Webpack también se puede usar para cargar recursos estáticos como imágenes, videos y otros archivos binarios. La forma más genérica de manejar este tipo de archivos es utilizando file-loader o el cargador url-loader , que proporcionará una referencia de URL para los recursos necesarios para sus consumidores.

En esta sección, agregaremos url-loader para manejar formatos de imagen comunes. Lo que diferencia url-loader de file-loader es que si el tamaño del archivo original es más pequeño que un umbral dado, incrustará el archivo completo en la URL como contenido codificado en base64, eliminando así la necesidad de una solicitud adicional.

Primero instalamos url-loader :

 npm install -D url-loader

Luego agregamos una nueva regla a la sección module.rules de nuestra configuración de Webpack:

 @@ -34,6 +34,16 @@ module.exports = function(_env, argv) { isProduction ? MiniCssExtractPlugin.loader : "style-loader", "css-loader" ] + }, + { + test: /\.(png|jpg|gif)$/i, + use: { + loader: "url-loader", + options: { + limit: 8192, + name: "static/media/[name].[hash:8].[ext]" + } + } } ] },

SVG

Para las imágenes SVG, vamos a utilizar el @svgr/webpack , que transforma los archivos importados en componentes React.

Instalamos @svgr/webpack :

 npm install -D @svgr/webpack

Luego agregamos una nueva regla a la sección module.rules de nuestra configuración de Webpack:

 @@ -44,6 +44,10 @@ module.exports = function(_env, argv) { name: "static/media/[name].[hash:8].[ext]" } } + }, + { + test: /\.svg$/, + use: ["@svgr/webpack"] } ] },

Las imágenes SVG como componentes de React pueden ser convenientes, y @svgr/webpack realiza la optimización mediante SVGO.

Nota: para ciertas animaciones o incluso efectos de mouseover, es posible que deba manipular el SVG usando JavaScript. Afortunadamente, @svgr/webpack incorpora contenido SVG en el paquete de JavaScript de forma predeterminada, lo que le permite eludir las restricciones de seguridad necesarias para esto.

cargador de archivos

Cuando necesitemos hacer referencia a otros tipos de archivos, el file-loader de archivos genérico hará el trabajo. Funciona de manera similar a url-loader , proporcionando una URL de activo para el código que lo requiere, pero no intenta optimizarlo.

Como siempre, primero instalamos el módulo Node.js. En este caso, file-loader :

 npm install -D file-loader

Luego agregamos una nueva regla a la sección module.rules de nuestra configuración de Webpack. Por ejemplo:

 @@ -48,6 +48,13 @@ module.exports = function(_env, argv) { { test: /\.svg$/, use: ["@svgr/webpack"] + }, + { + test: /\.(eot|otf|ttf|woff|woff2)$/, + loader: require.resolve("file-loader"), + options: { + name: "static/media/[name].[hash:8].[ext]" + } } ] },

Aquí agregamos file-loader para cargar fuentes, al que puede hacer referencia desde sus archivos CSS. Puede ampliar este ejemplo para cargar cualquier otro tipo de archivo que necesite.

Complemento de entorno

Podemos usar DefinePlugin DefinePlugin() de Webpack para exponer las variables de entorno del entorno de compilación a nuestro código de aplicación. Por ejemplo:

 @@ -1,5 +1,6 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const webpack = require("webpack"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -65,7 +66,12 @@ module.exports = function(_env, argv) { new MiniCssExtractPlugin({ filename: "assets/css/[name].[contenthash:8].css", chunkFilename: "assets/css/[name].[contenthash:8].chunk.css" - }) + }), + new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify( + isProduction ? "production" : "development" + ) + }) ].filter(Boolean) }; };

Aquí sustituimos process.env.NODE_ENV con una cadena que representa el modo de compilación: "development" o "production" .

Complemento HTML

En ausencia de un archivo index.html , nuestro paquete de JavaScript es inútil, simplemente está sentado allí sin que nadie pueda encontrarlo. En esta sección, presentaremos html-webpack-plugin para generar un archivo HTML para nosotros.

Instalamos html-webpack-plugin :

 npm install -D html-webpack-plugin

Luego agregamos html-webpack-plugin a la sección de plugins de nuestra configuración de Webpack:

 @@ -1,6 +1,7 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const webpack = require("webpack"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -71,6 +72,10 @@ module.exports = function(_env, argv) { "process.env.NODE_ENV": JSON.stringify( isProduction ? "production" : "development" ) + }), + new HtmlWebpackPlugin({ + template: path.resolve(__dirname, "public/index.html"), + inject: true }) ].filter(Boolean) };

El archivo public/index.html generado cargará nuestro paquete y arrancará nuestra aplicación.

Mejoramiento

Hay varias técnicas de optimización que podemos usar en nuestro proceso de compilación. Comenzaremos con la minificación del código, un proceso mediante el cual podemos reducir el tamaño de nuestro paquete sin costo alguno en términos de funcionalidad. Usaremos dos complementos para minimizar nuestro código: terser-webpack-plugin para código JavaScript y optimize-css-assets-webpack-plugin para CSS.

Vamos a instalarlos:

 npm install -D terser-webpack-plugin optimize-css-assets-webpack-plugin

Luego agregaremos una sección de optimization a nuestra configuración:

 @@ -2,6 +2,8 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const webpack = require("webpack"); const HtmlWebpackPlugin = require("html-webpack-plugin"); +const TerserWebpackPlugin = require("terser-webpack-plugin"); +const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -75,6 +77,27 @@ module.exports = function(_env, argv) { isProduction ? "production" : "development" ) }) - ].filter(Boolean) + ].filter(Boolean), + optimization: { + minimize: isProduction, + minimizer: [ + new TerserWebpackPlugin({ + terserOptions: { + compress: { + comparisons: false + }, + mangle: { + safari10: true + }, + output: { + comments: false, + ascii_only: true + }, + warnings: false + } + }), + new OptimizeCssAssetsPlugin() + ] + } }; };

La configuración anterior garantizará la compatibilidad del código con todos los navegadores modernos.

División de código

La división de código es otra técnica que podemos utilizar para mejorar el rendimiento de nuestra aplicación. La división de código puede referirse a dos enfoques diferentes:

  1. Usando una declaración de import() , podemos extraer partes de la aplicación que constituyen una parte significativa del tamaño de nuestro paquete y cargarlas a pedido.
  2. Podemos extraer el código que cambia con menos frecuencia para aprovechar el almacenamiento en caché del navegador y mejorar el rendimiento para los visitantes que repiten.

Completaremos la sección de optimization.splitChunks de nuestra configuración de Webpack con configuraciones para extraer dependencias de terceros y fragmentos comunes en archivos separados:

 @@ -99,7 +99,29 @@ module.exports = function(_env, argv) { sourceMap: true }), new OptimizeCssAssetsPlugin() - ] + ], + splitChunks: { + chunks: "all", + minSize: 0, + maxInitialRequests: 20, + maxAsyncRequests: 20, + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + name(module, chunks, cacheGroupKey) { + const packageName = module.context.match( + /[\\/]node_modules[\\/](.*?)([\\/]|$)/ + )[1]; + return `${cacheGroupKey}.${packageName.replace("@", "")}`; + } + }, + common: { + minChunks: 2, + priority: -10 + } + } + }, + runtimeChunk: "single" } }; };

Echemos un vistazo más profundo a las opciones que hemos usado aquí:

  • chunks: "all" : de forma predeterminada, la extracción de fragmentos comunes solo afecta a los módulos cargados con una import() . Esta configuración también permite la optimización para la carga del punto de entrada.
  • minSize: 0 : De forma predeterminada, solo los fragmentos por encima de un cierto umbral de tamaño son elegibles para la extracción. Esta configuración permite la optimización para todo el código común, independientemente de su tamaño.
  • maxInitialRequests: 20 y maxAsyncChunks: 20 : esta configuración aumenta la cantidad máxima de archivos de origen que se pueden cargar en paralelo para importaciones de punto de entrada y de punto de división, respectivamente.

Además, especificamos la siguiente configuración cacheGroups :

  • vendors : Configura la extracción para módulos de terceros.
    • test: /[\\/]node_modules[\\/]/ : patrón de nombre de archivo para hacer coincidir las dependencias de terceros.
    • name(module, chunks, cacheGroupKey) : Agrupa fragmentos separados del mismo módulo dándoles un nombre común.
  • common : configura la extracción de fragmentos comunes del código de la aplicación.
    • minChunks: 2 : un fragmento se considerará común si se hace referencia a él desde al menos dos módulos.
    • priority: -10 : asigna una prioridad negativa al grupo de caché common para que los fragmentos del grupo de caché de vendors se consideren primero.

También extraemos el código de tiempo de ejecución de Webpack en un solo fragmento que se puede compartir entre varios puntos de entrada, especificando runtimeChunk: "single" .

Servidor de desarrollo

Hasta ahora, nos hemos centrado en crear y optimizar la compilación de producción de nuestra aplicación, pero Webpack también tiene su propio servidor web con recarga en vivo e informes de errores, lo que nos ayudará en el proceso de desarrollo. Se llama webpack-dev-server y debemos instalarlo por separado:

 npm install -D webpack-dev-server

En este fragmento, presentamos una sección devServer en nuestra configuración de Webpack:

 @@ -120,6 +120,12 @@ module.exports = function(_env, argv) { } }, runtimeChunk: "single" + }, + devServer: { + compress: true, + historyApiFallback: true, + open: true, + overlay: true } }; };

Aquí hemos utilizado las siguientes opciones:

  • compress: true : Habilita la compresión de activos para recargas más rápidas.
  • historyApiFallback: true : habilita una alternativa a index.html para el enrutamiento basado en el historial.
  • open: true : Abre el navegador después de iniciar el servidor de desarrollo.
  • overlay: true : muestra errores de Webpack en la ventana del navegador.

Es posible que también deba configurar los ajustes del proxy para reenviar solicitudes de API al servidor backend.

Webpack y React: ¡Rendimiento optimizado y Listo!

Acabamos de aprender cómo cargar varios tipos de recursos con Webpack, cómo usar Webpack en un entorno de desarrollo y varias técnicas para optimizar una compilación de producción. Si lo necesita, siempre puede revisar el archivo de configuración completo en busca de inspiración para su propia configuración de React/Webpack. Mejorar tales habilidades es una tarifa estándar para cualquier persona que ofrezca servicios de desarrollo de React.

En la próxima parte de esta serie, ampliaremos esta configuración con instrucciones para casos de uso más específicos, incluido el uso de TypeScript, preprocesadores CSS y técnicas de optimización avanzadas que involucran la representación del lado del servidor y ServiceWorkers. Estén atentos para aprender todo lo que necesita saber sobre Webpack para llevar su aplicación React a producción.