Um guia para gerenciar dependências do Webpack
Publicados: 2022-03-11O conceito de modularização é uma parte inerente da maioria das linguagens de programação modernas. JavaScript, no entanto, carecia de qualquer abordagem formal para modularização até a chegada da versão mais recente do ECMAScript ES6.
No Node.js, uma das estruturas JavaScript mais populares da atualidade, os empacotadores de módulos permitem o carregamento de módulos NPM em navegadores da Web, e bibliotecas orientadas a componentes (como React) incentivam e facilitam a modularização do código JavaScript.
O Webpack é um dos empacotadores de módulos disponíveis que processa código JavaScript, bem como todos os ativos estáticos, como folhas de estilo, imagens e fontes, em um arquivo empacotado. O processamento pode incluir todas as tarefas necessárias para gerenciar e otimizar dependências de código, como compilação, concatenação, minificação e compactação.
No entanto, configurar o Webpack e suas dependências pode ser estressante e nem sempre é um processo simples, principalmente para iniciantes.
Esta postagem de blog fornece diretrizes, com exemplos, de como configurar o Webpack para diferentes cenários e aponta as armadilhas mais comuns relacionadas ao agrupamento de dependências de projeto usando o Webpack.
A primeira parte desta postagem de blog explica como simplificar a definição de dependências em um projeto. Em seguida, discutimos e demonstramos a configuração para divisão de código de aplicativos de página única e múltipla. Por fim, discutimos como configurar o Webpack, se quisermos incluir bibliotecas de terceiros em nosso projeto.
Configurando aliases e caminhos relativos
Os caminhos relativos não estão diretamente relacionados às dependências, mas os usamos quando definimos as dependências. Se uma estrutura de arquivo de projeto for complexa, pode ser difícil resolver caminhos de módulo relevantes. Um dos benefícios mais fundamentais da configuração do Webpack é que ela ajuda a simplificar a definição de caminhos relativos em um projeto.
Digamos que temos a seguinte estrutura de projeto:
- Project - node_modules - bower_modules - src - script - components - Modal.js - Navigation.js - containers - Home.js - Admin.js
Podemos fazer referência a dependências por caminhos relativos para os arquivos de que precisamos e, se quisermos importar componentes para contêineres em nosso código-fonte, parece com o seguinte:
Home.js
Import Modal from '../components/Modal'; Import Navigation from '../components/Navigation';
Modal.js
import {datepicker} from '../../../../bower_modules/datepicker/dist/js/datepicker';
Toda vez que queremos importar um script ou módulo, precisamos saber a localização do diretório atual e encontrar o caminho relativo para o que queremos importar. Podemos imaginar como esse problema pode aumentar em complexidade se tivermos um grande projeto com uma estrutura de arquivos aninhada ou quisermos refatorar algumas partes de uma estrutura de projeto complexa.
Podemos lidar facilmente com esse problema com a opção resolve.alias do resolve.alias
. Podemos declarar os chamados aliases – nome de um diretório ou módulo com sua localização, e não dependemos de caminhos relativos no código-fonte do projeto.
webpack.config.js
resolve: { alias: { 'node_modules': path.join(__dirname, 'node_modules'), 'bower_modules': path.join(__dirname, 'bower_modules'), } }
No arquivo Modal.js
, agora podemos importar o datepicker de forma muito mais simples:
import {datepicker} from 'bower_modules/datepicker/dist/js/datepicker';
Divisão de código
Podemos ter cenários em que precisamos anexar um script ao pacote final, ou dividir o pacote final, ou queremos carregar pacotes separados sob demanda. Configurar nosso projeto e configuração do Webpack para esses cenários pode não ser simples.
Na configuração do Webpack, a opção Entry
informa ao Webpack onde é o ponto de partida para o pacote final. Um ponto de entrada pode ter três tipos de dados diferentes: String, Array ou Object.
Se tivermos um único ponto de partida, podemos usar qualquer um desses formatos e obter o mesmo resultado.
Se quisermos anexar vários arquivos, e eles não dependem um do outro, podemos usar um formato Array. Por exemplo, podemos anexar analytics.js
ao final do bundle.js
:
webpack.config.js
module.exports = { // creates a bundle out of index.js and then append analytics.js entry: ['./src/script/index.jsx', './src/script/analytics.js'], output: { path: './build', filename: bundle.js ' } };
Gerenciando vários pontos de entrada
Digamos que temos um aplicativo de várias páginas com vários arquivos HTML, como index.html
e admin.html
. Podemos gerar vários pacotes usando o ponto de entrada como um tipo de objeto. A configuração abaixo gera dois pacotes JavaScript:
webpack.config.js
module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) } };
index.html
<script src=”build/index.js”></script>
admin.html
<script src=”build/admin.js”></script>
Ambos os pacotes JavaScript podem compartilhar bibliotecas e componentes comuns. Para isso, podemos usar CommonsChunkPlugin
, que encontra módulos que ocorrem em várias partes de entrada e cria um pacote compartilhado que pode ser armazenado em cache entre várias páginas.
webpack.config.js
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js'); module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) }, plugins: [commonsPlugin] };
Agora, não devemos esquecer de adicionar <script src="build/common.js"></script>
antes dos scripts empacotados.
Ativando o carregamento lento
O Webpack pode dividir ativos estáticos em partes menores, e essa abordagem é mais flexível do que a concatenação padrão. Se tivermos um grande aplicativo de página única (SPA), a concatenação simples em um pacote não é uma boa abordagem porque carregar um pacote enorme pode ser lento e os usuários geralmente não precisam de todas as dependências em cada exibição.
Explicamos anteriormente como dividir um aplicativo em vários pacotes, concatenar dependências comuns e se beneficiar do comportamento de cache do navegador. Essa abordagem funciona muito bem para aplicativos de várias páginas, mas não para aplicativos de página única.
Para o SPA, devemos fornecer apenas os ativos estáticos necessários para renderizar a exibição atual. O roteador do lado do cliente na arquitetura SPA é o local perfeito para lidar com a divisão de código. Quando o usuário insere uma rota, podemos carregar apenas as dependências necessárias para a visualização resultante. Alternativamente, podemos carregar dependências à medida que o usuário rola uma página.
Para isso, podemos usar as funções require.ensure
ou System.import
, que o Webpack pode detectar estaticamente. O Webpack pode gerar um pacote separado com base nesse ponto de divisão e chamá-lo sob demanda.
Neste exemplo, temos dois containers React; uma visualização de administrador e uma visualização de painel.
admin.jsx
import React, {Component} from 'react'; export default class Admin extends Component { render() { return <div > Admin < /div>; } }
dashboard.jsx
import React, {Component} from 'react'; export default class Dashboard extends Component { render() { return <div > Dashboard < /div>; } }
Se o usuário inserir a URL /dashboard
ou /admin
, apenas o pacote JavaScript necessário correspondente será carregado. Abaixo podemos ver exemplos com e sem o roteador do lado do cliente.
index.jsx
if (window.location.pathname === '/dashboard') { require.ensure([], function() { require('./containers/dashboard').default; }); } else if (window.location.pathname === '/admin') { require.ensure([], function() { require('./containers/admin').default; }); }
index.jsx
ReactDOM.render( <Router> <Route path="/" component={props => <div>{props.children}</div>}> <IndexRoute component={Home} /> <Route path="dashboard" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/dashboard').default) }, "dashboard")}} /> <Route path="admin" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/admin').default) }, "admin")}} /> </Route> </Router> , document.getElementById('content') );
Extraindo estilos em pacotes separados
No Webpack, carregadores, como style-loader
e css-loader
, pré-processam as folhas de estilo e as incorporam ao pacote JavaScript de saída, mas em alguns casos, eles podem causar o Flash de conteúdo sem estilo (FOUC).

