Webpack ou Browserify & Gulp: Qual é melhor?

Publicados: 2022-03-11

À medida que os aplicativos da Web se tornam cada vez mais complexos, tornar seu aplicativo da Web escalável se torna de extrema importância. Enquanto no passado escrever JavaScript e jQuery ad-hoc seria suficiente, hoje em dia construir um aplicativo web requer um grau muito maior de disciplina e práticas formais de desenvolvimento de software, tais como:

  • Testes de unidade para garantir que as modificações no seu código não interrompam a funcionalidade existente
  • Linting para garantir um estilo de codificação consistente e livre de erros
  • Compilações de produção que diferem das compilações de desenvolvimento

A web também oferece alguns de seus próprios desafios de desenvolvimento exclusivos. Por exemplo, como as páginas da Web fazem muitas solicitações assíncronas, o desempenho do seu aplicativo da Web pode ser significativamente prejudicado por ter que solicitar centenas de arquivos JS e CSS, cada um com sua própria pequena sobrecarga (cabeçalhos, handshakes e assim por diante). Esse problema específico geralmente pode ser resolvido agrupando os arquivos, portanto, você está solicitando apenas um único arquivo JS e CSS agrupado em vez de centenas de arquivos individuais.

Compensações de ferramentas de agrupamento: Webpack vs Browserify

Qual ferramenta de agrupamento você deve usar: Webpack ou Browserify + Gulp? Aqui está o guia para escolher.
Tweet

Também é bastante comum usar pré-processadores de linguagem como SASS e JSX que compilam para JS e CSS nativos, bem como transpiladores JS como Babel, para se beneficiar do código ES6, mantendo a compatibilidade com o ES5.

Isso equivale a um número significativo de tarefas que não têm nada a ver com escrever a lógica do próprio aplicativo Web. É aí que entram os executores de tarefas. O objetivo de um executor de tarefas é automatizar todas essas tarefas para que você possa se beneficiar de um ambiente de desenvolvimento aprimorado enquanto se concentra na escrita de seu aplicativo. Depois que o executor de tarefas estiver configurado, tudo o que você precisa fazer é invocar um único comando em um terminal.

Usarei o Gulp como um executor de tarefas porque é muito amigável ao desenvolvedor, fácil de aprender e facilmente compreensível.

Uma rápida introdução ao Gulp

A API do Gulp consiste em quatro funções:

  • gulp.src
  • gulp.dest
  • gulp.task
  • gulp.watch

Como funciona o Gulp

Aqui, por exemplo, está uma tarefa de exemplo que faz uso de três dessas quatro funções:

 gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) });

Quando my-first-task é executada, todos os arquivos correspondentes ao padrão glob /public/js/**/*.js são reduzidos e então transferidos para uma pasta de build .

A beleza disso está no encadeamento .pipe() . Você pega um conjunto de arquivos de entrada, canaliza-os por meio de uma série de transformações e retorna os arquivos de saída. Para tornar as coisas ainda mais convenientes, as transformações de tubulação reais, como minify() , geralmente são feitas por bibliotecas NPM. Como resultado, é muito raro na prática que você precise escrever suas próprias transformações além de renomear arquivos no pipe.

O próximo passo para entender o Gulp é entender a matriz de dependências de tarefas.

 gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });

Aqui, my-second-task só executa a função de retorno de chamada depois que as tarefas lint e bundle são concluídas. Isso permite a separação de interesses: você cria uma série de pequenas tarefas com uma única responsabilidade, como converter LESS em CSS e cria uma espécie de tarefa mestre que simplesmente chama todas as outras tarefas por meio do array de dependências de tarefas.

Finalmente, temos gulp.watch , que observa um padrão de arquivo glob para alterações e, quando uma alteração é detectada, executa uma série de tarefas.

 gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) })

No exemplo acima, qualquer alteração em um arquivo correspondente a /public/js/**/*.js acionaria a tarefa lint e reload . Um uso comum do gulp.watch é acionar recargas ao vivo no navegador, um recurso tão útil para o desenvolvimento que você não conseguirá viver sem ele depois de experimentá-lo.

E assim, você entende tudo o que realmente precisa saber sobre o gulp .

Onde o Webpack se encaixa?

Como funciona o Webpack

