Cada opção, um resultado diferente, ahh Webpack!

As partes confusas do Webpack

Webpack, é o empacotador que está liderando os projetos em React e Redux. E eu acho que o pessoal do Angular 2 e outros frameworks também estão usando ele hoje em dia.

Quando eu vi o arquivo de configuração do Webpack pela primeira vez, ele parecia um documento alien 👽 e muito confuso 😱.

Depois de brincar com ele um pouco, posso dizer agora, que é porquê o Webpack tem uma sintaxe única e novas filosofias que podem causar confusão para iniciantes. Acidentalmente, essas filosofias também são responsáveis por torna-lo tão popular.

Já que é confuso iniciar e entender o Webpack, eu vou escrever alguns posts e tentar tornar isso fácil para os outros começarem a usar todo seu potencial. Essa é a primeira parte.


A filosofia principal do Webpack

Webpack é baseado em duas idéias principais:

  1. Tudo é um módulo: Assim como arquivos JS podem ser “módulos”, tudo (CSS, imagens, HTML) pode ser um módulo. Isso é, você pode require(“meuArquivo.js”) ou require(“meuArquivo.css”). Isso quer dizer que você pode dividir qualquer artefato em pequenas e flexíveis partes, reusá-los e assim por diante.
  2. Carregar o que você precisa, quando você precisa: Normalmente, empacotadores de módulos pegam todos os módulos e geram um único arquivo final, como bundle.js. Mas, em vários projetos do mundo real, esse arquivo final pode ser algo em torno de 10~15MB e pode temorar muito para carregar. Webpack tem várias funcionalidades para dividir seu código e gerar múltiplos arquivos finais e também pode carregar partes do seu projeto assincronamente, assim, podendo carrregar o que você precisa, quando você precisa!

Certo, agora vamos olhar suas partes confusas…


1. Desenvolvimento x Produção

A primeira coisa para ficar atento, é que o Webpack tem inúmeras funcionalidades. Algumas são apenas para desenvolvimento e outras apenas para produção e outras para ambos.

Um exemplo básico de configuração dev x prod.

Normalmente, a maioria dos projetos usam tantas funcionalidades que eles acabam com dois arquivos de configuração. E para criar os pacotes, você irá escrever os comandos no seu arquivo package.json:

“scripts”: {
// npm run para gerar arquivos de produção
“build”: “webpack — config webpack.config.prod.js”,
  // npm run vai gerar arquivos para dev e rodar o servidor
“dev”: “webpack-dev-server”
}

2. Webpack CLI x webpack-dev-server

É importante tomar nota que o Webpack, o empacotador, fornece duas interfaces:

  1. Webpack CLI: A interface padrão (instalada como parte do próprio Webpack)
  2. webpack-dev-server: Um pacote a parte que contém um servidor node.js

Webpack CLI — Ótimo para produção

Essa interface recebe opções pela CLI e também pelo arquivo de configuração (padrão: webpack.config.js) e envia para o Webpack processar.

Talvez você comece a aprender Webpack pela CLI, no final, você só vai usá-la para gerar seus arquivos de produção.

Como usar:

OPÇÃO 1:
// Instalando globalmente
$ npm install webpack — g
// Gera arquivos usando a configuração em webpack.config.js
$ webpack

OPÇÃO 2 :
// Instalando localmente e usando o package.json
$ npm install webpack — save
// Adicionando o comando ao package.json
“scripts”: {
“build”: “webpack — config webpack.config.prod.js -p”,
...
}
// Usando no terminal
$ npm run build

webpack-dev-server — Ótimo para desenvolvimento

Esse é um servidor Express usando node.js rodando na porta 8080. Esse servidor chama o Webpack internamente. O benefício desse servidor, é que ele disponibiliza funcionalidades como: recarregar o navegador automáticamente (do inglês, Live Reloading) e/ou substituir apenas o módulo que foi alterado (do inglês, Hot Module Replacement ou HMR).

Como usar:

OPÇÃO 1:
// Instalando globalmente
$ npm install webpack-dev-server -g
// Usando no terminal
$ webpack-dev-server --inline --hot

OPÇÃO 2:
// Instalando localmente e usando o package.json
$ npm install webpack-dev-server --save
// Adicionando o comando ao package.json
“scripts”: {
“start”: “webpack-dev-server --inline --hot”,
...
}
// Usando no terminal
$ npm start
// Abra o navegador em:
http://localhost:8080

Webpack x webpack-dev-server e suas opções

É interessante saber que algumas opções como inline e hot estão disponíveis apenas no webpack-dev-server. E opções como hide-modules são apenas da CLI.

webpack-dev-server opções da CLI x arquivo de configuração

Outra coisa que precisamos saber é que você pode passar opções para o webpack-dev-server de duas maneiras:

  1. Através do objeto devServer no arquivo de configuração.
  2. Através de argumentos na CLI
// Via CLI
$ webpack-dev-server --hot --inline

// Via webpack.config.js
devServer: {
inline: true,
hot: true
}

