Gulp Under the Hood: creación de una herramienta de automatización de tareas basada en secuencias
Publicado: 2022-03-11Los desarrolladores front-end hoy en día utilizan múltiples herramientas para automatizar las operaciones de rutina. Tres de las soluciones más populares son Grunt, Gulp y Webpack. Cada una de estas herramientas se basa en diferentes filosofías, pero comparten el mismo objetivo común: agilizar el proceso de creación de front-end. Por ejemplo, Grunt se basa en la configuración, mientras que Gulp no impone casi nada. De hecho, Gulp se basa en el código de escritura del desarrollador para implementar el flujo de los procesos de compilación: las diversas tareas de compilación.
Cuando se trata de elegir una de estas herramientas, mi favorito personal es Gulp. En definitiva, es una solución sencilla, rápida y fiable. En este artículo, veremos cómo funciona Gulp bajo el capó al intentar implementar nuestra propia herramienta similar a Gulp.
API de trago
Gulp viene con solo cuatro funciones simples:
- trago.tarea
- trago.src
- trago.dest
- gulp.watch
Estas cuatro funciones simples, en varias combinaciones, ofrecen todo el poder y la flexibilidad de Gulp. En la versión 4.0, Gulp introdujo dos nuevas funciones: gulp.series y gulp.parallel. Estas API permiten que las tareas se ejecuten en serie o en paralelo.
De estas cuatro funciones, las tres primeras son absolutamente esenciales para cualquier archivo Gulp. permitiendo definir e invocar tareas desde la interfaz de línea de comandos. El cuarto es lo que hace que Gulp sea verdaderamente automático al permitir que se ejecuten tareas cuando cambian los archivos.
Gulpfile
Este es un archivo gulp elemental:
gulp.task('test', function{ gulp.src('test.txt') .pipe(gulp.dest('out')); });
Describe una tarea de prueba simple. Cuando se invoca, el archivo test.txt en el directorio de trabajo actual debe copiarse en el directorio ./out . Pruébalo ejecutando Gulp:
touch test.txt # Create test.txt gulp test
Tenga en cuenta que el método .pipe
no es parte de Gulp, es una API de flujo de nodo, conecta un flujo legible (generado por gulp.src('test.txt')
) con un flujo de escritura (generado por gulp.dest('out')
). Toda la comunicación entre Gulp y los complementos se basa en flujos. Esto nos permite escribir el código de gulpfile de una manera tan elegante.
Cumplir con el enchufe
Ahora que tenemos una idea de cómo funciona Gulp, construyamos nuestra propia herramienta similar a Gulp: Plug.
Comenzaremos con la API plug.task. Debería permitirnos registrar tareas, y las tareas deberían ejecutarse si el nombre de la tarea se pasa en los parámetros del comando.
var plug = { task: onTask }; module.exports = plug; var tasks = {}; function onTask(name, callback){ tasks[name] = callback; }
Esto permitirá registrar las tareas. Ahora tenemos que hacer que esta tarea sea ejecutable. Para mantener las cosas simples, no crearemos un iniciador de tareas por separado. En su lugar, lo incluiremos en nuestra implementación de complemento.
Todo lo que tenemos que hacer es ejecutar las tareas nombradas en los parámetros de la línea de comandos. También debemos asegurarnos de intentar hacerlo en el siguiente ciclo de ejecución, después de que se hayan registrado todas las tareas. La forma más fácil de hacerlo es ejecutar tareas en una devolución de llamada de tiempo de espera, o preferiblemente procesar.nextTick:
process.nextTick(function(){ var taskName = process.argv[2]; if (taskName && tasks[taskName]) { tasks[taskName](); } else { console.log('unknown task', taskName) } });
Componga plugfile.js así:
var plug = require('./plug'); plug.task('test', function(){ console.log('hello plug'); })
… y ejecutarlo.
node plugfile.js test
Mostrará:
hello plug
subtareas
Gulp también permite definir subtareas en el registro de tareas. En este caso, plug.task debe tomar 3 parámetros, el nombre, la matriz de subtareas y la función de devolución de llamada. Implementemos esto.
Tendremos que actualizar la API de la tarea 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); } });
Ahora, si nuestro plugfile.js se ve así:
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'); })
… ejecutándolo
node plugfile.js test
… debe mostrar:
from sub task 1 from sub task 2 hello plug
Tenga en cuenta que Gulp ejecuta subtareas en paralelo. Pero para simplificar las cosas, en nuestra implementación estamos ejecutando subtareas secuencialmente. Gulp 4.0 permite controlar esto usando sus dos nuevas funciones API, que implementaremos más adelante en este artículo.
Origen y Destino
El complemento será de poca utilidad si no permitimos que los archivos se lean y escriban. Entonces, a continuación, implementaremos plug.src
. Este método en Gulp espera un argumento que sea una máscara de archivo, un nombre de archivo o una matriz de máscaras de archivo. Devuelve un flujo de nodo legible.
Por ahora, en nuestra implementación de src
, solo permitiremos nombres de archivos:
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; }
Tenga en cuenta que usamos objectMode: true
, un parámetro opcional aquí. Esto se debe a que los flujos de nodos funcionan con flujos binarios de forma predeterminada. Si necesitamos pasar/recibir objetos de JavaScript a través de transmisiones, debemos usar este parámetro.
Como puede ver, creamos un objeto artificial:
{ name: path, //file name buffer: data //file content }
… y lo pasó al arroyo.
Por otro lado, el método plug.dest debe recibir un nombre de carpeta de destino y devolver un flujo de escritura que recibirá objetos del flujo .src. Tan pronto como se reciba un objeto de archivo, se almacenará en la carpeta 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; }
Actualicemos nuestro plugfile.js:

var plug = require('./plug'); plug.task('test', function(){ plug.src('test.txt') .pipe(plug.dest('out')) })
… crear prueba.txt
touch test.txt
… y ejecutarlo:
node plugfile.js test ls ./out
test.txt debe copiarse en la carpeta ./out .
Gulp funciona de la misma manera, pero en lugar de nuestros objetos de archivo artificial, utiliza objetos de vinilo. Es mucho más conveniente, ya que contiene no solo el nombre del archivo y el contenido, sino también metainformación adicional, como el nombre de la carpeta actual, la ruta completa al archivo, etc. Puede que no contenga todo el búfer de contenido, pero en su lugar tiene un flujo legible del contenido.
Vinilo: mejor que los archivos
Existe una excelente biblioteca vinyl-fs que nos permite manipular archivos representados como objetos de vinilo. Esencialmente, nos permite crear transmisiones legibles y escribibles basadas en la máscara de archivo.
Podemos reescribir funciones de enchufe usando la biblioteca vinyl-fs. Pero primero necesitamos instalar vinyl-fs:
npm i vinyl-fs
Con esto instalado, nuestra nueva implementación de Plug se verá así:
var vfs = require('vinyl-fs') function onSrc(fileName){ return vfs.src(fileName); } function onDest(path){ return vfs.dest(path); } // ...
… y para probarlo:
rm out/test.txt node plugFile.js test ls out/test.txt
Los resultados deberían seguir siendo los mismos.
Complementos de trago
Dado que nuestro servicio Plug utiliza la convención de flujo de Gulp, podemos usar complementos nativos de Gulp junto con nuestra herramienta Plug.
Probemos uno. Instalar trago-renombrar:
npm i gulp-rename
… y actualice plugfile.js para usarlo:
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')); });
Ejecutar plugfile.js ahora debería, lo adivinó, producir el mismo resultado.
node plugFile.js test ls out/renamed.txt
Supervisión de cambios
El último método, pero no menos importante, es gulp.watch
Este método nos permite registrar el detector de archivos e invocar tareas registradas cuando los archivos cambian. Vamos a implementarlo:
var plug = { task: onTask, src: onSrc, dest: onDest, watch: onWatch }; function onWatch(fileName, taskName){ fs.watchFile(fileName, (event, filename) => { if (filename) { tasks[taskName](); } }); }
Para probarlo, agregue esta línea a plugfile.js:
plug.watch('test.txt','test');
Ahora, en cada cambio de test.txt , el archivo se copiará en la carpeta de salida con su nombre cambiado.
Serie vs Paralelo
Ahora que todas las funciones fundamentales de la API de Gulp están implementadas, avancemos un paso más. La próxima versión de Gulp contendrá más funciones API. Esta nueva API hará que Gulp sea más potente:
- trago.paralelo
- gulp.series
Estos métodos permiten al usuario controlar la secuencia en la que se ejecutan las tareas. Para registrar subtareas en paralelo se puede usar gulp.parallel, que es el comportamiento actual de Gulp. Por otro lado, gulp.series puede usarse para ejecutar subtareas de manera secuencial, una tras otra.
Supongamos que tenemos test1.txt y test2.txt en la carpeta actual. Para copiar esos archivos a nuestra carpeta en paralelo, hagamos un archivo de complemento:
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 la implementación, las funciones de devolución de llamada de la subtarea están hechas para devolver su flujo. Esto nos ayudará a rastrear el ciclo de vida de la corriente.
Comenzaremos a modificar nuestra API:
var plug = { task: onTask, src: onSrc, dest: onDest, parallel: onParallel, series: onSeries };
Tendremos que actualizar la función onTask también, ya que necesitamos agregar metainformación de tareas adicional para ayudar a nuestro iniciador de tareas a manejar las subtareas correctamente.
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 mantener las cosas simples, usaremos async.js, una biblioteca de utilidades para manejar funciones asincrónicas para ejecutar tareas en paralelo o en serie:
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(); } } }
Confiamos en el 'fin' del flujo de nodos que se emite cuando un flujo ha procesado todos los mensajes y se cierra, lo que indica que la subtarea está completa. Con async.js, no tenemos que lidiar con un gran lío de devoluciones de llamada.
Para probarlo, primero ejecutemos las subtareas en 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
Y ejecute las mismas subtareas en serie:
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
Conclusión
Eso es todo, hemos implementado la API de Gulp y ahora podemos usar los complementos de Gulp. Por supuesto, no use Plug en proyectos reales, ya que Gulp es más que lo que hemos implementado aquí. Espero que este pequeño ejercicio lo ayude a comprender cómo funciona Gulp bajo el capó y nos permita usarlo con mayor fluidez y ampliarlo con complementos.