Desenvolvimento

6 fev, 2017

Webpack para React: o guia final

Publicidade

O React, criado pelo Facebook, é uma das formas mais modernas de se criar aplicações web. No último artigo, falei como organizar aplicações com React. Agora, vamos aprender a configurar o Webpack, o bundler mais usado pela comunidade React, tanto para um ambiente de desenvolvimento, quanto para produção.

Continue lendo para aprender:

  • O que é um module bundler;
  • Como funciona o Webpack;
  • Como criar um build de desenvolvimento/produção.

Module Bundler

Os browsers atuais foram construídos para interpretar somente HTML, CSS e JavaScript.

Porém, atualmente temos muitas opções para desenvolver aplicações web: TypeScript, ES2015, CoffeeScript… Só pra citar alguns exemplos. Por isso, pode não fazer mais tanto sentido ficarmos presos na velha combinação HTML, CSS e JavaScript. E é aí que entra o Webpack.

A função principal dele é analisar o código que você escreveu, independente se for em CoffeeScript, ES2015 ou TypeScript, e transformar em JavaScript puro, que é entendido pelos browsers. Não só isso, ele faz a mesma coisa com HTML e CSS. E mais: ele também é uma ferramenta que nos auxilia no desenvolvimento da aplicação.

Hello World

Pra começar, vamos construir a versão mais simples possível de uma aplicação com React: um Hello World.

Começando um novo projeto com o npm:

mkdir webpack-react-example
cd webpack-react-example
npm init

Aceite todos os valores defaults do npm.

Agora, vamos instalar o React:

npm install --save react react-dom

E os pacotes que precisaremos para fazer o build inicial do Webpack:

npm install --save-dev webpack babel-core babel-loader babel-preset-react babel-preset-es2015

Agora, vamos criar o index.html, o arquivo principal do qual a aplicação importará todo o JavaScript referente à aplicação:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Webpack React Example</title>
</head>
<body>
  <div id="app"></div>
  <script src="bundle.js"></script>
</body>
</html>

Repare na linha 9, na qual o arquivo bundle.js é importado. Esse arquivo (que ainda não existe) será basicamente todo o JavaScript da aplicação, exceto no build de produção que falarei mais para frente.

Para que esse arquivo seja criado, precisaremos do Webpack. Portanto, vamos criar o arquivo webpack.config.js, que será o arquivo de configuração do Webpack:

module.exports = {
  entry: './app.js',
  output: {
    filename: 'bundle.js',
  },
};

Essa é a configuração mínima para uma aplicação com Webpack. Ela precisa de um arquivo de entrada (entry) e um de saída (output).

No caso, estamos apontando para o arquivo app.js (que ainda vamos criar) e ele vai gerar automaticamente o arquivo bundle.js, que já falamos anteriormente.

Vamos também alterar o package.json, para que possamos rodar o Webpack e gerar o build:

{
  "name": "webpack-react-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^15.4.1",
    "react-dom": "^15.4.1"
  },
  "devDependencies": {
    "babel-core": "^6.18.2",
    "babel-loader": "^6.2.8",
    "babel-preset-es2015": "^6.18.0",
    "babel-preset-react": "^6.16.0",
    "webpack": "^1.13.3"
  }
}

Repare na linha 7. Para rodarmos o Webpack, precisamos de um simples:

npm start

Agora, vamos criar o arquivo que falta, o app.js, que terá o Hello World em React:

import React from 'react';
import ReactDOM from 'react-dom';
 
ReactDOM.render(<h1>Hello World</h1>, document.getElementById('app'));

Esse código, apesar de muito simples, possui duas limitações que nossos browsers não conseguem interpretar:

  • Código em React
  • Código em ES2015

Por isso, precisamos voltar ao nosso arquivo de configuração do Webpack para usar loaders que transformem esse código em JavaScript puro, que é entendido pelos browsers:

module.exports = {
  entry: "./app.js",
  output: {
    filename: "bundle.js"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['react', 'es2015']
        }
      }
    ]
  }
}

Pode ter ficado complicado agora, mas vamos passo a passo… Primeiro, vamos pegar todos os arquivos que terminem com .js:

test: /\.js$/

Depois, vamos excluir todos os arquivos que vêm da pasta node_modules, porque não faz sentido tocarmos em libs externas:

exclude: /node_modules/

Agora, basta informar que vamos usar o Babel como loader principal:

loader: 'babel'

E pra fechar, vamos setar os dois loaders que precisamos, o babel-preset-es2015, e o babel-preset-react:

query: {
  presets: ['react', 'es2015']
}

Pronto. Agora basta digitar o comando para rodar o Webpack:

npm start