Dica: Já percebi, algumas vezes, que as opções da chave devServer (como hot: true e inline: true) não funcionam! Então, eu prefiro passar elas diretamente na CLI no package.json:

//package.json
{
scripts: {
“start”: “webpack-dev-server --hot --inline”
}
}

Atenção: Certifique-se de que você NÃO está passando hot: true e --hot ao mesmo tempo.

webpack-dev-server — “hot” x “inline”

inline adiciona a opção de “recarregamento automático” (do inglês, live reloading), para a página inteira. hot permite habilitar a opção Hot Module Reloading (ou HMR), que tenta recarregar apenas o componente que foi modificado (ao invés da página inteira). Se passarmos as duas opções, então, quando o arquivo for modificado, webpack-dev-server tentará usar HMR como primeira opção, se não funcionar, ele irá recarregar a página inteira.

// Quando o arquivo for modificado, as 3 opções seguintes são válidas
// 1. Não recarrega a página automáticamente
$ webpack-dev-server
// 2. Recarrega a página automáticamente
$ webpack-dev-server --inline
// 3. Recarrega apenas o módulo (HMR) ou a página inteira caso falhe
$ webpack-dev-server --inline --hot

3. Chave “entry” — string x array x objeto

A chave entry fala para o Webpack onde é a raíz de início do nosso módulo. Ela pode ser uma string, um array ou um objeto. Isso pode causar confusão, mas diferentes tipos significam diferentes resultados.

entry — String

Se você utilizar apenas um único ponto de início (como a maioria dos apps), você pode usar qualquer formato e o resultado será o mesmo.

Diferentes entradas, mas resultados iguais.

entry — Array

Mas, se você quiser concatenar múltiplos arquivos que NÃO DEPENDEM UM DO OUTRO, você pode usar um array como entrada.

Por exemplo: talvez você precise adicionar um googleAnalytics.js no seu HTML. Você pode dizer para o Webpack adicionar ele no final do seu arquivo bundle, dessa maneira:

entry — Object

Agora, vamos dizer que você tenha uma verdadeira aplicação com múltiplas páginas e módulos, não uma SPA com múltiplas rotas, mas com múltiplos arquivos HTML (como index.html, profile.html, cart.html). Você pode dizer ao Webpack para gerar múltiplos arquivos bundle de uma só vez usando um objeto como entrada.

A configuração abaixo irá criar dois arquivos finais: indexEntry.js e profileEntry.js que você pode usar no index.html e profile.html respectivamente.

E então no HTML:

//profile.html
<script src=”dist/profileEntry.js”></script>
//index.html
<script src=”dist/indexEntry.js”></script>

Atenção: Perceba que o nome do arquivo vem da chave do objeto entry.

entry — Combinando ambos

Você também pode usar array como valor nas de entrada em objetos. No exemplo abaixo, a configuração irá gerar 3 arquivos: vendor.js que contém três dependências, um index.js e o profile.js.


4. Chave “ouput” — “path” x “publicPath”

A chave output fala para o Webpack onde e como ele deve armazenar os arquivos finais. Ele tem duas propriedades importantes, path e publichPath, que podem gerar confusão.

A chave path simplesmente diz ao Webpack qual caminho ele deve usar para salvar os arquivos finais.

E o publichPath é usado por vários plugins do Webpack para atualizar URLs dentro de arquivos CSS, HTML ou gerar arquivos para produção.

Chave publicPath em Dev x Prod

Por exemplo, no seu CSS, você pode ter uma url que carregue ./test.png no seu localhost. Mas, em produção, esse test.png pode estar armazenado em uma CDN, enquanto seu servidor node.js está rodando no Heroku. Isso quer dizer, que você teria que manualmente atualizar a URL em todos os arquivos e apontar para sua CDN quando enviar os arquivos para produção.

Ao invés disso, você pode usar a chave publicPath e alguns puglins que usam essa propriedade para automáticamente atualizar as URLs para produção.

Você pode clicar na imagem abaixo e ver alguns exemplos:

Exemplo da chave publicPath para Prod
// Dev: Servidor e imagem estão no seu localhost
.image {
background-image: url(./test.png);
}
// Prod: Servidor no Heroku e sua imagem na CDN
.image {
background-image: url(https://someCDN/test.png);
}

5. Usando e encadeando Loaders

Loaders é uma parte essencial do Webpack, eles são módulos node que ajudam a carregar ou importar arquivos de vários tipos transformando-os em arquivos aceitáveis para o navegador, tais como JS, CSS e etc. E tudo isso através de arquivos JS usando require (CommonJS) ou import (ES6).

Por exemplo, você pode usar o babel-loader para converter JS escrito em ES6 para uma versão ES5 aceitável pela maioria dos navegadores:

module: {
loaders: [{
// Expressão regular procurando por arquivos
// que terminem em “.js”, se encontrar, use esse loader
test: /\.js$/,
    // Exclui a pasta node_modules
exclude: /node_modules/,
    // Você pode omitir o “-loader”, atalho para “babel-loader”
loader: ‘babel’
}]
}

Encadeando Loaders (da direita para esquerda)

Múltiplos loaders podem ser encadeados para trabalhar em um mesmo arquivo. O encadeamento funciona da direita para esquerda e os loaders são separados por "!".

Por exemplo, vamos supor que você tenha uma arquivo CSS chamado myCssFile.css e você quer colocar seu conteúdo em uma tag <style> dentro do seu arquivo HTML. Podemos conseguir isso usando dois loaders: css-loader e o style-loader.

module: {
loaders: [{
test: /\.css$/,
loader: 'style!css' // Atalho para style-loader!css-loader
}]
}

E ele funciona assim:

  1. Webpack irá procurar por arquivos CSS que estejam sendo chamados pelo seus módulos. Ou seja, Webpack irá examinar seu arquivo JS e procurar por require(“myCssFile.css”). Se ele encontrar essa dependência, então, o Webpack irá entregar esse arquivo para o primeiro loader, neste caso, o css-loader.
  2. css-loader irá carregar todo o arquivo CSS e suas dependências (caso ele tenha alguma declaração @import) em formato JSON e enviar esse conteúdo para o style-loader.
  3. style-loader por sua vez, vai receber esse JSON e adicionar seu conteúdo a uma tag <style> e injetar automáticamente no seu arquivo HTML.

6. Configurando seus Loaders

Seus loaders podem funcionar de diferentes maneiras dependendo dos parâmetros que você passa.

Nos exemplos abaixo, nós estamos configurando o url-loader para transformar imagens menores de 1024 bytes em DataURLs e, manter as URLs normais para imagens maiores. Nós podemos fazer isso adicionando o parâmetro limit, que podemos configurar de duas maneiras:


7. O arquivo .babelrc

O babel-loader usa algumas configurações pré-definidas em pacotes chamados presets. Alguma delas são os conhecidos presets para ES6/ES5 e também React/JSX. Nós podemos passar a configuração através de parâmetros:

module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
loader: ‘babel’,
query: {
presets: [‘react’, ‘es2015’]
}
}
]
}

Porém, na maioria dos projetos que usam Babel, suas configurações e presets podem ficar bem grandes. Ao invés de manter tudo na configuração do babel-loader, podemos mover essa configuração para o arquivo .babelrc na raíz do seu projeto. babel-loader irá carregar esse arquivo automaticamente se ele existir:

//webpack.config.js
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
loader: ‘babel’
}
]
}
//.bablerc
{
“presets”: [“react”, “es2015”]
}

8. Plugins

Plugins são módulos node adicionais para transformar seu resultado final.

Por exemplo, uglifyJSPlugin recebe seu arquivo bundle.js, minimiza e ofusca seu conteúdo, diminuindo o tamanho final.

Similarmente, extract-text-webpack-plugin, internamente usa o css-loader e style-loader para garantir que todo CSS final seja extraído em um arquivo style.css separado e inclui o link no seu arquivo HTML.

//webpack.config.js
//Procura todos os arquivos .css, combinando eles e extraindo para um único “styles.css”
var ETP = require(“extract-text-webpack-plugin”);
module: {
loaders: [
{
test: /\.css$/,
loader: ETP.extract("style-loader", "css-loader")
}
]
},
plugins: [
//Extrai todo CSS para "styles.css"
new ExtractTextPlugin("styles.css")
]

Nota: Se você quer injetar seu CSS inline no seu arquivo HTML, você pode fazer isso sem o extract-text-webpack-plugin, apenas usando css-loader e style-loader:

module: {
loaders: [{
test: /\.css$/,

// Você pode omitir o “-loader”,
// Atalho para style-loader/css-loader
loader: 'style!css'
}]
}

9. Loaders x Plugins

Se você chegou até aqui, acredito que tenha identificado duas coisas:

  1. Loaders: Trabalham exclusivamente nos arquivos durante ou antes do arquivo final (bundle.js) ser gerado.
  2. Plugins: Trabalham exclusivamente no arquivo final ou pedaços (chunks) e geralmente no final do processo de bundle.

Alguns plugins, como no caso do commonsChunksPlugins, vão além disso, e modificam como o bundle final é criado.


10. Resolvendo extensões de arquivos

Várias configurações do Webpack contêm um objeto de configuração chamado resolve com uma propriedade extensions que recebe uma string vazia, como no exemplo abaixo. Essa string vazia é importante porque ajuda a resolver caminhos de importação sem precisar saber a extensão do arquivo, tais como: require(“./myJSFile”) ou import myJSFile from './myJSFile'.

{
resolve: {
extensions: [‘’, ‘.js’, ‘.jsx’]
}
}

E agora José?

Menos confuso? Eu espero que sim, fica minha recomendação para ler novamente cada tópico e tentar aplicar no seu dia-a-dia.

Apesar de sucinto as dicas, a documentação do Webpack continua sendo um caos.

Obrigado Tobias Koppers (Criador do Webpack) por revisar esse post!

Créditos

Meu muito obrigado! 🙏