Ao usar o padrão CommonJS, agrupar arquivos JavaScript não é tão simples quanto concatená-los. Em vez disso, você tem um ponto de entrada (geralmente chamado index.js ou app.js ) com uma série de instruções require ou import na parte superior do arquivo:

ES5

 var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');

ES6

 import Component1 from './components/Component1'; import Component2 from './components/Component2';

As dependências precisam ser resolvidas antes do código restante em app.js e essas dependências podem ter outras dependências para resolver. Além disso, você pode require a mesma dependência em vários locais em seu aplicativo, mas deseja resolver essa dependência apenas uma vez. Como você pode imaginar, uma vez que você tenha uma árvore de dependência com alguns níveis de profundidade, o processo de empacotar seu JavaScript se torna bastante complexo. É aqui que entram os bundlers como Browserify e Webpack.

Por que os desenvolvedores estão usando o Webpack em vez do Gulp?

O Webpack é um empacotador, enquanto o Gulp é um executor de tarefas, então você esperaria ver essas duas ferramentas comumente usadas juntas. Em vez disso, há uma tendência crescente, especialmente entre a comunidade React, de usar o Webpack em vez do Gulp. Por que é isso?

Simplificando, o Webpack é uma ferramenta tão poderosa que já pode executar a grande maioria das tarefas que você faria por meio de um executor de tarefas. Por exemplo, o Webpack já oferece opções para minificação e mapas de origem para seu pacote. Além disso, o Webpack pode ser executado como middleware por meio de um servidor personalizado chamado webpack-dev-server , que suporta tanto o recarregamento ao vivo quanto o recarregamento a quente (falaremos sobre esses recursos mais tarde). Ao usar carregadores, você também pode adicionar transpilação ES6 a ES5 e pré e pós-processadores CSS. Isso realmente deixa os testes de unidade e linting como tarefas principais que o Webpack não pode lidar de forma independente. Dado que reduzimos pelo menos meia dúzia de tarefas de gulp em potencial para duas, muitos desenvolvedores optam por usar scripts NPM diretamente, pois isso evita a sobrecarga de adicionar o Gulp ao projeto (sobre o qual também falaremos mais tarde) .

A principal desvantagem de usar o Webpack é que ele é bastante difícil de configurar, o que não é atraente se você estiver tentando colocar um projeto em funcionamento rapidamente.

Nossas 3 configurações do executor de tarefas

Vou configurar um projeto com três configurações diferentes de executores de tarefas. Cada configuração executará as seguintes tarefas:

  • Configure um servidor de desenvolvimento com recarregamento ao vivo nas alterações de arquivos observadas
  • Agrupe nossos arquivos JS e CSS (incluindo transpilação de ES6 para ES5, conversão de SASS para CSS e mapas de origem) de maneira escalável nas alterações de arquivos observadas
  • Execute testes de unidade como uma tarefa autônoma ou no modo de observação
  • Execute o linting como uma tarefa independente ou no modo de observação
  • Forneça a capacidade de executar todos os itens acima por meio de um único comando no terminal
  • Tenha outro comando para criar um pacote de produção com minificação e outras otimizações

Nossas três configurações serão:

  • Gulp + Browserify
  • Gulp + Webpack
  • Webpack + Scripts NPM

O aplicativo usará o React para o front-end. Originalmente, eu queria usar uma abordagem agnóstica de framework, mas usar o React na verdade simplifica as responsabilidades do executor de tarefas, já que apenas um arquivo HTML é necessário, e o React funciona muito bem com o padrão CommonJS.

Abordaremos os benefícios e as desvantagens de cada configuração para que você possa tomar uma decisão informada sobre o tipo de configuração que melhor atende às necessidades do seu projeto.

Configurei um repositório Git com três branches, um para cada abordagem (link). Testar cada configuração é tão simples quanto:

 git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)

Vamos examinar o código em cada branch em detalhes…

Código Comum

Estrutura de pastas

 - app - components - fonts - styles - index.html - index.js - index.test.js - routes.js

index.html

Um arquivo HTML simples. O aplicativo React é carregado em <div></div> e usamos apenas um único arquivo JS e CSS empacotado. Na verdade, em nossa configuração de desenvolvimento do Webpack, nem precisaremos bundle.css .

index.js

