Mantenha o controle: um guia para Webpack e React, Pt. 1

Publicados: 2022-03-11

Ao iniciar um novo projeto React, você tem muitos modelos para escolher: Create React App, react-boilerplate e React Starter Kit, para citar alguns.

Esses modelos, adotados por milhares de desenvolvedores, são capazes de dar suporte ao desenvolvimento de aplicativos em grande escala. Mas eles deixam a experiência do desenvolvedor e agrupam a saída sobrecarregada com vários padrões, o que pode não ser o ideal.

Se você deseja manter um maior grau de controle sobre o processo de compilação, pode optar por investir em uma configuração personalizada do Webpack. Como você aprenderá neste tutorial do Webpack, essa tarefa não é muito complicada, e o conhecimento pode até ser útil ao solucionar problemas de configurações de outras pessoas.

Webpack: Introdução

A maneira como escrevemos JavaScript hoje é diferente do código que o navegador pode executar. Frequentemente, contamos com outros tipos de recursos, linguagens transpiladas e recursos experimentais que ainda não foram suportados em navegadores modernos. O Webpack é um empacotador de módulos para JavaScript que pode preencher essa lacuna e produzir código compatível com vários navegadores sem nenhum custo quando se trata de experiência do desenvolvedor.

Antes de começarmos, você deve ter em mente que todo o código apresentado neste tutorial do Webpack também está disponível na forma de um arquivo de configuração de exemplo Webpack/React completo no GitHub. Por favor, sinta-se à vontade para consultá-lo lá e voltar a este artigo se tiver alguma dúvida.

Configuração básica

Desde o Legato (versão 4), o Webpack não requer nenhuma configuração para ser executado. Escolher um modo de construção aplicará um conjunto de padrões mais adequado ao ambiente de destino. No espírito deste artigo, vamos deixar esses padrões de lado e implementar uma configuração sensata para cada ambiente de destino.

Primeiro, precisamos instalar webpack e webpack-cli :

 npm install -D webpack webpack-cli

Em seguida, precisamos preencher o webpack.config.js com uma configuração com as seguintes opções:

  • devtool : Habilita a geração do mapa de origem no modo de desenvolvimento.
  • entry : O arquivo principal do nosso aplicativo React.
  • output.path : O diretório raiz para armazenar os arquivos de saída.
  • output.filename : O padrão de nome de arquivo a ser usado para arquivos gerados.
  • output.publicPath : O caminho para o diretório raiz onde os arquivos serão implantados no 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: "/" } }; };

A configuração acima funciona bem para arquivos JavaScript simples. Mas ao usar Webpack e React, precisaremos realizar transformações adicionais antes de enviar o código para nossos usuários. Na próxima seção, usaremos o Babel para alterar a maneira como o Webpack carrega os arquivos JavaScript.

Carregador JS

Babel é um compilador JavaScript com muitos plugins para transformação de código. Nesta seção, vamos apresentá-lo como um carregador em nossa configuração do Webpack e configurá-lo para transformar o código JavaScript moderno em tal que seja entendido por navegadores comuns.

Primeiro, precisaremos instalar babel-loader e @babel/core :

 npm install -D @babel/core babel-loader

Em seguida, adicionaremos uma seção de module à nossa configuração do Webpack, tornando o babel-loader responsável por carregar arquivos 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 configurar o Babel usando um arquivo de configuração separado, babel.config.js . Ele usará os seguintes recursos:

  • @babel/preset-env : transforma recursos JavaScript modernos em código compatível com versões anteriores.
  • @babel/preset-react react : Transforma a sintaxe JSX em chamadas de função JavaScript simples.
  • @babel/plugin-transform-runtime : Reduz a duplicação de código extraindo os auxiliares Babel em módulos compartilhados.
  • @babel/plugin-syntax-dynamic-import : Habilita a sintaxe de importação dinâmica import() em navegadores sem suporte nativo ao Promise .
  • @babel/plugin-proposal-class-properties : Habilita o suporte para a proposta de sintaxe de campo de instância pública, para escrever componentes React baseados em classe.

Também habilitaremos algumas otimizações de produção específicas do React:

  • babel-plugin-transform-react-remove-prop-types remove prop-types desnecessários do código de produção.
  • @babel/plugin-transform-react-inline-elements avalia React.createElement durante a compilação e inline o resultado.
  • @babel/plugin-transform-react-constant-elements extrai elementos React estáticos como constantes.

O comando abaixo irá instalar todas as dependências necessárias:

 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