Podemos evitar o FOUC com ExtractTextWebpackPlugin
que permite a geração de todos os estilos em pacotes CSS separados em vez de tê-los incorporados no pacote JavaScript final.
webpack.config.js
var ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { module: { loaders: [{ test: /\.css/, loader: ExtractTextPlugin.extract('style', 'css')' }], }, plugins: [ // output extracted CSS to a file new ExtractTextPlugin('[name].[chunkhash].css') ] }
Lidando com bibliotecas e plugins de terceiros
Muitas vezes, precisamos usar bibliotecas de terceiros, vários plugins ou scripts adicionais, porque não queremos perder tempo desenvolvendo os mesmos componentes do zero. Existem muitas bibliotecas e plugins legados disponíveis que não são mantidos ativamente, não entendem os módulos JavaScript e assumem a presença de dependências globalmente sob nomes predefinidos.
Abaixo estão alguns exemplos com plugins jQuery, com uma explicação de como configurar o Webpack corretamente para poder gerar o pacote final.
ProvidePlugin
A maioria dos plugins de terceiros depende da presença de dependências globais específicas. No caso do jQuery, os plugins dependem da variável $
ou jQuery
que está sendo definida, e podemos usar plugins jQuery chamando $('div.content').pluginFunc()
em nosso código.
Podemos usar o plug-in ProvidePlugin do ProvidePlugin
para preceder var $ = require("jquery")
toda vez que encontrar o identificador global $
.
webpack.config.js
webpack.ProvidePlugin({ '$': 'jquery', })
Quando o Webpack processa o código, ele procura a presença $
e fornece uma referência às dependências globais sem importar o módulo especificado pela função require
.
Carregador de importações
Alguns plugins jQuery assumem $
no namespace global ou confiam que this
seja o objeto window
. Para isso, podemos usar imports-loader
que injeta variáveis globais nos módulos.
example.js
$('div.content').pluginFunc();
Então, podemos injetar a variável $
no módulo configurando o imports-loader
:
require("imports?$=jquery!./example.js");
Isso simplesmente precede var $ = require("jquery");
para example.js
.
No segundo caso de uso:
webpack.config.js
module: { loaders: [{ test: /jquery-plugin/, loader: 'imports?jQuery=jquery,$=jquery,this=>window' }] }
Usando o símbolo =>
(não confundir com as funções ES6 Arrow), podemos definir variáveis arbitrárias. O último valor redefine a variável global this
para apontar para o objeto window
. É o mesmo que envolver todo o conteúdo do arquivo com a (function () { ... }).call(window);
e chamando this
função com window
como argumento.
Também podemos exigir bibliotecas usando o formato de módulo CommonJS ou AMD:
// CommonJS var $ = require("jquery"); // jquery is available // AMD define(['jquery'], function($) { // jquery is available });
Algumas bibliotecas e módulos podem suportar diferentes formatos de módulo.
No próximo exemplo, temos um plugin jQuery que usa o formato de módulo AMD e CommonJS e tem uma dependência do jQuery:
jquery-plugin.js
(function(factory) { if (typeof define === 'function' && define.amd) { // AMD format is used define(['jquery'], factory); } else if (typeof exports === 'object') { // CommonJS format is used module.exports = factory(require('jquery')); } else { // Neither AMD nor CommonJS used. Use global variables. } });
webpack.config.js
module: { loaders: [{ test: /jquery-plugin/, loader: "imports?define=>false,exports=>false" }] }
Podemos escolher qual formato de módulo queremos usar para a biblioteca específica. Se declararmos define
como igual false
, o Webpack não analisa o módulo no formato de módulo AMD, e se declaramos as exports
de variável como igual false
, o Webpack não analisa o módulo no formato de módulo CommonJS.
Carregador de exposição
Se precisarmos expor um módulo ao contexto global, podemos usar expose-loader
. Isso pode ser útil, por exemplo, se tivermos scripts externos que não fazem parte da configuração do Webpack e se basearem no símbolo no namespace global, ou se usarmos plugins do navegador que precisam acessar um símbolo no console do navegador.
webpack.config.js
module: { loaders: [ test: require.resolve('jquery'), loader: 'expose-loader?jQuery!expose-loader?$' ] }
A biblioteca jQuery agora está disponível no namespace global para outros scripts na página da web.
window.$ window.jQuery
Configurando dependências externas
Se quisermos incluir módulos de scripts hospedados externamente, precisamos defini-los na configuração. Caso contrário, o Webpack não poderá gerar o pacote final.
Podemos configurar scripts externos usando a opção externals
na configuração do Webpack. Por exemplo, podemos usar uma biblioteca de uma CDN por meio de uma tag <script>
separada, enquanto ainda a declaramos explicitamente como uma dependência de módulo em nosso projeto.
webpack.config.js
externals: { react: 'React', 'react-dom': 'ReactDOM' }
Suporte a várias instâncias de uma biblioteca
É ótimo usar o gerenciador de pacotes NPM no desenvolvimento front-end para gerenciar bibliotecas e dependências de terceiros. No entanto, às vezes podemos ter várias instâncias da mesma biblioteca com versões diferentes e elas não funcionam bem juntas em um ambiente.
Isso pode acontecer, por exemplo, com a biblioteca React, onde podemos instalar o React a partir do NPM e posteriormente uma versão diferente do React pode ser disponibilizada com algum pacote ou plugin adicional. A estrutura do nosso projeto pode ter a seguinte aparência:
project | |-- node_modules | |-- react |-- react-plugin | |--node_modules | |--react
Componentes vindos do react-plugin
têm uma instância React diferente do resto dos componentes do projeto. Agora temos duas cópias separadas do React, e elas podem ser versões diferentes. Em nosso aplicativo, esse cenário pode atrapalhar nosso DOM mutável global e podemos ver mensagens de erro no log do console da web. A solução para este problema é ter a mesma versão do React em todo o projeto. Podemos resolvê-lo por aliases do Webpack.
webpack.config.js
module.exports = { resolve: { alias: { 'react': path.join(__dirname, './node_modules/react'), 'react/addons': path.join(__dirname, '/node_modules/react/addons'), } } }
Quando o react react-plugin
tenta requerer o React, ele usa a versão no node_modules
do projeto. Se quisermos descobrir qual versão do React usamos, podemos adicionar console.log(React.version)
no código-fonte.
Foco no desenvolvimento, não na configuração do Webpack
Este post apenas arranha a superfície do poder e utilidade do Webpack.
Existem muitos outros carregadores e plugins do Webpack que o ajudarão a otimizar e agilizar o agrupamento de JavaScript.
Mesmo se você for um iniciante, este guia oferece uma base sólida para começar a usar o Webpack, o que permitirá que você se concentre mais no desenvolvimento e menos na configuração de pacotes.