Isso funciona como o ponto de entrada JS do nosso aplicativo. Essencialmente, estamos apenas carregando o React Router no app div com id que mencionamos anteriormente.

route.js

Este arquivo define nossas rotas. As urls / , /about e /contact são mapeadas para os componentes HomePage , AboutPage e ContactPage , respectivamente.

index.test.js

Esta é uma série de testes de unidade que testam o comportamento do JavaScript nativo. Em um aplicativo de qualidade de produção real, você escreveria um teste de unidade por componente React (pelo menos aqueles que manipulam o estado), testando o comportamento específico do React. No entanto, para os propósitos deste post, basta ter um teste de unidade funcional que possa ser executado no modo de observação.

componentes/App.js

Isso pode ser pensado como o contêiner para todas as nossas visualizações de página. Cada página contém um componente <Header/> assim como this.props.children , que avalia a própria visualização da página (ex/ ContactPage se estiver em /contact no navegador).

componentes/home/HomePage.js

Esta é a nossa visão de casa. Eu optei por usar react-bootstrap já que o sistema de grade do bootstrap é excelente para criar páginas responsivas. Com o uso adequado do bootstrap, o número de consultas de mídia que você deve escrever para viewports menores é drasticamente reduzido.

Os componentes restantes ( Header , AboutPage , ContactPage ) são estruturados de forma semelhante (marcação react react-bootstrap , sem manipulação de estado).

Agora vamos falar mais sobre estilo.

Abordagem de estilo CSS

Minha abordagem preferida para estilizar componentes React é ter uma folha de estilo por componente, cujos estilos são definidos para aplicar apenas a esse componente específico. Você notará que em cada um dos meus componentes React, a div de nível superior tem um nome de classe que corresponde ao nome do componente. Assim, por exemplo, HomePage.js tem sua marcação envolvida por:

 <div className="HomePage"> ... </div>

Há também um arquivo HomePage.scss associado que está estruturado da seguinte forma:

 @import '../../styles/variables'; .HomePage { // Content here }

Por que essa abordagem é tão útil? Isso resulta em CSS altamente modular, eliminando amplamente o problema de comportamento em cascata indesejado.

Suponha que temos dois componentes React, Component1 e Component2 . Em ambos os casos, queremos substituir o tamanho da fonte h2 .

 /* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } }

O tamanho da fonte h2 de Component1 e Component2 são independentes se os componentes são adjacentes ou se um componente está aninhado dentro do outro. Idealmente, isso significa que o estilo de um componente é completamente independente, o que significa que o componente terá exatamente a mesma aparência, não importa onde seja colocado em sua marcação. Na realidade, nem sempre é tão simples, mas certamente é um grande passo na direção certa.

Além dos estilos por componente, gosto de ter uma pasta de styles contendo uma folha de estilo global global.scss , juntamente com parciais SASS que lidam com uma responsabilidade específica (neste caso, _fonts.scss e _variables.scss para fontes e variáveis, respectivamente ). A folha de estilo global nos permite definir a aparência geral de todo o aplicativo, enquanto as parciais auxiliares podem ser importadas pelas folhas de estilo por componente, conforme necessário.

Agora que o código comum em cada branch foi explorado em profundidade, vamos mudar nosso foco para a primeira abordagem do executor de tarefas/empacotador.

Configuração do Gulp + Browserify

gulpfile.js

Isso resulta em um gulpfile surpreendentemente grande, com 22 importações e 150 linhas de código. Portanto, por uma questão de brevidade, revisarei apenas as tarefas js , css , server , watch e default em detalhes.

Pacote JS

 // Browserify specific configuration const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...) gulp.task('js', bundle); (...) // Bundles our JS using Browserify. Sourcemaps are used in development, while minification is used in production. function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); }

Esta abordagem é bastante feia por uma série de razões. Por um lado, a tarefa é dividida em três partes separadas. Primeiro, você cria seu objeto de pacote Browserify b , passando algumas opções e definindo alguns manipuladores de eventos. Então você tem a própria tarefa Gulp, que tem que passar uma função nomeada como seu retorno de chamada em vez de inline (já que b.on('update') usa esse mesmo retorno de chamada). Isso dificilmente tem a elegância de uma tarefa do Gulp, onde você apenas passa um gulp.src e canaliza algumas alterações.

Outra questão é que isso nos obriga a ter abordagens diferentes para recarregar html , css e js no navegador. Observando nossa tarefa de watch do Gulp:

 gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });

Quando um arquivo HTML é alterado, a tarefa html é executada novamente.

 gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });

O último pipe chama livereload() se NODE_ENV não for production , o que aciona uma atualização no navegador.

A mesma lógica é usada para o relógio CSS. Quando um arquivo CSS é alterado, a tarefa css é executada novamente e o último pipe na tarefa css aciona livereload() e atualiza o navegador.

No entanto, o relógio js não chama a tarefa js . Em vez disso, o manipulador de eventos do b.on('update', bundle) manipula o recarregamento usando uma abordagem completamente diferente (ou seja, substituição de módulo a quente). A inconsistência nesta abordagem é irritante, mas infelizmente necessária para ter compilações incrementais . Se ingenuamente livereload() no final da função de bundle , isso reconstruiria todo o pacote JS em qualquer alteração de arquivo JS individual. Tal abordagem obviamente não escala. Quanto mais arquivos JS você tiver, mais tempo levará cada reempacotamento. De repente, seus pacotes de 500 ms começam a levar 30 segundos, o que realmente inibe o desenvolvimento ágil.

Pacote CSS

 gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });

O primeiro problema aqui é a incômoda inclusão de CSS do fornecedor. Sempre que um novo arquivo CSS de fornecedor é adicionado ao projeto, temos que nos lembrar de alterar nosso gulpfile para adicionar um elemento ao array gulp.src , em vez de adicionar a importação em um local relevante em nosso código-fonte real.

A outra questão principal é a lógica complicada em cada tubo. Eu tive que adicionar uma biblioteca NPM chamada gulp-cond apenas para configurar a lógica condicional em meus pipes, e o resultado final não é muito legível (colchetes triplos em todos os lugares!).

Tarefa do Servidor

 gulp.task('server', () => { nodemon({ script: 'server.js' }); });

Esta tarefa é muito simples. É essencialmente um wrapper em torno da chamada de linha de comando nodemon server.js , que executa server.js em um ambiente de nó. nodemon é usado em vez de node para que qualquer alteração no arquivo faça com que ele reinicie. Por padrão, o nodemon reiniciaria o processo em execução em qualquer alteração do arquivo JS, e é por isso que é importante incluir um arquivo nodemon.json para limitar seu escopo:

 { "watch": "server.js" }

Vamos revisar nosso código de servidor.

server.js

 const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();

Isso configura o diretório base do servidor e a porta com base no ambiente do nó e cria uma instância de express.

 app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir)));

Isso adiciona middleware connect-livereload (necessário para nossa configuração de recarga ao vivo) e middleware estático (necessário para lidar com nossos ativos estáticos).

 app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); });

Esta é apenas uma rota de API simples. Se você navegar para localhost:3000/api/sample-route no navegador, verá:

 { website: "Toptal", blogPost: true }

Em um backend real, você teria uma pasta inteira dedicada a rotas de API, arquivos separados para estabelecer conexões de banco de dados e assim por diante. Esta rota de exemplo foi incluída apenas para mostrar que podemos construir facilmente um back-end em cima do front-end que configuramos.

 app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); });

Esta é uma rota abrangente, o que significa que não importa qual url você digite no navegador, o servidor retornará nossa página única index.html . É então responsabilidade do React Router resolver nossas rotas no lado do cliente.

 app.listen(port, () => { open(`http://localhost:${port}`); });

Isso diz à nossa instância expressa para ouvir a porta que especificamos e abrir o navegador em uma nova guia na URL especificada.

Até agora, a única coisa que não gosto na configuração do servidor é:

 app.use(require('connect-livereload')({port: 35729}));

Dado que já estamos usando gulp-livereload em nosso gulpfile, isso cria dois locais separados onde o livereload deve ser usado.

Agora, por último, mas não menos importante:

Tarefa padrão

 gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); });

Esta é a tarefa que é executada ao simplesmente digitar gulp no terminal. Uma curiosidade é a necessidade de usar runSequence para que as tarefas sejam executadas sequencialmente. Normalmente, um array de tarefas é executado em paralelo, mas nem sempre esse é o comportamento desejado. Por exemplo, precisamos que a tarefa clean seja executada antes do html para garantir que nossas pastas de destino estejam vazias antes de mover os arquivos para elas. Quando o gulp 4 for lançado, ele suportará os métodos gulp.series e gulp.parallel nativamente, mas por enquanto temos que deixar com esta pequena peculiaridade em nossa configuração.

Além disso, isso é realmente muito elegante. Toda a criação e hospedagem do nosso aplicativo é realizada em um único comando, e entender qualquer parte do fluxo de trabalho é tão simples quanto examinar uma tarefa individual na sequência de execução. Além disso, podemos dividir toda a sequência em partes menores para uma abordagem mais granular para criar e hospedar o aplicativo. Por exemplo, podemos configurar uma tarefa separada chamada validate que executa as tarefas lint e test . Ou podemos ter uma tarefa de host que execute server and watch . Essa capacidade de orquestrar tarefas é muito poderosa, especialmente porque seu aplicativo é dimensionado e requer tarefas mais automatizadas.

Desenvolvimento vs Builds de Produção

 if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production';

Usando a biblioteca yargs NPM, podemos fornecer sinalizadores de linha de comando para o Gulp. Aqui eu instruo o gulpfile a definir o ambiente do nó para produção se --prod for passado para gulp no terminal. Nossa variável PROD é então usada como condicional para diferenciar o comportamento de desenvolvimento e produção em nosso gulpfile. Por exemplo, uma das opções que passamos para nossa configuração do browserify é:

 plugin: PROD ? [] : [hmr, watchify]

Isso informa ao browserify para não usar nenhum plug-in no modo de produção e usar os plug-ins hmr e watchify em outros ambientes.

Essa condicional PROD é muito útil porque evita que tenhamos que escrever um gulpfile separado para produção e desenvolvimento, que acabaria por conter muita repetição de código. Em vez disso, podemos fazer coisas como gulp --prod para executar a tarefa padrão em produção ou gulp html --prod para executar apenas a tarefa html em produção. Por outro lado, vimos anteriormente que encher nossos pipelines do Gulp com instruções como .pipe(cond(!PROD, livereload())) não é o mais legível. No final, é uma questão de preferência se você deseja usar a abordagem de variável booleana ou configurar dois gulpfiles separados.

Agora vamos ver o que acontece quando continuamos usando Gulp como nosso executor de tarefas, mas substituimos Browserify por Webpack.

Configuração do Gulp + Webpack

De repente, nosso gulpfile tem apenas 99 linhas com 12 importações, uma grande redução em relação à nossa configuração anterior! Se verificarmos a tarefa padrão:

 gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });

Agora, nossa configuração completa do aplicativo da Web requer apenas cinco tarefas em vez de nove, uma melhoria dramática.

Além disso, eliminamos a necessidade de livereload . Nossa tarefa de watch agora é simplesmente:

 gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });

Isso significa que nosso gulp watcher não está acionando nenhum tipo de comportamento de reagrupamento. Como um bônus adicional, não precisamos mais transferir index.html do app para dist ou build .

Voltando nosso foco para a redução de tarefas, nossas tarefas html , css , js e fonts foram todas substituídas por uma única tarefa de build :

 gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); });

Simples o suficiente. Execute as tarefas clean e html em sequência. Quando eles estiverem completos, pegue nosso ponto de entrada, canalize-o através do Webpack, passando um arquivo webpack.config.js para configurá-lo e envie o pacote resultante para nosso baseDir ( dist ou build , dependendo do nó env).

Vamos dar uma olhada no arquivo de configuração do Webpack:

webpack.config.js

Este é um arquivo de configuração bastante grande e intimidador, então vamos explicar algumas das propriedades importantes que estão sendo definidas em nosso objeto module.exports .

 devtool: PROD ? 'source-map' : 'eval-source-map',

Isso define o tipo de mapas de origem que o Webpack usará. O Webpack não apenas suporta mapas de origem prontos para uso, como também suporta uma ampla variedade de opções de mapas de origem. Cada opção fornece um equilíbrio diferente de detalhes do mapa de origem versus velocidade de reconstrução (o tempo necessário para reagrupar as alterações). Isso significa que podemos usar uma opção de mapa de origem “barata” para desenvolvimento para obter recargas rápidas e uma opção de mapa de origem mais cara na produção.

 entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ]

Este é o nosso ponto de entrada do pacote. Observe que uma matriz é passada, o que significa que é possível ter vários pontos de entrada. Nesse caso, temos nosso app/index.js de ponto de entrada esperado, bem como o ponto de entrada webpack-hot-middleware que é usado como parte de nossa configuração de recarga de módulo quente.

 output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' },

É aqui que o pacote compilado será gerado. A opção mais confusa é publicPath . Ele define o URL base para onde seu pacote será hospedado no servidor. Assim, por exemplo, se seu publicPath for /public/assets , o pacote aparecerá em /public/assets/bundle.js no servidor.

 devServer: { contentBase: PROD ? './build' : './app' }

Isso informa ao servidor qual pasta em seu projeto usar como diretório raiz do servidor.

Se você ficar confuso sobre como o Webpack está mapeando o pacote criado em seu projeto para o pacote no servidor, lembre-se do seguinte:

  • path + filename : A localização exata do pacote no código-fonte do projeto
  • contentBase (como root, / ) + publicPath : A localização do pacote no servidor
 plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ],

Estes são plugins que melhoram a funcionalidade do Webpack de alguma forma. Por exemplo, webpack.optimize.UglifyJsPlugin é responsável pela minificação.

 loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} ]

Estes são carregadores. Essencialmente, eles pré-processam arquivos que são carregados por meio de instruções require() . Eles são um pouco semelhantes aos tubos Gulp, pois você pode encadear carregadores juntos.

Vamos examinar um de nossos objetos loader:

 {test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'}

A propriedade test informa ao Webpack que o carregador fornecido se aplica se um arquivo corresponder ao padrão regex fornecido, neste caso /\.scss$/ . A propriedade loader corresponde à ação que o loader executa. Aqui estamos encadeando os carregadores style , css , resolve-url e sass , que são executados na ordem inversa.

Devo admitir que não acho a loader3!loader2!loader1 muito elegante. Afinal, quando você tem que ler alguma coisa em um programa da direita para a esquerda? Apesar disso, os carregadores são um recurso muito poderoso do webpack. Na verdade, o carregador que acabei de mencionar nos permite importar arquivos SASS diretamente em nosso JavaScript! Por exemplo, podemos importar nosso fornecedor e folhas de estilo globais em nosso arquivo de ponto de entrada:

index.js

 import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app'));

Da mesma forma, em nosso componente Header podemos adicionar import './Header.scss' para importar a folha de estilo associada ao componente. Isso também se aplica a todos os nossos outros componentes.

Na minha opinião, isso quase pode ser considerado uma mudança revolucionária no mundo do desenvolvimento JavaScript. Não há necessidade de se preocupar com pacotes CSS, minificação ou mapas de origem, pois nosso carregador lida com tudo isso para nós. Até mesmo o recarregamento de módulo quente funciona para nossos arquivos CSS. Então, ser capaz de lidar com importações de JS e CSS no mesmo arquivo torna o desenvolvimento conceitualmente mais simples: Mais consistência, menos alternância de contexto e mais fácil de raciocinar.

Para dar um breve resumo de como esse recurso funciona: Webpack inline o CSS em nosso pacote JS. Na verdade, o Webpack também pode fazer isso para imagens e fontes:

 {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'}

O carregador de URL instrui o Webpack a inserir nossas imagens e fontes como urls de dados se tiverem menos de 100 KB, caso contrário, sirva-as como arquivos separados. Obviamente, também podemos configurar o tamanho de corte para um valor diferente, como 10 KB.

E essa é a configuração do Webpack em poucas palavras. Admito que há uma boa quantidade de configuração, mas os benefícios de usá-la são simplesmente fenomenais. Embora o Browserify tenha plugins e transformações, eles simplesmente não podem ser comparados aos carregadores Webpack em termos de funcionalidade adicional.

Configuração de Webpack + Scripts NPM

Nesta configuração, usamos scripts npm diretamente em vez de depender de um gulpfile para automatizar nossas tarefas.

pacote.json

 "scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist && mkdir dist", "clean-build": "rimraf ./build && mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" }

Para executar compilações de desenvolvimento e produção, insira npm start e npm run start:prod , respectivamente.

Isso é certamente mais compacto que nosso gulpfile, já que reduzimos de 99 a 150 linhas de código para 19 scripts NPM, ou 12 se excluirmos os scripts de produção (a maioria dos quais apenas espelha os scripts de desenvolvimento com o ambiente de nó definido para produção ). A desvantagem é que esses comandos são um pouco enigmáticos em comparação com nossas contrapartes de tarefas Gulp, e não tão expressivos. Por exemplo, não há como (pelo menos que eu saiba) ter um único script npm executando certos comandos em série e outros em paralelo. É um ou outro.

No entanto, há uma grande vantagem nessa abordagem. Usando bibliotecas NPM como mocha diretamente da linha de comando, você não precisa instalar um wrapper Gulp equivalente para cada um (neste caso, gulp-mocha ).

Em vez de instalar o NPM

  • gole-eslint
  • gole-mocha
  • gulp-nodemon
  • etc

Instalamos os seguintes pacotes:

  • estreito
  • mocha
  • nómon
  • etc

Citando o post de Cory House, Why I Left Gulp and Grunt for NPM Scripts :

Eu era um grande fã de Gulp. Mas no meu último projeto, acabei com centenas de linhas no meu gulpfile e cerca de uma dúzia de plugins do Gulp. Eu estava lutando para integrar Webpack, Browsersync, hot reloading, Mocha e muito mais usando Gulp. Por quê? Bem, alguns plugins tinham documentação insuficiente para o meu caso de uso. Alguns plugins expuseram apenas parte da API que eu precisava. Um tinha um bug estranho em que apenas observava um pequeno número de arquivos. Outra cor despojada ao enviar para a linha de comando.

Ele especifica três questões centrais com o Gulp:

  1. Dependência de autores de plugins
  2. Frustrante para depurar
  3. Documentação separada

Eu tenderia a concordar com tudo isso.

1. Dependência de autores de plugins

Sempre que uma biblioteca como eslint é atualizada, a biblioteca gulp-eslint associada precisa de uma atualização correspondente. Se o mantenedor da biblioteca perder o interesse, a versão gulp da biblioteca ficará fora de sincronia com a nativa. O mesmo vale para quando uma nova biblioteca é criada. Se alguém cria uma biblioteca xyz e ela pega, de repente você precisa de uma biblioteca gulp-xyz correspondente para usá-la em suas tarefas gulp.

Em certo sentido, essa abordagem simplesmente não escala. Idealmente, gostaríamos de uma abordagem como Gulp que pode usar as bibliotecas nativas.

2. Frustrante para depurar

Embora bibliotecas como gulp-plumber ajudem a aliviar consideravelmente esse problema, é verdade que o relatório de erros no gulp simplesmente não é muito útil. Se mesmo um pipe lançar uma exceção não tratada, você obterá um rastreamento de pilha para um problema que parece completamente não relacionado ao que está causando o problema em seu código-fonte. Isso pode tornar a depuração um pesadelo em alguns casos. Nenhuma quantidade de pesquisa no Google ou no Stack Overflow pode realmente ajudá-lo se o erro for enigmático ou enganoso o suficiente.

3. Documentação Desarticulada

Muitas vezes, acho que pequenas bibliotecas gulp tendem a ter documentação muito limitada. Suspeito que isso seja porque o autor geralmente faz a biblioteca principalmente para seu próprio uso. Além disso, é comum ter que olhar a documentação tanto do plugin Gulp quanto da própria biblioteca nativa, o que significa muita troca de contexto e duas vezes mais leitura para fazer.

Conclusão

Parece bastante claro para mim que o Webpack é preferível ao Browserify e os scripts NPM são preferíveis ao Gulp, embora cada opção tenha suas vantagens e desvantagens. O Gulp é certamente mais expressivo e conveniente de usar do que os scripts NPM, mas você paga o preço por toda a abstração adicionada.

Nem todas as combinações podem ser perfeitas para o seu aplicativo, mas se você quiser evitar um grande número de dependências de desenvolvimento e uma experiência de depuração frustrante, o Webpack com scripts NPM é o caminho a seguir. Espero que você ache este artigo útil para escolher as ferramentas certas para o seu próximo projeto.

Relacionado:
  • Mantenha o controle: um guia para Webpack e React, Pt. 1
  • Gulp Under the Hood: construindo uma ferramenta de automação de tarefas baseada em fluxo