Em seguida, preencheremos babel.config.js com estas configurações:

 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" ] } } };

Essa configuração nos permite escrever JavaScript moderno de forma compatível com todos os navegadores relevantes. Existem outros tipos de recursos que podemos precisar em um aplicativo React, que abordaremos nas seções a seguir.

Carregador de CSS

Quando se trata de estilizar aplicativos React, no mínimo, precisamos incluir arquivos CSS simples. Vamos fazer isso no Webpack usando os seguintes carregadores:

  • css-loader : analisa arquivos CSS, resolvendo recursos externos, como imagens, fontes e importações de estilo adicionais.
  • style-loader : Durante o desenvolvimento, injeta estilos carregados no documento em tempo de execução.
  • mini-css-extract-plugin : Extrai estilos carregados em arquivos separados para uso em produção para aproveitar o cache do navegador.

Vamos instalar os carregadores CSS acima:

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

Em seguida, adicionaremos uma nova regra à seção module.rules de nossa configuração do 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" + ] } ] },

Também adicionaremos MiniCssExtractPlugin à seção de plugins , que só ativaremos no modo de produção:

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

Essa configuração funciona para arquivos CSS simples e pode ser estendida para trabalhar com vários processadores CSS, como Sass e PostCSS, que discutiremos no próximo artigo.

Carregador de imagens

O Webpack também pode ser usado para carregar recursos estáticos, como imagens, vídeos e outros arquivos binários. A maneira mais genérica de lidar com esses tipos de arquivos é usando file-loader ou url-loader , que fornecerá uma referência de URL para os recursos necessários para seus consumidores.

Nesta seção, adicionaremos o url-loader para lidar com formatos de imagem comuns. O que diferencia url-loader do file-loader é que, se o tamanho do arquivo original for menor que um determinado limite, ele incorporará o arquivo inteiro na URL como conteúdo codificado em base64, eliminando assim a necessidade de uma solicitação adicional.

Primeiro instalamos url-loader :

 npm install -D url-loader

Em seguida, adicionamos uma nova regra à seção module.rules de nossa configuração do 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 imagens SVG, usaremos o carregador @svgr/webpack , que transforma arquivos importados em componentes React.

Instalamos @svgr/webpack :

 npm install -D @svgr/webpack

Em seguida, adicionamos uma nova regra à seção module.rules de nossa configuração do Webpack:

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

Imagens SVG como componentes React podem ser convenientes, e @svgr/webpack realiza otimização usando SVGO.

Nota: Para certas animações ou mesmo efeitos de mouseover, pode ser necessário manipular o SVG usando JavaScript. Felizmente, @svgr/webpack incorpora o conteúdo SVG no pacote JavaScript por padrão, permitindo que você ignore as restrições de segurança necessárias para isso.

Carregador de arquivos

Quando precisarmos referenciar qualquer outro tipo de arquivo, o file-loader genérico fará o trabalho. Ele funciona de maneira semelhante ao url-loader , fornecendo um URL de recurso para o código que o requer, mas não faz nenhuma tentativa de otimizá-lo.

Como sempre, primeiro instalamos o módulo Node.js. Nesse caso, file-loader :

 npm install -D file-loader

Em seguida, adicionamos uma nova regra à seção module.rules de nossa configuração do Webpack. Por exemplo:

 @@ -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]" + } } ] },

Aqui adicionamos file-loader para carregar fontes, que você pode referenciar em seus arquivos CSS. Você pode estender este exemplo para carregar quaisquer outros tipos de arquivos necessários.

Plugin de ambiente

Podemos usar DefinePlugin DefinePlugin() do Webpack para expor variáveis ​​de ambiente do ambiente de compilação para o código do nosso aplicativo. Por exemplo:

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

Aqui substituímos process.env.NODE_ENV por uma string representando o modo de construção: "development" ou "production" .

Plug-in HTML

Na ausência de um arquivo index.html , nosso pacote JavaScript é inútil, apenas parado sem que ninguém consiga encontrá-lo. Nesta seção, apresentaremos o html-webpack-plugin para gerar um arquivo HTML para nós.

Instalamos html-webpack-plugin :

 npm install -D html-webpack-plugin

Em seguida, adicionamos html-webpack-plugin à seção de plugins da nossa configuração do 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) };

O arquivo public/index.html gerado carregará nosso pacote e inicializará nosso aplicativo.

Otimização

Existem várias técnicas de otimização que podemos usar em nosso processo de construção. Começaremos com a minificação de código, um processo pelo qual podemos reduzir o tamanho do nosso pacote sem nenhum custo em termos de funcionalidade. Usaremos dois plugins para minimizar nosso código: terser-webpack-plugin para código JavaScript e optimize-css-assets-webpack-plugin para CSS.

Vamos instalá-los:

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

Em seguida, adicionaremos uma seção de optimization à nossa configuração:

 @@ -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() + ] + } }; };

As configurações acima garantirão a compatibilidade do código com todos os navegadores modernos.

Divisão de código

A divisão de código é outra técnica que podemos usar para melhorar o desempenho do nosso aplicativo. A divisão de código pode se referir a duas abordagens diferentes:

  1. Usando uma instrução import() dinâmica, podemos extrair partes do aplicativo que compõem uma parte significativa do tamanho do nosso pacote e carregá-las sob demanda.
  2. Podemos extrair o código que muda com menos frequência, a fim de aproveitar o cache do navegador e melhorar o desempenho dos visitantes recorrentes.

Vamos preencher a seção optimization.splitChunks da nossa configuração do Webpack com configurações para extrair dependências de terceiros e partes comuns em arquivos 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" } }; };

Vamos dar uma olhada mais profunda nas opções que usamos aqui:

  • chunks: "all" : Por padrão, a extração de chunk comum afeta apenas os módulos carregados com um import() dinâmico. Essa configuração também permite a otimização para carregamento de ponto de entrada.
  • minSize: 0 : Por padrão, apenas os fragmentos acima de um determinado limite de tamanho se qualificam para extração. Essa configuração permite a otimização de todos os códigos comuns, independentemente de seu tamanho.
  • maxInitialRequests: 20 e maxAsyncChunks: 20 : Essas configurações aumentam o número máximo de arquivos de origem que podem ser carregados em paralelo para importações de ponto de entrada e importações de ponto de divisão, respectivamente.

Além disso, especificamos a seguinte configuração cacheGroups :

  • vendors : Configura a extração para módulos de terceiros.
    • test: /[\\/]node_modules[\\/]/ : Padrão de nome de arquivo para correspondência de dependências de terceiros.
    • name(module, chunks, cacheGroupKey) : Agrupa pedaços separados do mesmo módulo, dando-lhes um nome comum.
  • common : Configura a extração de partes comuns do código do aplicativo.
    • minChunks: 2 : Um pedaço será considerado comum se referenciado em pelo menos dois módulos.
    • priority: -10 : Atribui uma prioridade negativa ao grupo de cache common para que os fragmentos do grupo de cache de vendors sejam considerados primeiro.

Também extraímos o código de tempo de execução do Webpack em um único bloco que pode ser compartilhado entre vários pontos de entrada, especificando runtimeChunk: "single" .

Servidor de desenvolvimento

Até agora, nos concentramos em criar e otimizar a compilação de produção de nosso aplicativo, mas o Webpack também possui seu próprio servidor web com recarga ao vivo e relatórios de erros, o que nos ajudará no processo de desenvolvimento. É chamado webpack-dev-server e precisamos instalá-lo separadamente:

 npm install -D webpack-dev-server

Neste trecho, introduzimos uma seção devServer em nossa configuração do Webpack:

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

Aqui usamos as seguintes opções:

  • compress: true : habilita a compactação de ativos para recargas mais rápidas.
  • historyApiFallback: true : habilita um fallback para index.html para roteamento baseado em histórico.
  • open: true : abre o navegador após iniciar o servidor dev.
  • overlay: true : Exibe os erros do Webpack na janela do navegador.

Você também pode precisar definir as configurações de proxy para encaminhar solicitações de API para o servidor de back-end.

Webpack e React: otimizado para desempenho e pronto!

Acabamos de aprender como carregar vários tipos de recursos com o Webpack, como usar o Webpack em um ambiente de desenvolvimento e várias técnicas para otimizar uma compilação de produção. Se precisar, você sempre pode revisar o arquivo de configuração completo para se inspirar para sua própria configuração do React/Webpack. Aprimorar essas habilidades é uma tarifa padrão para qualquer pessoa que ofereça serviços de desenvolvimento React.

Na próxima parte desta série, expandiremos essa configuração com instruções para casos de uso mais específicos, incluindo uso de TypeScript, pré-processadores CSS e técnicas avançadas de otimização envolvendo renderização no lado do servidor e ServiceWorkers. Fique atento para saber tudo o que você precisa saber sobre o Webpack para levar seu aplicativo React à produção.