E abrir o arquivo index.html para visualizar o Hello World:

Melhorando o build de desenvolvimento

Agora que já temos uma versão mínima, vamos melhorar o ambiente de desenvolvimento adicionando o servidor do Webpack, que rodará a aplicação em alguma porta do localhost para que não precisemos abrir o index.html na mão.

Para isso, vamos instalar o servidor:

npm install --save-dev webpack-dev-server

E agora, basta mudarmos uma linha no package.json:

"scripts": {
  "start": "webpack-dev-server"
},

Agora só falta digitar o mesmo comando anterior:

npm start

Podemos visualizar a aplicação em localhost:

Mas ainda podemos fazer bem mais do que isso, com um simples comando.

Hot Reload

Se desejarmos que toda vez que modificamos um arquivo, o browser seja automaticamente atualizado com essa mudança, sem a necessidade de um refresh manual, podemos alterar o package.json dessa forma:

"scripts": {
  "start": "webpack-dev-server --inline --hot"
},

Agora, falta apenas mais um detalhe para ter uma versão completa do ambiente de desenvolvimento.

Vamos instalar um plugin para adicionar o bundle do JavaScript que é gerado automaticamente no index.html:

npm install --save-dev html-webpack-plugin

Vamos remover a inserção manual do bundle.js:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Webpack React Example</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>

E alterar a configuração do Webpack para utilizar o plugin:

var HtmlWebpackPlugin = require('html-webpack-plugin');
 
var HTMLWebpackPluginConfig = new HtmlWebpackPlugin({ template: 'index.html' });
 
module.exports = {
  entry: "./app.js",
  output: {
    filename: "bundle.js"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['react', 'es2015']
        }
      }
    ]
  },
  plugins: [HTMLWebpackPluginConfig]
}

Pra entender melhor essas alterações:

  • Na linha 1 importamos o plugin;
  • Na linha 3 configuramos o plugin para injetar o bundle.js no arquivo index.html;
  • Na linha 22 dizemos pro Webpack que estamos utilizando o plugin.

E com isso, temos uma versão bem completa de um ambiente de desenvolvimento com React e Webpack.

Se precisar de mais mudanças, como alterar a porta ou rodar o servidor com https, basta ler a documentação do servidor do Webpack.

Separando as configurações

Antes de começarmos a desenvolver o build de produção da aplicação, vamos aprender a separar arquivos de configuração do Webpack da forma mais simples possível.

Primeiro, vamos renomear o arquivo do build de desenvolvimento que já criamos de webpack.config.js para webpack.dev.config.js:

var HtmlWebpackPlugin = require('html-webpack-plugin');
 
var HTMLWebpackPluginConfig = new HtmlWebpackPlugin({ template: 'index.html' });
 
module.exports = {
  entry: "./app.js",
  output: {
    filename: "bundle.js"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['react', 'es2015']
        }
      }
    ]
  },
  plugins: [HTMLWebpackPluginConfig]
}

Agora, vamos criar novamente o arquivo webpack.config.js e colocar o seguinte código:

var devConfig = require('./webpack.dev.config.js');
 
var config;
 
switch (process.env.npm_lifecycle_event) {
  case 'start':
    config = devConfig;
    break;
  default:
    config = devConfig;
    break;
}
 
module.exports = config;

Pode ter ficado confuso, então, vamos por partes.

Primeiro, importamos a configuração do ambiente de desenvolvimento, que já havíamos criado:

var devConfig = require('./webpack.dev.config.js');

Depois criamos um switch que vai ler qual comando npm usamos:

switch (process.env.npm_lifecycle_event)

E depois, criamos um case para cada cenário possível. Como, por enquanto, ainda não criamos o build de produção, apenas o de desenvolvimento será usado.

Build de produção

Pra começar, vamos alterar o package.json para adicionar um npm script que invoque o build de produção:

"scripts": {
  "start": "webpack-dev-server --inline --hot",
  "build": "webpack"
},

Agora, vamos pro arquivo principal do Webpack e adicionar o novo cenário:

var devConfig = require('./webpack.dev.config.js');
var prodConfig = require('./webpack.prod.config.js');
 
var config;
 
switch (process.env.npm_lifecycle_event) {
  case 'start':
    config = devConfig;
    break;
  case 'build':
    config = prodConfig;
    break;
  default:
    config = devConfig;
    break;
}
 
module.exports = config;

Basta criar o arquivo novo de configuração para o ambiente de produção (webpack.prod.config.js). Para isso, vamos usar como base o arquivo de desenvolvimento:

var HtmlWebpackPlugin = require('html-webpack-plugin');
 
var HTMLWebpackPluginConfig = new HtmlWebpackPlugin({ template: 'index.html' });
 
