Artigo original: How to use Webpack with React: an in-depth tutorial

Atualizado para o Babel 7

Neste tutorial, veremos o básico sobre o Webpack com o React para você começar, incluindo React Router, Hot Module Replacement (HMR, ou substituição rápida de módulos), Code Splitting (ou divisão de código, em português) por rota e por fornecedor, configuração de produção e mais.

Antes de começarmos, aqui está uma lista completa dos recursos que vamos configurar juntos neste tutorial:

  • React 16
  • React Router 5
  • Semantic UI como o Framework CSS
  • Hot Module Replacement (HMR)
  • Autoprefixador do CSS
  • Módulos do CSS
  • @babel/plugin-proposal-class-properties
  • @babel/plugin-syntax-dynamic-import
  • Webpack 4
  • Code Splitting por rota e por fornecedor
  • Analisador de pacotes do Webpack
Nota do tradutor: no momento da tradução, algumas das bibliotecas principais deste tutorial estão em versões bem mais atualizadas (React está na versão 18, React Router na versão 6 e o Webpack na versão 5). Sugerimos, na hora da instalação, utilizar as versões acima. Para isso, use a notação com @. Exemplo: em vez de instalar o React usando yarn add react, use  yarn add react@16.2.0. Você evitará, desse modo, problemas de incompatibilidade com as versões mais recentes. A lista completa das versões a serem instaladas, você encontra nas dependências listadas no package.json abaixo.

Pré-requisitos

Ter o seguinte pré-instalado:

Você deve ter ao menos algum conhecimento básico de React e React Router.

Observação: você pode usar o npm em vez do Yarn, se desejar, embora os comandos variem um pouco.

Dependências iniciais

Vamos iniciar criando nosso diretório e o arquivo package.json.

Em seu terminal, digite o seguinte:

mkdir webpack-for-react && cd $_
yarn init -y

O primeiro comando criará nosso diretório e entrará nele. Então, iniciamos o package.json aceitando as opções padrão.
Se você o inspecionar, verá a configuração básica:

{
  "name": "webpack-for-react",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

Agora, instalaremos nossas dependências iniciais de produção e as dependências de desenvolvimento.
No seu terminal, digite o seguinte:

yarn add react react-dom prop-types react-router-dom semantic-ui-react
yarn add @babel/core babel-loader @babel/preset-env @babel/preset-react @babel/plugin-proposal-class-properties @babel/plugin-syntax-dynamic-import css-loader style-loader html-webpack-plugin webpack webpack-dev-server webpack-cli -D
Nota do tradutor: Conforme mencionamos anteriormente, é no comando acima que você instalará as dependências pela versão específica.

As dependências de desenvolvimento serão usadas somente, conforme o que se espera, durante a fase de desenvolvimento, enquanto as dependências de produção serão o que nossa aplicação precisa em produção.

{
  "name": "webpack-for-react",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "prop-types": "^15.6.2",
    "react-router-dom": "^4.2.2",
    "semantic-ui-react": "^0.77.1"
  },
  "devDependencies": {
    "@babel/core": "^7.0.0",
    "@babel/plugin-proposal-class-properties": "^7.0.0",
    "@babel/plugin-syntax-dynamic-import": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "@babel/preset-react": "^7.0.0", 
    "babel-loader": "^8.0.1",
    "css-loader": "^0.28.10",
    "html-webpack-plugin": "^3.0.4",
    "style-loader": "^0.19.1",
    "webpack": "^4.0.0",
    "webpack-cli": "^2.0.14",
    "webpack-dev-server": "^3.0.0"
  }
}

Observação 1: as alterações em arquivos criados anteriormente estarão em negrito.
Observação 2: as versões de dependências podem ser diferentes das suas desde o momento em que esse artigo foi escrito.

  • react — Tenho certeza de que você sabe o que é React
  • react-dom — Fornece métodos específicos do DOM para o navegador
  • prop-types — Verificação do tipo em tempo de execução para props do React
  • react-router-dom — Fornece recursos de roteamento para React no navegador
  • semantic-ui-react — Framework de CSS
  • @babel/core — Principais dependências para Babel
  • Babel é um transpilador que compila JavaScript ES6 para JavaScript ES5, permitindo escrever JavaScript "do futuro" para que os navegadores atuais o entendam. Descrição detalhada no Quora (em inglês).
  • babel-loader — Este pacote permite a transpilação de arquivos JavaScript usando Babel e o Webpack
  • @babel/preset-env — Com ele, você não precisa especificar se você vai escrever ES2015, ES2016 ou ES2017. O Babel detectará e fará a transpilação automaticamente de acordo
  • @babel/preset-react — Diz ao Babel que usaremos o React
  • @babel/plugin-proposal-class-properties — Usa propriedades de classe. Nós não usamos propriedades de classe neste projeto, mas é mais do que provável que você as use no seu
  • @babel/plugin-syntax-dynamic-import — Possibilita utilizar importações dinâmicas
  • css-loader — Interpreta @import e url() como import/require() e fará sua resolução
  • html-webpack-plugin — Pode gerar um arquivo HTML para sua aplicação, ou você pode fornecer um modelo
  • style-loader — Adiciona CSS ao DOM, injetando uma tag <style>
  • webpack — bundler de módulos
  • webpack-cli — Interface de Linha de Comando, necessária para o Webpack 4.0.1 e versões mais recentes
  • webpack-dev-server — Fornece um servidor de desenvolvimento para sua aplicação

