Gulp: a arma secreta de um desenvolvedor da Web para maximizar a velocidade do site
Publicados: 2022-03-11Muitos de nós temos que lidar com projetos baseados na web que são usados na produção, que fornecem vários serviços ao público. Ao lidar com esses projetos, é importante poder construir e implantar nosso código rapidamente. Fazer algo rapidamente geralmente leva a erros, especialmente se um processo for repetitivo, portanto, é uma boa prática automatizar esse processo o máximo possível.
Neste post, veremos uma ferramenta que pode fazer parte do que nos permitirá alcançar essa automação. Esta ferramenta é um pacote npm chamado Gulp.js. Para se familiarizar com a terminologia básica do Gulp.js usada neste post, consulte “Uma introdução à automação de JavaScript com Gulp” que foi publicado anteriormente no blog por Antonios Minas, um de nossos colegas desenvolvedores da Toptal. Assumiremos familiaridade básica com o ambiente npm, pois ele é usado extensivamente ao longo deste post para instalar pacotes.
Como veicular recursos de front-end
Antes de continuarmos, vamos dar alguns passos para trás para obter uma visão geral do problema que o Gulp.js pode resolver para nós. Muitos projetos baseados na web apresentam arquivos JavaScript front-end que são servidos ao cliente para fornecer várias funcionalidades à página da web. Normalmente, há também um conjunto de folhas de estilo CSS que também são fornecidas ao cliente. Às vezes, ao olhar para o código-fonte de um site ou aplicativo da web, podemos ver um código como este:
<link href="css/main.css" rel="stylesheet"> <link href="css/custom.css" rel="stylesheet"> <script src="js/jquery.min.js"></script> <script src="js/site.js"></script> <script src="js/module1.js"></script> <script src="js/module2.js"></script>
Existem alguns problemas com este código. Ele tem referências a duas folhas de estilo CSS separadas e quatro arquivos JavaScript separados. Isso significa que o servidor precisa fazer um total de seis solicitações ao servidor e cada solicitação deve carregar separadamente um recurso antes que a página esteja pronta. Este é um problema menor com HTTP/2 porque HTTP/2 introduz paralelismo e compressão de cabeçalho, mas ainda é um problema. Aumenta o volume total de tráfego necessário para carregar esta página e reduz a qualidade da experiência do usuário, pois leva mais tempo para carregar os arquivos. No caso do HTTP 1.1, ele também sobrecarrega a rede e reduz o número de canais de solicitação disponíveis. Teria sido muito melhor combinar os arquivos CSS e JavaScript em um único pacote para cada um. Dessa forma, haveria apenas um total de duas solicitações. Também teria sido bom servir versões reduzidas desses arquivos, que geralmente são muito menores do que os originais. Nosso aplicativo da web também pode quebrar se algum dos ativos estiver em cache e o cliente receber uma versão desatualizada.
Uma abordagem primitiva para resolver alguns desses problemas é combinar manualmente cada tipo de ativo em um pacote usando um editor de texto e, em seguida, executar o resultado por meio de um serviço minificador, como http://jscompress.com/. Isso prova ser muito tedioso de fazer continuamente durante o processo de desenvolvimento. Uma melhoria leve, mas questionável, seria hospedar nosso próprio servidor minifier, usando um dos pacotes disponíveis no GitHub. Então, poderíamos fazer coisas que seriam um pouco semelhantes ao seguinte:
<script src="min/f=js/site.js,js/module1.js"></script>
Isso serviria arquivos minificados para nosso cliente, mas não resolveria o problema de armazenamento em cache. Isso também causaria carga adicional no servidor, pois nosso servidor teria essencialmente que concatenar e reduzir todos os arquivos de origem repetidamente em cada solicitação.
Automatizando com Gulp.js
Certamente podemos fazer melhor do que qualquer uma dessas duas abordagens. O que realmente queremos é automatizar o empacotamento e incluí-lo na fase de construção do nosso projeto. Queremos terminar com pacotes de ativos pré-construídos que já estão minificados e prontos para veiculação. Também queremos forçar o cliente a receber as versões mais atualizadas de nossos ativos agrupados em cada solicitação, mas ainda queremos aproveitar o cache, se possível. Felizmente para nós, Gulp.js pode lidar com isso. No restante do artigo, construiremos uma solução que aproveitará o poder do Gulp.js para concatenar e reduzir os arquivos. Também usaremos um plugin para quebrar o cache quando houver atualizações.
Estaremos criando o seguinte diretório e estrutura de arquivos em nosso exemplo:
public/ |- build/ |- js/ |- bundle-{hash}.js |- css/ |- stylesheet-{hash}.css assets/ |- js/ |- vendor/ |- jquery.js |- site.js |- module1.js |- module2.js |- css/ |- main.css |- custom.css gulpfile.js package.json
O arquivo gulpfile.js é onde vamos definir as tarefas que o Gulp irá realizar para nós. O package.json
é usado pelo npm para definir o pacote da nossa aplicação e rastrear as dependências que iremos instalar. O diretório público é o que deve ser configurado para enfrentar a web. O diretório de ativos é onde armazenaremos nossos arquivos de origem. Para usar o Gulp no projeto, precisaremos instalá-lo via npm e salvá-lo como uma dependência de desenvolvedor para o projeto. Também vamos querer começar com o plugin concat
para Gulp, que nos permitirá concatenar vários arquivos em um.
Para instalar esses dois itens, executaremos o seguinte comando:
npm install --save-dev gulp gulp-concat
Em seguida, vamos querer começar a escrever o conteúdo de gulpfile.js.
var gulp = require('gulp'); var concat = require('gulp-concat'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(gulp.dest('public/build/js')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(gulp.dest('public/build/css')); }); gulp.task('default', ['pack-js', 'pack-css']);
Aqui, estamos carregando a biblioteca gulp e seu plugin concat. Em seguida, definimos três tarefas.
A primeira tarefa ( pack-js
) define um procedimento para compactar vários arquivos de origem JavaScript em um pacote. Listamos os arquivos de origem, que serão agrupados, lidos e concatenados na ordem especificada. Nós canalizamos isso para o plug-in concat para obter um arquivo final chamado bundle.js
. Por fim, dizemos ao gulp para gravar o arquivo em public/build/js
.
A segunda tarefa ( pack-css
) faz a mesma coisa que acima, mas para as folhas de estilo CSS. Ele diz ao Gulp para armazenar a saída concatenada como stylesheet.css
em public/build/css
.
A terceira tarefa ( default
) é aquela que o Gulp executa quando a invocamos sem argumentos. No segundo parâmetro, passamos a lista de outras tarefas a serem executadas quando a tarefa padrão for executada.
Vamos colar esse código em gulpfile.js usando qualquer editor de código-fonte que normalmente usamos e, em seguida, salve o arquivo na raiz do aplicativo.
Em seguida, vamos abrir a linha de comando e executar:
gulp
Se examinarmos nossos arquivos depois de executar este comando, encontraremos dois novos arquivos: public/build/js/bundle.js
e public/build/css/stylesheet.css
. São concatenações de nossos arquivos de origem, o que resolve parte do problema original. No entanto, eles não são reduzidos e ainda não há bloqueio de cache. Vamos adicionar minificação automatizada.
Otimizando Ativos Construídos
Vamos precisar de dois novos plugins. Para adicioná-los, executaremos o seguinte comando:
npm install --save-dev gulp-clean-css gulp-minify
O primeiro plugin é para reduzir o CSS e o segundo é para reduzir o JavaScript. O primeiro usa o pacote clean-css e o segundo usa o pacote UglifyJS2. Vamos carregar esses dois pacotes em nosso gulpfile.js primeiro:
var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css');
Em seguida, precisaremos usá-los em nossas tarefas antes de gravar a saída no disco:
.pipe(minify()) .pipe(cleanCss())
O gulpfile.js agora deve ficar assim:
var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify()) .pipe(gulp.dest('public/build/js')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(gulp.dest('public/build/css')); }); gulp.task('default', ['pack-js', 'pack-css']);
Vamos tomar gole novamente. Veremos que o arquivo stylesheet.css
é salvo em formato reduzido e o arquivo bundle.js
ainda é salvo como está. Notaremos que agora também temos bundle-min.js, que foi reduzido. Queremos apenas o arquivo minificado e queremos que ele seja salvo como bundle.js
, então modificaremos nosso código com parâmetros adicionais:

.pipe(minify({ ext:{ min:'.js' }, noSource: true }))
De acordo com a documentação do plug-in gulp-minify (https://www.npmjs.com/package/gulp-minify), isso definirá o nome desejado para a versão minificada e informará ao plug-in para não criar a versão que contém a fonte original. Se excluirmos o conteúdo do diretório de compilação e executarmos o gulp na linha de comando novamente, teremos apenas dois arquivos reduzidos. Acabamos de implementar a fase de minificação do nosso processo de construção.
Bloqueio de cache
Em seguida, vamos querer adicionar cache busting, e precisaremos instalar um plugin para isso:
npm install --save-dev gulp-rev
E exija isso em nosso arquivo gulp:
var rev = require('gulp-rev');
Usar o plugin é um pouco complicado. Temos que canalizar a saída minificada através do plugin primeiro. Então, temos que chamar o plugin novamente depois de gravar os resultados no disco. O plug-in renomeia os arquivos para que sejam marcados com um hash exclusivo e também cria um arquivo de manifesto. O arquivo de manifesto é um mapa que pode ser usado por nosso aplicativo para determinar os nomes de arquivos mais recentes aos quais devemos nos referir em nosso código HTML. Depois de modificar o arquivo gulp, ele deve ficar assim:
var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); var rev = require('gulp-rev'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify({ ext:{ min:'.js' }, noSource: true })) .pipe(rev()) .pipe(gulp.dest('public/build/js')) .pipe(rev.manifest()) .pipe(gulp.dest('public/build')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(rev()) .pipe(gulp.dest('public/build/css')) .pipe(rev.manifest()) .pipe(gulp.dest('public/build')); }); gulp.task('default', ['pack-js', 'pack-css']);
Vamos excluir o conteúdo do nosso diretório de compilação e executar o gulp novamente. Descobriremos que agora temos dois arquivos com hashtags afixadas a cada um dos nomes de arquivo e um manifest.json salvo em public/build
. Se abrirmos o arquivo de manifesto, veremos que ele tem apenas uma referência a um de nossos arquivos minificados e marcados. O que está acontecendo é que cada tarefa escreve um arquivo de manifesto separado, e uma delas acaba sobrescrevendo a outra. Precisaremos modificar as tarefas com parâmetros adicionais que dirão a eles para procurar o arquivo de manifesto existente e mesclar os novos dados nele, se existir. A sintaxe para isso é um pouco complicada, então vamos ver como o código deve se parecer e depois revisá-lo:
var gulp = require('gulp'); var concat = require('gulp-concat'); var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css'); var rev = require('gulp-rev'); gulp.task('pack-js', function () { return gulp.src(['assets/js/vendor/*.js', 'assets/js/main.js', 'assets/js/module*.js']) .pipe(concat('bundle.js')) .pipe(minify({ ext:{ min:'.js' }, noSource: true })) .pipe(rev()) .pipe(gulp.dest('public/build/js')) .pipe(rev.manifest('public/build/rev-manifest.json', { merge: true })) .pipe(gulp.dest('')); }); gulp.task('pack-css', function () { return gulp.src(['assets/css/main.css', 'assets/css/custom.css']) .pipe(concat('stylesheet.css')) .pipe(cleanCss()) .pipe(rev()) .pipe(gulp.dest('public/build/css')) .pipe(rev.manifest('public/build/rev-manifest.json', { merge: true })) .pipe(gulp.dest('')); }); gulp.task('default', ['pack-js', 'pack-css']);
Estamos canalizando a saída para rev.manifest()
primeiro. Isso cria arquivos marcados em vez dos arquivos que tínhamos antes. Estamos fornecendo o caminho desejado do nosso rev-manifest.json
e informando rev.manifest()
para mesclar no arquivo existente, se ele existir. Então estamos dizendo ao gulp para escrever o manifesto no diretório atual, que nesse ponto será public/build. O problema do caminho é devido a um bug que é discutido com mais detalhes no GitHub.
Agora temos minificação automatizada, arquivos marcados e um arquivo de manifesto. Tudo isso nos permitirá entregar os arquivos mais rapidamente ao usuário, e quebrar seu cache sempre que fizermos nossas modificações. Existem apenas dois problemas restantes.
O primeiro problema é que, se fizermos alguma modificação em nossos arquivos de origem, obteremos arquivos recém-marcados, mas os antigos também permanecerão lá. Precisamos de alguma maneira de excluir automaticamente arquivos antigos minificados. Vamos resolver este problema usando um plugin que nos permitirá deletar arquivos:
npm install --save-dev del
Vamos exigir isso em nosso código e definir duas novas tarefas, uma para cada tipo de arquivo de origem:
var del = require('del'); gulp.task('clean-js', function () { return del([ 'public/build/js/*.js' ]); }); gulp.task('clean-css', function () { return del([ 'public/build/css/*.css' ]); });
Vamos então garantir que a nova tarefa termine de ser executada antes de nossas duas tarefas principais:
gulp.task('pack-js', ['clean-js'], function () { gulp.task('pack-css', ['clean-css'], function () {
Se executarmos o gulp
novamente após essa modificação, teremos apenas os arquivos minificados mais recentes.
O segundo problema é que não queremos continuar engasgando toda vez que fizermos uma mudança. Para resolver isso, precisaremos definir uma tarefa de observador:
gulp.task('watch', function() { gulp.watch('assets/js/**/*.js', ['pack-js']); gulp.watch('assets/css/**/*.css', ['pack-css']); });
Também alteraremos a definição de nossa tarefa padrão:
gulp.task('default', ['watch']);
Se agora executarmos o gulp a partir da linha de comando, descobriremos que ele não cria mais nada na invocação. Isso ocorre porque agora ele chama a tarefa do inspetor que observará nossos arquivos de origem para quaisquer alterações e compilará apenas quando detectar uma alteração. Se tentarmos alterar qualquer um dos nossos arquivos de origem e, em seguida, olharmos para o nosso console novamente, veremos que as tarefas pack-js
e pack-css
são executadas automaticamente junto com suas dependências.
Agora, tudo o que temos a fazer é carregar o arquivo manifest.json em nosso aplicativo e obter os nomes dos arquivos marcados a partir dele. Como fazemos isso depende de nossa linguagem de back-end específica e pilha de tecnologia, e seria bastante trivial para implementar, então não vamos entrar em detalhes. No entanto, a ideia geral é que podemos carregar o manifesto em uma matriz ou objeto e, em seguida, definir uma função auxiliar que nos permitirá chamar ativos versionados de nossos modelos de maneira semelhante à seguinte:
gulp('bundle.js')
Assim que fizermos isso, não precisaremos mais nos preocupar com tags alteradas em nossos nomes de arquivos e poderemos nos concentrar em escrever código de alta qualidade.
O código-fonte final deste artigo, juntamente com alguns ativos de exemplo, podem ser encontrados neste repositório do GitHub.
Conclusão
Neste artigo, vimos como implementar a automação baseada em Gulp para nosso processo de construção. Espero que isso seja útil para você e permita que você desenvolva processos de compilação mais sofisticados em seus próprios aplicativos.
Lembre-se de que o Gulp é apenas uma das ferramentas que podem ser usadas para esse fim, e existem muitas outras, como Grunt, Browserify e Webpack. Eles variam em seus propósitos e no escopo dos problemas que podem resolver. Alguns podem resolver problemas que o Gulp não consegue, como agrupar módulos JavaScript com dependências que podem ser carregadas sob demanda. Isso é chamado de “divisão de código” e é uma melhoria em relação à ideia de servir um arquivo grande com todas as partes do nosso programa em cada página. Essas ferramentas são bastante sofisticadas, mas podem ser abordadas no futuro. Em um post a seguir, veremos como automatizar a implantação do nosso aplicativo.