module.exports = {
  entry: "./app.js",
  output: {
    filename: "bundle.js"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['react', 'es2015']
        }
      }
    ]
  },
  plugins: [HTMLWebpackPluginConfig]
}

Variável de ambiente

A primeira diferença do build de produção que vamos fazer é adicionar uma variável de ambiente de produção. Não precisamos instalar nenhum plugin externo, o Webpack já vem com essa possibilidade built-in.

Repare nas linhas 4 até 8:

var Webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
 
var DefinePlugin = new Webpack.DefinePlugin({
  'process.env': {
    NODE_ENV: JSON.stringify('production'),
  },
});
var HTMLWebpackPluginConfig = new HtmlWebpackPlugin({ template: 'index.html' });
 
module.exports = {
  entry: "./app.js",
  output: {
    filename: "bundle.js"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['react', 'es2015']
        }
      }
    ]
  },
  plugins: [DefinePlugin, HTMLWebpackPluginConfig]
}

Minificação

Agora vamos adicionar a minificação dos arquivos. Com o Webpack isso é extremamente fácil, basta uma linha com o UglifyJsPlugin.

Repare na linha 10:

var Webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
 
var DefinePlugin = new Webpack.DefinePlugin({
  'process.env': {
    NODE_ENV: JSON.stringify('production'),
  },
});
var HTMLWebpackPluginConfig = new HtmlWebpackPlugin({ template: 'index.html' });
var UglifyPlugin = new Webpack.optimize.UglifyJsPlugin({ compress: { warnings: false }});
 
module.exports = {
  entry: "./app.js",
  output: {
    filename: "bundle.js"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['react', 'es2015']
        }
      }
    ]
  },
  plugins: [DefinePlugin, HTMLWebpackPluginConfig, UglifyPlugin]
}

Dedupe

Ao usar bibliotecas externas, como o lodash, o seu código pode ter dependências duplicadas. O Webpack pode encontrar essas dependências duplicadas e remover as redundâncias com o DedupePlugin.

Repare na linha 11:

var Webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
 
var DefinePlugin = new Webpack.DefinePlugin({
  'process.env': {
    NODE_ENV: JSON.stringify('production'),
  },
});
var HTMLWebpackPluginConfig = new HtmlWebpackPlugin({ template: 'index.html' });
var UglifyPlugin = new Webpack.optimize.UglifyJsPlugin({ compress: { warnings: false }});
var DedupePlugin = new Webpack.optimize.DedupePlugin();
 
module.exports = {
  entry: "./app.js",
  output: {
    filename: "bundle.js"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['react', 'es2015']
        }
      }
    ]
  },
  plugins: [DefinePlugin, HTMLWebpackPluginConfig, UglifyPlugin, DedupePlugin]
}

Code Splitting

Em algumas circunstâncias, especialmente em aplicações grandes, não é muito eficiente colocar todo o JavaScript em um arquivo único.

Como as circunstâncias variam de aplicação para aplicação, vou mostrar o caso mais comum de code splitting com o Webpack: separar o código da sua aplicação do código de bibliotecas externas. Para isso, vamos usar o CommonChunksPlugin.

Linhas 12 até 20:

var Webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
 
var DefinePlugin = new Webpack.DefinePlugin({
  'process.env': {
    NODE_ENV: JSON.stringify('production'),
  },
});
var HTMLWebpackPluginConfig = new HtmlWebpackPlugin({ template: 'index.html' });
var UglifyPlugin = new Webpack.optimize.UglifyJsPlugin({ compress: { warnings: false }});
var DedupePlugin = new Webpack.optimize.DedupePlugin();
var CommonChunksPlugin = new Webpack.optimize.CommonsChunkPlugin({ name: 'vendor' });
 
module.exports = {
  entry: {
    vendor: ['react', 'react-dom'],
    app: './app.js',
  },
  output: {
    filename: '[name].js',
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['react', 'es2015']
        }
      }
    ]
  },
  plugins: [DefinePlugin, HTMLWebpackPluginConfig, UglifyPlugin, DedupePlugin, CommonChunksPlugin]
}

Essa parte é um pouco mais complicada, então vou explicar linha a linha.

Na linha 12, configuramos o CommonChunks para ser nomeado como vendor, que é uma string comum quando queremos nos referir a pacotes externos.

Na linha 15, separamos os arquivos de entrada (entry) em dois:

  • app (o próprio código que escrevemos)
  • vendor (bibliotecas externas, no nosso caso react e react-dom)

E por último, alteramos a linha 20, para que o filename seja dinâmico.

Com isso, temos os dois arquivos importados automaticamente no nosso index.html: