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:
Caching
Uma das vantagens de se separar em múltiplos arquivos, citado no passo anterior, é o cache.
Por exemplo, se fizermos alguma alteração no código da nossa aplicação, o usuário final não precisa fazer o download novamente das bibliotecas externas, somente do novo código que foi adicionado.
Para que isso funcione, precisamos apenas adicionar chunkhashes nos JavaScripts finais.
Linhas 12 a 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({ names: ['vendor', 'manifest'] }); module.exports = { entry: { vendor: ['react', 'react-dom'], app: './app.js', }, output: { filename: '[name].[chunkhash].js', chunkFilename: '[chunkhash].bundle.js', }, module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel', query: { presets: ['react', 'es2015'] } } ] }, plugins: [DefinePlugin, HTMLWebpackPluginConfig, UglifyPlugin, DedupePlugin, CommonChunksPlugin] }
E com isso temos um build final de produção feito com o Webpack.
Se desejar ver o código completo da aplicação que foi construída, é só entrar aqui.
E se ficou alguma dúvida ou tem algum comentário a fazer, aproveite os campos abaixo!
***
Este post foi originalmente publicado no Medium pessoal do autor. Acesse aqui.
***
Artigo publicado originalmente em: http://www.concretesolutions.com.br/2017/01/25/webpack-para-react-o-guia-final/