Configurando o Babel

No diretório raiz (webpack-for-react), criaremos o arquivo de configuração do Babel.

touch .babelrc

Neste ponto, você pode abrir seu editor favorito (a propósito, o meu é o VS Code) e depois apontar o editor para a raiz deste projeto, abrindo o arquivo .babelrc e copiando o seguinte:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-proposal-class-properties"
  ]
}

Isto diz ao Babel para utilizar as pré-definições (plug-ins) que instalamos anteriormente. Mais tarde, quando chamarmos o babel-loader do Webpack, é aqui que ele vai procurar saber o que fazer.

Configurando o Webpack

Agora, a diversão começa! Vamos criar o arquivo de configuração do Webpack.

Em seu terminal, digite o seguinte:

touch webpack.config.js

Abra o arquivo webpack.config.js e copie o seguinte:

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const port = process.env.PORT || 3000;

module.exports = {
  // A configuração do Webpack vai aqui
};

Este é o shell básico para o Webpack. Nós precisamos do webpack e do html-webpack-plugin. Forneça uma porta padrão se a variável de ambiente PORT não existir e exporte o módulo.

Será acrescentado o seguinte ao arquivo webpack.config.js (um após o outro).

...
module.exports = {
  mode: 'development',
};

mode diz ao Webpack que esta configuração será para development ou production. "O modo de desenvolvimento [é] otimizado para velocidade e experiência do desenvolvedor... Os padrões de produção darão a você um conjunto de padrões úteis para a implantação de sua aplicação" (Fonte: Webpack 4: mode and optimization - texto em inglês).

...
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.[hash].js'
  },
};

Para ter um exemplo do Webpack em funcionamento, precisamos do seguinte:

  • entry — Especifica o ponto de entrada de sua aplicação – é aqui que sua aplicação do React vive e onde o processo de empacotamento começará (documentação)

O Webpack 4 introduziu alguns padrões. Então, se você não incluir entry em suas configurações, ele assumirá que seu ponto de entrada está localizado sob o diretório ./src , tornando entry opcional, ao contrário do que ocorria no Webpack 3. Para este tutorial, eu decidi deixar entry, pois torna óbvio onde nosso ponto de entrada estará. Você, no entanto, é mais do que bem-vindo para removê-lo se assim o decidir.

  • output — Diz ao Webpack como escrever os arquivos compilados em disco (documentação)
  • filename — Este será o nome do arquivo do pacote da aplicação. A porção [hash] do nome do arquivo será substituída por um hash gerado pelo Webpack toda vez que sua aplicação mudar e for recompilada (ajuda com caching).
...
module.exports = {
  ...
  devtool: 'inline-source-map',
};

devtool criará source maps (texto em inglês) para ajudar você na depuração de sua aplicação. Há vários tipos de source maps. O mapa inline-source-map, em particular, é para ser usado somente no desenvolvimento (consulte a documentação para ver mais opções).

...
module.exports = {
  ...
  module: {
    rules: [

      // Primeira regra
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },

      // Segunda regra
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localsConvention: 'camelCase',
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
};
  • module — Que tipos de módulos sua aplicação incluirá. Em nosso caso, daremos suporte aos Módulos ESNext (Babel) e CSS
  • rules — Como lidamos com cada tipo diferente de módulo

Primeira regra

Testamos arquivos com uma extensão .js, excluindo o diretório node_modules, e usando Babel, por meio do babel-loader, para executar uma transpilação para JavaScript puro (basicamente, procurando por nossos arquivos do React).

Se lembra de nossa configuração em .babelrc? É aqui que o Babel procura por esse arquivo.

Segunda regra

Buscamos os arquivos de CSS pela extensão .css. Aqui, usamos dois loaders (em português, carregadores), style-loader e css-loader, para tratar nossos arquivos em CSS. Em seguida, instruímos css-loader para usar Módulos do CSS, formatar em camel case e criar os source maps.

Módulos CSS e a formatação em camel case

Isto nos dá a capacidade de usar a sintaxe import Styles from './styles.css' (ou de usar desestruturação, assim: import { style1, style2 } from ‘./styles.css’).

Então, podemos usar isso deste modo em uma aplicação do React:

...
<div className={Style.style1}>Olá Mundo</div>
// ou com a sintaxe de desestruturação
<div className={style1}>Olá Mundo</div>
...

O camel case nos dá a capacidade de escrever nossas regras de CSS desta forma:

.home-button {...}

Podemos utilizá-lo em nossos arquivos do React, assim:

...
import { homeButton } from './styles.css'
...
...
module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ],
};

Esta seção é onde configuramos (como o nome indica) os plug-ins.

html-webpack-plugin aceita um objeto com diferentes opções. Em nosso caso, especificamos o modelo HTML que estaremos usando e o favicon. (Consulte a documentação para ver mais opções).

Mais tarde, adicionaremos outros plug-ins para o analisador de pacotes e para o HMR (substituição rápida de módulos).

...
module.exports = {
  ...
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    open: true
  }
};

Por fim, vamos configurar o servidor de desenvolvimento. Especificamos localhost como o host e atribuímos a variável port como a porta (se você se lembra, atribuímos a porta 3000 a esta variável). Definimos historyApiFallback como true e open como true. Isto abrirá o navegador automaticamente e iniciará sua aplicação em http://localhost:3000 (veja a documentação).

Agora, abaixo está a configuração completa do Webpack (webpack.config.js):

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const port = process.env.PORT || 3000;

module.exports = {
  mode: 'development',  
  entry: './src/index.js',
  output: {
    filename: 'bundle.[hash].js'
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localsConvention: 'camelCase',
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ],
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    open: true
  }
};

Criando a aplicação em React

Estaremos criando uma simples para a aplicação Hello World com três rotas: uma home, uma página não encontrada e uma página dinâmica, que carregaremos de modo assíncrono quando implementarmos mais tarde o code splitting (divisão de código, em português).

Observação: assumindo que você tenha um entendimento básico de React e de React Router, não entrarei em detalhes aqui. Destacarei apenas o que é relevante para este tutorial.

Atualmente, temos a seguinte estrutura de projeto:

|-- node_modules
|-- .babelrc
|-- package.json
|-- webpack.config.js
|-- yarn.lock

Em seu terminal, digite o seguinte:

mkdir public && cd $_
touch index.html

Criamos o diretório public, entramos no diretório e criamos o arquivo index.html. Também temos aqui o favicon. Você pode pegá-lo aqui e copiá-lo para o diretório public.

Abra o arquivo index.html e copie o seguinte:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.13/semantic.min.css"></link>
  <title>webpack-for-react</title>
</head>

<body>
  <div id="root"></div>
</body>

</html>

Nada demais aqui (apenas um modelo HTML padrão). Estamos somente adicionando a folha de estilo Semantic UI e criando uma div com o ID root. Aqui é onde nossa aplicação React será renderizada.

Volte ao seu terminal e digite:

cd ..
mkdir src && cd $_
touch index.js

Abra o arquivo index.js e copie o seguinte:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';

ReactDOM.render(<App />, document.getElementById('root'));

Em seu terminal, digite:

mkdir components && cd $_
touch App.js Layout.js Layout.css Home.js DynamicPage.js NoMatch.js

Após a criação dos arquivos de componentes do React, temos a seguinte estrutura de projeto:

|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- webpack.config.js
|-- yarn.lock

Abra o arquivo App.js e copie o seguinte:

import React from 'react';
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom';

import Home from './Home';
import DynamicPage from './DynamicPage';
import NoMatch from './NoMatch';

const App = () => {
  return (
    <Router>
      <div>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/dynamic" component={DynamicPage} />
          <Route component={NoMatch} />
        </Switch>
      </div>
    </Router>
  );
};

export default App;

Criamos nosso "shell" básico com React Router, contendo rotas para home, para a página dinâmica e para a página não encontrada.

Abra o arquivo Layout.css e copie o seguinte:

.pull-right {
  display: flex;
  justify-content: flex-end;
}

.h1 {
  margin-top: 10px !important;
  margin-bottom: 20px !important;
}

Abra o arquivo Layout.js e insira:

import React from 'react';
import { Link } from 'react-router-dom';
import { Header, Container, Divider, Icon } from 'semantic-ui-react';

import { pullRight, h1 } from './layout.css';

const Layout = ({ children }) => {
  return (
    <Container>
      <Link to="/">
        <Header as="h1" className={h1}>
          webpack-for-react
        </Header>
      </Link>
      {children}
      <Divider />
      <p className={pullRight}>
        Feito com <Icon name="heart" color="red" /> por Esau Silva
      </p>
    </Container>
  );
};

export default Layout;

Este é o nosso componente de contêiner, onde definimos o layout do site. Fazendo uso dos módulos de CSS, estamos importando duas regras de CSS do layout.css. Observe, também, como estamos usando camel case para pullRight.

Abra o arquivo Home.js e copie o seguinte:

import React from 'react';
import { Link } from 'react-router-dom';

import Layout from './Layout';

const Home = () => {
  return (
    <Layout>
      <p>Olá Mundo do React e Webpack!</p>
      <p>
        <Link to="/dynamic">Navegar para a Página Dinâmica</Link>
      </p>
    </Layout>
  );
};

export default Home;

Abra o arquivo DynamicPage.js e insira:

import React from 'react';
import { Header } from 'semantic-ui-react';

import Layout from './Layout';

const DynamicPage = () => {
  return (
    <Layout>
      <Header as="h2">Página Dinâmica</Header>
      <p>Esta página foi carregada de forma assíncrona!!!</p>
    </Layout>
  );
};

export default DynamicPage;

Abra o arquivo NoMatch.js e adicione:

import React from 'react';
import { Icon, Header } from 'semantic-ui-react';

import Layout from './Layout';

const NoMatch = () => {
  return (
    <Layout>
      <Icon name="minus circle" size="big" />
      <strong>Página não encontrada!</strong>
    </Layout>
  );
};

export default NoMatch;

Já terminamos de criar os componentes do React. Para uma etapa final antes de rodar nossa aplicação, abra package.json e faça com que tenha essa aparência:

{
  "name": "webpack-for-react",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "webpack-dev-server"
  },
  "dependencies": {
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "prop-types": "^0.4.0",
    "react-router-dom": "^4.2.2",
    "semantic-ui-react": "^0.77.1"
  },
  "devDependencies": {
    "@babel/core": "^7.0.0",
    "@babel/plugin-proposal-class-properties": "^7.0.0",
    "@babel/plugin-syntax-dynamic-import": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.1",
    "css-loader": "^0.28.10",
    "html-webpack-plugin": "^3.0.4",
    "style-loader": "^0.19.1",
    "webpack": "^4.0.0",
    "webpack-cli": "^2.0.14",
    "webpack-dev-server": "^3.0.0"
  }
}

Adicionamos o elemento scripts e também o elemento start. Isto nos permitirá rodar o React com o Webpack Development Server. Se você não especificar um arquivo de configuração, webpack-dev-server procurará pelo arquivo webpack.config.js como a entrada de configuração padrão dentro do diretório raiz.

Agora, chegou o momento da verdade! Digite o seguinte em seu terminal (lembre-se de estar no diretório raiz) e ouse o yarn para chamar nosso script start.

yarn start
iHJNOlQ4O11i7ONPbmXDRkbKF7WAGqZRFcCY
React em execução

Agora temos uma aplicação do React em funcionamento, alimentada por nossa própria configuração de Webpack. No final do GIF acima, estou destacando o pacote de arquivos JavaScript gerado para nós e, como indicamos na configuração, o nome do arquivo tem um hash único, bundle.d505bbab002262a9bc07.js.

Configurando um Hot Module Replacement (HMR)

De volta ao seu terminal, instale React Hot Loader como dependência de desenvolvimento.

yarn add react-hot-loader @hot-loader/react-dom -D

Abra o arquivo .babelrc e acrescente as linhas ["@babel/preset-env", { "modules": false }], (não se esqueça de incluir a vírgula (,) no final da linha) e "react-hot-loader/babel":

{
  "presets": [
    ["@babel/preset-env", { "modules": false }],
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-proposal-class-properties",
    "react-hot-loader/babel"
  ]
}

Abra o arquivo webpack.config.js e modifique-o conforme vemos abaixo.

Estou apenas incluindo o código relevante e omitindo o código que permaneceu o mesmo para auxiliar na brevidade.

...
module.exports = {
  entry: './src/index.js',
  output: {
    ...
    publicPath: '/'
  },
  resolve: {
    alias: {
      "react-dom": "@hot-loader/react-dom",
    },
  },
  ...
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    ...
  ],
  devServer: {
    ...
    hot: true
  }
};
  • publicPath: '/' — O hot reloading (recarregamento rápido) não funcionará como esperado para as rotas aninhadas sem ele
  • webpack.HotModuleReplacementPlugin — Imprime nomes de módulos mais legíveis no terminal do navegador em atualizações do HMR
  • hot: true — Habilita HMR no servidor
  • resolve: alias —  substitui react-dom pelo react-dom customizado do hot-loader

Abra o arquivo index.js e altere-o para o seguinte:

import { hot } from "react-hot-loader/root";
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";

import "./index.css";

const render = (Component) =>
  ReactDOM.render(<Component />, document.getElementById("root"));

render(hot(App));

Agora, estamos prontos para testar o HMR! De volta ao terminal, execute sua aplicação, faça uma mudança e veja como a aplicação é atualizada sem uma atualização de página inteira.

yarn start
tBaz3tT6Vsnxk7ijy1xFl03YFWmktaaktUWH
HMR em ação

Após a atualização do arquivo, a página muda sem uma atualização completa. Para mostrar essa mudança no navegador, seleciono Rendering -> Paint flashing em nas ferramentas de desenvolvedor do Chrome, que destaca as áreas da página que mudaram na cor verde. Destaco também, no Terminal, a mudança do Webpack, enviado para o navegador para que isso aconteça.

Code Splitting

Com o code splitting (divisão de código, em português), em vez de ter sua aplicação em um grande pacote, você pode ter vários pacotes, cada um carregando de modo assíncrono ou em paralelo. Você também pode separar o código de outros fornecedores do código de sua aplicação, o que pode diminuir potencialmente o tempo de carregamento.

Por rota

Há várias maneiras de conseguirmos o code splitting por rota. No entanto, em nosso caso, usaremos o react-imported-component.

Gostaríamos, também, de mostrar um spinner de carregamento quando o usuário navega para uma rota diferente. Essa é uma boa prática, pois não queremos que o usuário fique apenas olhando para uma tela em branco enquanto espera que a nova página seja carregada. Assim, criaremos um componente Loading.

No entanto, se a nova página carregar muito rápido, não queremos que o usuário veja um spinner de carregamento piscando por alguns milissegundos. Assim, vamos atrasar o componente Loading em 300 milissegundos. Para conseguir isso, usaremos o React-Delay-Render.

Comece instalando as duas dependências adicionais.

Em seu terminal, digite:

yarn add react-imported-component react-delay-render

Agora, vamos criar o componente Loading.

Em seu terminal, digite:

touch ./src/components/Loading.js

Abra o arquivo Loading.js e copie o seguinte:

import React from 'react';
import { Loader } from 'semantic-ui-react';
import ReactDelayRender from 'react-delay-render';

const Loading = () => <Loader active size="massive" />;

export default ReactDelayRender({ delay: 300 })(Loading);

Agora que temos o componente Loading, abra o arquivo App.js e modifique-o assim:

import React from 'react';
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom';
import importedComponent from 'react-imported-component';

import Home from './Home';
import Loading from './Loading';

const AsyncDynamicPAge = importedComponent(
  () => import(/* webpackChunkName:'DynamicPage' */ './DynamicPage'),
  {
    LoadingComponent: Loading
  }
);
const AsyncNoMatch = importedComponent(
  () => import(/* webpackChunkName:'NoMatch' */ './NoMatch'),
  {
    LoadingComponent: Loading
  }
);

const App = () => {
  return (
    <Router>
      <div>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/dynamic" component={AsyncDynamicPAge} />
          <Route component={AsyncNoMatch} />
        </Switch>
      </div>
    </Router>
  );
};

export default App;

Isto criará três pacotes, ou partes, uma para o componente DynamicPage, outra para o componente NoMatch, e uma terceira para a aplicação principal.

Vamos também mudar o nome do arquivo de pacote. Abra o arquivo webpack.config.js e modifique-o assim:

...
module.exports = {
  ...
  output: {
    filename: '[name].[hash].js',
    ...
  },
}

É hora de rodar a aplicação e dar uma olhada no code splitting por rota em ação.

yarn start
dNGa5cqDKitNHQAKtH7ZDce3fvrCCWuE1zYh
Code splitting por rota

No GIF, primeiro, destaco as três partes diferentes criadas pelo Webpack no terminal. Depois, destaco que, na execução da aplicação, apenas a parte principal foi carregada. Por fim, vemos que, ao clicar em Navigate to Dynamic Page (Navegar para a página dinâmica), a parte correspondente a essa página é carregada de modo assíncrono.

Vemos também que a parte correspondente à gina não encontrada nunca foi carregada, economizando a largura de banda do usuário.

Por fornecedor

Agora vamos dividir a aplicação por fornecedor (em inglês, vendor). Abra o arquivo webpack.config.js e faça as seguintes mudanças:

...
module.exports = {
  entry: {
    vendor: ['semantic-ui-react'],
    app: './src/index.js'
  },
  ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        },
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  ...
};
  • entry.vendor: ['semantic-ui-react'] — Especifica de qual biblioteca queremos extrair de nossa aplicação principal para a parte do vendor
  • optimization — Se você deixar de fora esta entrada, o Webpack ainda dividirá sua aplicação por fornecedor. Entretanto, notei que os tamanhos dos pacotes eram grandes e, depois de adicionar essa entrada, os tamanhos dos pacotes foram reduzidos significativamente (descobri isso em Webpack 4 migration draft CommonsChunkPlugin -> Initial vendor chunk - texto em inglês)

Observação: anteriormente, o Webpack 3 fazia uso do CommonsChunkPlugin para dividir o código por fornecedor e/ou commons. Porém, ele foi depreciado no Webpack 4 e muitas de suas características estão agora habilitadas por padrão. Com a remoção do CommonsChunkPlugin, foi acrescentado o optimization.splitChunks, para aqueles que precisam de um controle de qualidade sobre sua estratégia de cache (veja isto para ter uma explicação detalhada).

No terminal, inicie a aplicação:

yarn start
L5D5dDH3r5DkNfhN2oilt1BaejnhCYfpdnQw
Code splitting por fornecedor

No terminal, destaco as três partes anteriores e a nova parte do fornecedor (vendor). Depois, quando inspecionamos o HTML, vemos que as partes da aplicação principal e do fornecedor foram carregadas.

Como fizemos várias atualizações na configuração de nosso Webpack, você encontrará abaixo o arquivo completo webpack.config.js.

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const port = process.env.PORT || 3000;
module.exports = {
  mode: 'development',
  entry: {
    vendor: ['semantic-ui-react'],
    app: './src/index.js'
  },
  output: {
    filename: '[name].[hash].js'
  },
  resolve: {
    alias: {
      "react-dom": "@hot-loader/react-dom",
    },
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localsConvention: 'camelCase',
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        },
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ],
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    open: true,
    hot: true
  }
};

Configuração de produção

Renomeie o arquivo de configuração do Webpack de webpack.config.js para webpack.config.development.js. Depois, faça uma cópia dele e nomeie-o como webpack.config.production.js.

No seu terminal, digite o seguinte:

mv webpack.config.js webpack.config.development.js
cp webpack.config.development.js webpack.config.production.js

Vamos precisar de uma dependência de desenvolvimento, Extract Text Plugin. De acordo com sua documentação, ela "move todos os módulos *.css nas partes de entry para um arquivo CSS separado. Portanto, seus estilos não estão mais alinhados no pacote do JS, mas em um arquivo CSS em separado (styles.css). Se seu volume total de folha de estilo for grande, será mais rápido, pois o pacote do CSS é carregado em paralelo ao pacote do JS".

Em seu terminal, digite o seguinte:

yarn add mini-css-extract-plugin -D

Abra o arquivo webpack.config.production.js e faça as seguintes alterações:

Observação: farei algo diferente aqui... vou acrescentar explicações com comentários em linha.
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  mode: 'production',
  entry: {
    vendor: ['semantic-ui-react'],
    app: './src/index.js'
  },
  output: {
    // Queremos criar os pacotes de JavaScript sob um
    // diretório 'static'
    filename: 'static/[name].[hash].js',
    // Caminho absoluto para o diretório de saída desejado. Em nosso 
    // caso um diretório chamado 'dist'.
    // '__dirname' é uma variável de Node que nos dá o absoluto
    // caminho para nosso diretório atual. Então com 'path.resolve', 
    // juntamos diretórios
    // O Webpack 4 assume que seu caminho de saída será './dist',  
    // então você pode simplesmente deixar esta
    // entry de fora.
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/'
  },
  // Mudança para source maps em produção
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },
      {
        test: /\.css$/,
          use: [
            {
              // Nós configuramos 'MiniCssExtractPlugin'              
              loader: MiniCssExtractPlugin.loader,
            }, 
            {
              loader: 'css-loader',
              options: {
                modules: true,
                // Permite configurar quantos carregadores 
                // antes de css-loader deve ser aplicado
                // para recursos @import(ed)
                importLoaders: 1,
                localsConvention: 'camelCase',
                // Cria source maps para arquivos CSS
                sourceMap: true
              }
            },
            {
              // PostCSS funcionará antes do css-loader e 
              // minificar e autoprefixar nossas regras de CSS.
              loader: 'postcss-loader',
            }
          ]
      }
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        },
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    }),
    
    // Criar a folha de estilos no diretório 'styles'
    new MiniCssExtractPlugin({
      filename: 'styles/styles.[hash].css'
    })
  ]
};

Note que removemos a variável port, os plugins relacionados com HMR e o entry devServer.

Também uma vez que adicionamos o PostCSS à configuração de produção, precisamos instalá-lo e criar um arquivo de configuração para ele.

No seu terminal, digite o seguinte:

yarn add postcss-loader autoprefixer cssnano postcss-preset-env -D
touch postcss.config.js

Abra o arquivo postcss.config.js e copie o seguinte:

const postcssPresetEnv = require('postcss-preset-env');
module.exports = {
  plugins: [
    postcssPresetEnv({
      browsers: ['>0.25%', 'not ie 11', 'not op_mini all']
    }),
    require('cssnano')
  ]
};

Aqui, estamos especificando os navegadores para os quais queremos que o autoprefixerdê suporte (consulte a Documentação para mais opções) e minificando a saída do CSS.

Agora, para o último passo antes de criarmos nosso build de produção, precisamos criar um script build no arquivo package.json.

Abra o arquivo e faça as seguintes mudanças na seção scripts:

...
"scripts": {
  "dev":"webpack-dev-server --config webpack.config.development.js",
  "prebuild": "rimraf dist",
  "build": "cross-env NODE_ENV=production webpack -p --config webpack.config.production.js"
},
...

A primeira coisa a notar aqui é que mudamos o script inicial de start para dev, então acrescentamos dois scripts adicionais, prebuild e build.

Finalmente, estamos indicando qual configuração usar quando em desenvolvimento ou produção.

  • prebuild — Será executado antes do script build e excluirá o diretório dist criado por nosso último build de produção. Utilizamos a biblioteca rimraf para isso
  • build — Primeiro, usamos a biblioteca cross-env, no caso de alguém estar usando Windows. Desse modo, a configuração de variáveis de ambiente com NODE_ENV funcionará. Então, chamamos o Webpack com a flag -p para dizer a ele que otimize esse build para produção. Finalmente, especificamos a configuração da produção.

Em seu terminal, instale as duas novas dependências que incluímos no arquivo package.json:

yarn add rimraf cross-env -D

Antes de criar o build de produção, vamos olhar para nossa nova estrutura de projeto:

|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- Loading.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- postcss.config.js
|-- webpack.config.development.js
|-- webpack.config.production.js
|-- yarn.lock

Finalmente, podemos criar nosso pacote de produção.

Em seu terminal, digite o seguinte:

yarn build
T81PYokNYP1m76WiEbGipkFrmGYKee46P4IR
Criação (build) do pacote de produção

Como você notou, depois de executarmos o script build, o Webpack criou um diretório dist contendo nossa aplicação pronta para produção. Agora, inspecione os arquivos que foram criados e perceba que eles estão minificados. Cada um corresponde a um source map. Você também notará que o PostCSS adicionou a autoprefixação ao arquivo CSS.

Agora, pegamos nossos arquivos de produção e disparamos um servidor Node para servir nosso site. Este é o resultado:

A44vaXq-Jc68ClNMgWe5O0o4a3XjIfj3XLrU
Executando o build de produção

Observação: estou usando este servidor no GIF acima para servir nossos arquivos de produção.

Neste ponto, temos duas configurações de Webpack em funcionamento, uma para desenvolvimento e outra para produção. No entanto, como ambas as configurações são muito semelhantes, elas compartilham muitas das mesmas opções. Se quiséssemos adicionar algo mais, teríamos que adicionar aos dois arquivos de configuração. Vamos corrigir este inconveniente.

Composição do Webpack

Vamos iniciar instalando webpack-merge e Chalk como dependências de desenvolvimento.

Em seu terminal, digite o seguinte:

yarn add webpack-merge chalk -D

Também precisaremos de novos diretórios e alguns arquivos novos.

Em seu terminal, digite o seguinte:

mkdir -p build-utils/addons
cd build-utils
touch build-validations.js common-paths.js webpack.common.js webpack.dev.js webpack.prod.js

Agora, vamos olhar para nossa nova estrutura de projeto:

|-- build-utils
    |-- addons
    |-- build-validations.js
    |-- common-paths.js
    |-- webpack.common.js
    |-- webpack.dev.js
    |-- webpack.prod.js
|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- Loading.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- postcss.config.js
|-- webpack.config.development.js
|-- webpack.config.production.js
|-- yarn.lock

Abra o arquivo common-paths.js e copie o seguinte:

const path = require('path');
const PROJECT_ROOT = path.resolve(__dirname, '../');

module.exports = {
  projectRoot: PROJECT_ROOT,
  outputPath: path.join(PROJECT_ROOT, 'dist'),
  appEntry: path.join(PROJECT_ROOT, 'src')
};

Aqui definimos, como o nome sugere, os caminhos comuns para nossas configurações de Webpack. PROJECT_ROOT precisa procurar um diretório acima, já que estamos trabalhando sob o diretório build-utils (um nível abaixo do caminho raiz real em nosso projeto).

Abra o arquivo build-validations.js e copie o seguinte:

const chalk = require('chalk');
const ERR_NO_ENV_FLAG = chalk.red(
  `Você deve passar um --env.env flag em seu build para que o webpack funcione!`
);

module.exports = {
  ERR_NO_ENV_FLAG
};

Mais tarde, quando modificarmos nosso package.json, vamos requerer a flag –env.env nos scripts. Essas validações são para verificar se a bandeira está presente. Caso contrário, ela lançará um erro.

Nos próximos três arquivos, separaremos as configurações do Webpack em configurações que são compartilhadas entre desenvolvimento e produção, configurações que são apenas para desenvolvimento e configurações apenas para produção.

Abra o arquivo webpack.common.js e copie o seguinte:

const commonPaths = require('./common-paths');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const config = {
  entry: {
    vendor: ['semantic-ui-react']
  },
  output: {
    path: commonPaths.outputPath,
    publicPath: '/'
  },
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      }
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        },
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ]
};
module.exports = config;

Basicamente, extraímos o que foi compartilhado entre webpack.config.development.js e webpack.config.production.js e transferimos para este arquivo. Na parte superior, requeremos common-paths.js para definir o output.path.

Abra o arquivo webpack.dev.js e copie o seguinte:

const commonPaths = require('./common-paths');
const webpack = require('webpack');
const port = process.env.PORT || 3000;
const config = {
  mode: 'development',
  entry: {
    app: `${commonPaths.appEntry}/index.js`
  },
  output: {
    filename: '[name].[hash].js'
  },
  resolve: {
    alias: {
      "react-dom": "@hot-loader/react-dom",
    },
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localsConvention: 'camelCase',
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    hot: true,
    open: true
  }
};
module.exports = config;

Esse é o mesmo conceito que o do arquivo anterior. Aqui, extraímos apenas configurações de desenvolvimento.

Abra o arquivo webpack.prod.js e copie o seguinte:

const commonPaths = require('./common-paths');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const config = {
  mode: 'production',
  entry: {
    app: [`${commonPaths.appEntry}/index.js`]
  },
  output: {
    filename: 'static/[name].[hash].js'
  },
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.css$/,
          use: [
            {
              // Configuramos 'MiniCssExtractPlugin'              
              loader: MiniCssExtractPlugin.loader,
            }, 
            {
              loader: 'css-loader',
              options: {
                modules: true,
                importLoaders: 1,
                localsConvention: 'camelCase',
                sourceMap: true
              }
            },
            {
              loader: 'postcss-loader'
            }
          ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'styles/styles.[hash].css'
    })
  ]
};
module.exports = config;

Extraímos aqui apenas as configurações de produção.

Agora que temos as configurações compartilhadas e as específicas para desenvolvimento e produção em arquivos separados, é hora de colocar tudo junto.

No terminal, se você ainda estiver no diretório build-utils, suba um nível até a raiz do projeto, depois apague as configurações anteriores do Webpack e crie uma nova configuração do Webpack. Nomeie como webpack.config.js.

No seu terminal, digite o seguinte:

cd ..
rm webpack.config.development.js webpack.config.production.js
touch webpack.config.js

Antes de configurar o webpack.config.js, vamos abrir o package.json e atualizar a seção de scripts.

Modifique a seção assim:

...
"scripts": {
  "dev": "webpack-dev-server --env.env=dev",
  "prebuild": "rimraf dist",
  "build": "cross-env NODE_ENV=production webpack -p --env.env=prod"
},
...

Uma vez que removemos a flag –config, o Webpack agora procurará a configuração padrão, que é webpack.config.js. Agora, usamos a flag –env para passar uma variável de ambiente para o Webpack, env=dev para desenvolvimento e env=prod para produção.

Abra o arquivo webpack.config.js e copie o seguinte:

Explicações com comentários em linha.

const buildValidations = require('./build-utils/build-validations');
const commonConfig = require('./build-utils/webpack.common');

const webpackMerge = require('webpack-merge');

// Podemos incluir plugins Webpack, através de addons, que fazem 
// com que não precisemos executá-lo toda vez que estamos desenvolvendo.
// Veremos um exemplo quando nos configurarmos 'Bundle Analyzer'
const addons = (/* string | string[] */ addonsArg) => {
  
  // Normalizar array de addons (flatten)
  let addons = [...[addonsArg]] 
    .filter(Boolean); // Se addons é indefinido, filtrá-lo

  return addons.map(addonName =>
    require(`./build-utils/addons/webpack.${addonName}.js`)
  );
};

// 'env' conterá a variável ambiental da seção 'scripts' 
// no arquivo 'package.json'.
// console.log(env); => { env: 'dev' }
module.exports = env => {

  // Usamos 'buildValidations' para checar a flag 'env'
  if (!env) {
    throw new Error(buildValidations.ERR_NO_ENV_FLAG);
  }

  // Selecione a configuração do Webpack a ser usada; desenvolvimento 
  // ou produção
  // console.log(env.env); => dev
  const envConfig = require(`./build-utils/webpack.${env.env}.js`);
  
  // 'webpack-merge' combinará nossas configurações compartilhadas, 
  // as configurações específicas do ambiente e qualquer adição que 
  // estejamos incluindo
  const mergedConfig = webpackMerge(
    commonConfig,
    envConfig,
    ...addons(env.addons)
  );

  // Em seguida, retorna a configuração final para Webpack
  return mergedConfig;
};

Essa pode parecer uma configuração muito extensa, mas, a longo prazo, será útil.

Neste momento, você pode rodar a aplicação ou criar (build) os arquivos de produção. Tudo funcionará como esperado (desculpe, sem GIF desta vez).

yarn dev
yarn build

Observação: esta técnica de "Composição do Webpack" foi retirada da Webpack Academy, um curso gratuito de Sean Larkin que eu recomendo fazer para aprender mais sobre o Webpack. Ele não é específico para o React.

BÔNUS: Configurando um analisador de pacotes do Webpack

Você não precisa necessariamente de um Webpack Bundle Analyzer (analisador de pacotes do Webpack), mas ele vem a calhar quando se trata de otimizar suas criações.

Comece instalando a dependência e criando o arquivo de configuração.

Em seu terminal digite o seguinte:

yarn add webpack-bundle-analyzer -D
touch build-utils/addons/webpack.bundleanalyzer.js

Abra o arquivo webpack.bundleanalyzer.js e copie o seguinte:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
  .BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'server'
    })
  ]
};

Estamos apenas exportando a seção de plug-ins, que inclui o Webpack Bundle Analyzer. Então, o webpack-merge vai combiná-lo na configuração final do Webpack. Lembra-se dos addons no webpack.config.js? Bem, é aqui que eles entram em cena.

Para a etapa final, vamos abrir o package.json e incluir os novos scripts, deste modo:

"scripts": {
  "dev": "webpack-dev-server --env.env=dev",
  "dev:bundleanalyzer": "yarn dev --env.addons=bundleanalyzer",
  "prebuild": "rimraf dist",
  "build": "cross-env NODE_ENV=production webpack -p --env.env=prod",
  "build:bundleanalyzer": "yarn build --env.addons=bundleanalyzer"
},
  • dev:bundleanalyzer — Chama o script dev e passa uma nova variável de ambiente addons=bundleanalyzer
  • build:bundleanalyzer — Chama o script build e passa uma nova variável de ambiente addons=bundleanalyzer

Hora de rodar a aplicação com o addon do Webpack Bundle Analyzer.

Em seu terminal, digite o seguinte:

yarn dev:bundleanalyzer
EFwowawQtLlHdJQGL9gwuz77O2wfhz784xW2
Webpack Bundle Analyzer

A aplicação é lançada juntamente ao Webpack Bundle Analyzer.

Incluir addons com a composição do Webpack pode ser muito útil, pois há muitos plug-ins que você gostaria de usar apenas em determinados momentos.

Conclusão

Antes de mais nada, você pode obter o código completo no repositório do GitHub.

Como você conseguiu chegar até aqui, parabéns! Agora que você conhece o básico (e um pouco mais) sobre o Webpack para o React, você pode continuar explorando e aprendendo características e técnicas mais avançadas.

Obrigado pela leitura. Espero que tenha gostado. Se você tiver alguma dúvida, sugestão ou correção, informe o autor. Não se esqueça de compartilhar o artigo para divulgar.

Você pode seguir o autor no Medium, no Twitter, no GitHub, no LinkedIn ou em todos eles.

Este artigo foi originalmente publicado no blog pessoal do autor.


Atualização do autor, de 25/08/19: Estou criando uma aplicação de orações, chamada "My Quiet Time - A Prayer Journal". Se você quiser saber das atualizações, inscreva-se através do link: http://b.link/mqt.

A aplicação será lançada antes do final do ano. Tenho grandes planos para ela. Para ver algumas imagens do mockup, siga o seguinte link: http://pc.cd/Lpy7

Se tiver alguma dúvida sobre a aplicação, fique à vontade de enviar ao autor pelo Twitter por mensagem direta.