Gulp Under the Hood: construindo uma ferramenta de automação de tarefas baseada em fluxo
Publicados: 2022-03-11Os desenvolvedores front-end hoje em dia estão usando várias ferramentas para automatizar operações de rotina. Três das soluções mais populares são Grunt, Gulp e Webpack. Cada uma dessas ferramentas é construída em filosofias diferentes, mas elas compartilham o mesmo objetivo comum: simplificar o processo de construção de front-end. Por exemplo, o Grunt é orientado à configuração, enquanto o Gulp não impõe quase nada. Na verdade, o Gulp depende do desenvolvedor escrever código para implementar o fluxo dos processos de compilação - as várias tarefas de compilação.
Quando se trata de escolher uma dessas ferramentas, meu favorito é o Gulp. Em suma, é uma solução simples, rápida e confiável. Neste artigo, veremos como o Gulp funciona nos bastidores, tentando implementar nossa própria ferramenta semelhante ao Gulp.
API Gulp
O Gulp vem com apenas quatro funções simples:
- gulp.tarefa
- gulp.src
- gole.dest
- gulp.watch
Estas quatro funções simples, em várias combinações, oferecem toda a potência e flexibilidade do Gulp. Na versão 4.0, Gulp introduziu duas novas funções: gulp.series e gulp.parallel. Essas APIs permitem que as tarefas sejam executadas em série ou em paralelo.
Dessas quatro funções, as três primeiras são absolutamente essenciais para qualquer arquivo Gulp. permitindo que as tarefas sejam definidas e invocadas a partir da interface de linha de comando. O quarto é o que torna o Gulp verdadeiramente automático, permitindo que as tarefas sejam executadas quando os arquivos são alterados.
Gulpfile
Este é um gulpfile elementar:
gulp.task('test', function{ gulp.src('test.txt') .pipe(gulp.dest('out')); });
Ele descreve uma tarefa de teste simples. Quando chamado, o arquivo test.txt no diretório de trabalho atual deve ser copiado para o diretório ./out . Experimente executando o Gulp:
touch test.txt # Create test.txt gulp test
Observe que o método .pipe
não faz parte do Gulp, é uma API de fluxo de nó, ele conecta um fluxo legível (gerado por gulp.src('test.txt')
) com um fluxo gravável (gerado por gulp.dest('out')
). Toda a comunicação entre Gulp e plugins é baseada em streams. Isso nos permite escrever código gulpfile de uma maneira tão elegante.
Plugue do Meet
Agora que temos uma ideia de como o Gulp funciona, vamos construir nossa própria ferramenta parecida com o Gulp: Plug.
Começaremos com a API plug.task. Deve nos permitir registrar tarefas, e as tarefas devem ser executadas se o nome da tarefa for passado nos parâmetros de comando.
var plug = { task: onTask }; module.exports = plug; var tasks = {}; function onTask(name, callback){ tasks[name] = callback; }
Isso permitirá que as tarefas sejam registradas. Agora precisamos tornar essa tarefa executável. Para manter as coisas simples, não faremos um lançador de tarefas separado. Em vez disso, vamos incluí-lo em nossa implementação de plug.
Tudo o que precisamos fazer é executar as tarefas nomeadas nos parâmetros da linha de comando. Também precisamos ter certeza de tentar fazer isso no próximo loop de execução, depois que todas as tarefas forem registradas. A maneira mais fácil de fazer isso é executar tarefas em um retorno de chamada de tempo limite ou, de preferência, process.nextTick:
process.nextTick(function(){ var taskName = process.argv[2]; if (taskName && tasks[taskName]) { tasks[taskName](); } else { console.log('unknown task', taskName) } });
Componha plugfile.js assim:
var plug = require('./plug'); plug.task('test', function(){ console.log('hello plug'); })
… e execute-o.
node plugfile.js test
Ele irá exibir:
hello plug
Subtarefas
O Gulp também permite definir subtarefas no registro de tarefas. Neste caso, plug.task deve ter 3 parâmetros, o nome, array de subtarefas e função callback. Vamos implementar isso.
Precisaremos atualizar a API da tarefa como tal:
var tasks = {}; function onTask(name) { if(Array.isArray(arguments[1]) && typeof arguments[2] === "function"){ tasks[name] = { subTasks: arguments[1], callback: arguments[2] }; } else if(typeof arguments[1] === "function"){ tasks[name] = { subTasks: [], callback: arguments[1] }; } else{ console.log('invalid task registration') } } function runTask(name){ if(tasks[name].subTasks){ tasks[name].subTasks.forEach(function(subTaskName){ runTask(subTaskName); }); } if(tasks[name].callback){ tasks[name].callback(); } } process.nextTick(function(){ if (taskName && tasks[taskName]) { runTask(taskName); } });
Agora, se nosso plugfile.js estiver assim:
plug.task('subTask1', function(){ console.log('from sub task 1'); }) plug.task('subTask2', function(){ console.log('from sub task 2'); }) plug.task('test', ['subTask1', 'subTask2'], function(){ console.log('hello plug'); })
… executando
node plugfile.js test
… deve exibir:
from sub task 1 from sub task 2 hello plug
Observe que o Gulp executa subtarefas em paralelo. Mas para manter as coisas simples, em nossa implementação estamos executando subtarefas sequencialmente. O Gulp 4.0 permite que isso seja controlado usando suas duas novas funções de API, que implementaremos mais adiante neste artigo.
Origem e Destino
Plug será de pouca utilidade se não permitirmos que os arquivos sejam lidos e gravados. Então a seguir vamos implementar plug.src
. Esse método no Gulp espera um argumento que seja uma máscara de arquivo, um nome de arquivo ou uma matriz de máscaras de arquivo. Ele retorna um fluxo de nó legível.
Por enquanto, em nossa implementação de src
, permitiremos apenas nomes de arquivos:
var plug = { task: onTask, src: onSrc }; var stream = require('stream'); var fs = require('fs'); function onSrc(fileName){ var src = new stream.Readable({ read: function (chunk) { }, objectMode: true }); //read file and send it to the stream fs.readFile(path, 'utf8', (e,data)=> { src.push({ name: path, buffer: data }); src.push(null); }); return src; }
Observe que usamos objectMode: true
, um parâmetro opcional aqui. Isso ocorre porque os fluxos de nós funcionam com fluxos binários por padrão. Se precisarmos passar/receber objetos JavaScript via streams, temos que usar este parâmetro.
Como você pode ver, criamos um objeto artificial:
{ name: path, //file name buffer: data //file content }
… e passou para o córrego.
Por outro lado, o método plug.dest deve receber um nome de pasta de destino e retornar um fluxo gravável que receberá objetos do fluxo .src. Assim que um objeto de arquivo for recebido, ele será armazenado na pasta de destino.
function onDest(path){ var writer = new stream.Writable({ write: function (chunk, encoding, next) { if (!fs.existsSync(path)) fs.mkdirSync(path); fs.writeFile(path +'/'+ chunk.name, chunk.buffer, (e)=> { next() }); }, objectMode: true }); return writer; }
Vamos atualizar nosso plugfile.js:

var plug = require('./plug'); plug.task('test', function(){ plug.src('test.txt') .pipe(plug.dest('out')) })
… criar teste.txt
touch test.txt
… e execute-o:
node plugfile.js test ls ./out
test.txt deve ser copiado para a pasta ./out .
O próprio Gulp funciona da mesma maneira, mas em vez de nossos objetos de arquivo artificiais, ele usa objetos de vinil. É muito mais conveniente, pois contém não apenas o nome do arquivo e o conteúdo, mas também informações meta adicionais, como o nome da pasta atual, o caminho completo para o arquivo e assim por diante. Ele pode não conter todo o buffer de conteúdo, mas possui um fluxo legível do conteúdo.
Vinil: melhor que arquivos
Existe uma excelente biblioteca vinil-fs que nos permite manipular arquivos representados como objetos de vinil. Essencialmente, nos permite criar fluxos legíveis e graváveis com base na máscara de arquivo.
Podemos reescrever funções de plug usando a biblioteca vinil-fs. Mas primeiro precisamos instalar o vinil-fs:
npm i vinyl-fs
Com isso instalado, nossa nova implementação do Plug ficará assim:
var vfs = require('vinyl-fs') function onSrc(fileName){ return vfs.src(fileName); } function onDest(path){ return vfs.dest(path); } // ...
… e para experimentar:
rm out/test.txt node plugFile.js test ls out/test.txt
Os resultados ainda devem ser os mesmos.
Plug-ins Gulp
Como nosso serviço Plug usa a convenção de fluxo Gulp, podemos usar plugins nativos do Gulp junto com nossa ferramenta Plug.
Vamos experimentar um. Instale gulp-rename:
npm i gulp-rename
… e atualize plugfile.js para usá-lo:
var plug = require('./app.js'); var rename = require('gulp-rename'); plug.task('test', function () { return plug.src('test.txt') .pipe(rename('renamed.txt')) .pipe(plug.dest('out')); });
A execução do plugfile.js agora ainda deve, você adivinhou, produzir o mesmo resultado.
node plugFile.js test ls out/renamed.txt
Mudanças de monitoramento
O último mas não menos importante método é gulp.watch
Este método nos permite registrar o ouvinte de arquivo e invocar tarefas registradas quando os arquivos são alterados. Vamos implementá-lo:
var plug = { task: onTask, src: onSrc, dest: onDest, watch: onWatch }; function onWatch(fileName, taskName){ fs.watchFile(fileName, (event, filename) => { if (filename) { tasks[taskName](); } }); }
Para experimentar, adicione esta linha ao plugfile.js:
plug.watch('test.txt','test');
Agora a cada mudança de test.txt , o arquivo será copiado para a pasta out com seu nome alterado.
Série x Paralelo
Agora que todas as funções fundamentais da API do Gulp estão implementadas, vamos dar um passo adiante. A próxima versão do Gulp conterá mais funções de API. Esta nova API tornará o Gulp mais poderoso:
- gole.paralelo
- gulp.series
Esses métodos permitem que o usuário controle a sequência na qual as tarefas são executadas. Para registrar subtarefas em paralelo pode-se usar gulp.parallel, que é o comportamento atual do Gulp. Por outro lado, gulp.series pode ser usado para executar subtarefas de maneira sequencial, uma após a outra.
Suponha que temos test1.txt e test2.txt na pasta atual. Para copiar esses arquivos para a pasta em paralelo, vamos fazer um plugfile:
var plug = require('./plug'); plug.task('subTask1', function(){ return plug.src('test1.txt') .pipe(plug.dest('out')) }) plug.task('subTask2', function(){ return plug.src('test2.txt') .pipe(plug.dest('out')) }) plug.task('test-parallel', plug.parallel(['subTask1', 'subTask2']), function(){ console.log('done') }) plug.task('test-series', plug.series(['subTask1', 'subTask2']), function(){ console.log('done') })
Para simplificar a implementação, as funções de retorno de chamada da subtarefa são feitas para retornar seu fluxo. Isso nos ajudará a rastrear o ciclo de vida do fluxo.
Começaremos a alterar nossa API:
var plug = { task: onTask, src: onSrc, dest: onDest, parallel: onParallel, series: onSeries };
Também precisaremos atualizar a função onTask , pois precisamos adicionar metainformações de tarefas adicionais para ajudar nosso iniciador de tarefas a lidar com as subtarefas corretamente.
function onTask(name, subTasks, callback){ if(arguments.length < 2){ console.error('invalid task registration',arguments); return; } if(arguments.length === 2){ if(typeof arguments[1] === 'function'){ callback = subTasks; subTasks = {series: []}; } } tasks[name] = subTasks; tasks[name].callback = function(){ if(callback) return callback(); }; } function onParallel(tasks){ return { parallel: tasks }; } function onSeries(tasks){ return { series: tasks }; }
Para simplificar, usaremos async.js, uma biblioteca de utilitários para lidar com funções assíncronas para executar tarefas em paralelo ou em série:
var async = require('async') function _processTask(taskName, callback){ var taskInfo = tasks[taskName]; console.log('task ' + taskName + ' is started'); var subTaskNames = taskInfo.series || taskInfo.parallel || []; var subTasks = subTaskNames.map(function(subTask){ return function(cb){ _processTask(subTask, cb); } }); if(subTasks.length>0){ if(taskInfo.series){ async.series(subTasks, taskInfo.callback); }else{ async.parallel(subTasks, taskInfo.callback); } }else{ var stream = taskInfo.callback(); if(stream){ stream.on('end', function(){ console.log('stream ' + taskName + ' is ended'); callback() }) }else{ console.log('task ' + taskName +' is completed'); callback(); } } }
Contamos com o 'fim' do fluxo de nó que é emitido quando um fluxo processa todas as mensagens e é fechado, o que é uma indicação de que a subtarefa está concluída. Com o async.js, não precisamos lidar com uma grande confusão de retornos de chamada.
Para experimentá-lo, vamos primeiro executar as subtarefas em paralelo:
node plugFile.js test-parallel
task test-parallel is started task subTask1 is started task subTask2 is started stream subTask2 is ended stream subTask1 is ended done
E execute as mesmas subtarefas em série:
node plugFile.js test-series
task test-series is started task subTask1 is started stream subTask1 is ended task subTask2 is started stream subTask2 is ended done
Conclusão
É isso, implementamos a API do Gulp e podemos usar plugins do Gulp agora. Claro, não use Plug-in em projetos reais, pois o Gulp é mais do que apenas o que implementamos aqui. Espero que este pequeno exercício ajude você a entender como o Gulp funciona nos bastidores e nos permita usá-lo com mais fluência e estendê-lo com plugins.