Cualquiera que haya desarrollado algo medianamente complejo (tampoco mucho) usando Javascript conocerá herramientas de «compilación» como grunt con las cuales podemos automatizar procesos como la validación de sintaxis, la concatenación de ficheros, la minificación, la ejecución de tests, etc.
A principios del 2014, una alternativa a grunt empezó a tomar peso dentro de la comunidad de Javascript, donde como todos sabéis, si algo tiene más de 24 horas ya está obsoleto y es necesario reemplazarlo inmediatamente. Esa alternativa es gulp, un sistema de compilación basado en streams maravilloso y que nos permite hacer cosas nunca vistas.
¿Qué aporta Gulp?
La idea fundamental detrás de gulp es el uso de streams para procesar la información. En lugar de estar leyendo archivos de disco, procesándolos, generando archivos intermedios, volviéndolos a leer y procesándolos con otra herramienta, en gulp los archivos se convierten en streams, se van procesando en memoria y se almacenan en disco al final, lo que debería suponer una mejora en rendimiento al evitar parte de la entrada/salida.
Además, esto hace que la arquitectura de los plugins se simplifique, puesto que básicamente un plugin de gulp es únicamente una función que recibe un stream, lo transforma, y devuelve otro stream. Eso ha dado lugar a un ecosistema lleno de plugins muy simples, enfocados a realizar una tarea muy concreta y hacerla bien. ¿Os suena? Sí, es la típica filosofía unix.
Junto a los streams, otro factor que define la forma de trabajar con gulp es el uso de código frente a configuración. Mientra que en grunt se utiliza la configuración a través del método grunt.initConfig
para definir de una forma más o menos declarativa lo que hará cada plugin, en gulp esto se hace explícitamente a través de código, como veremos más adelante.
Por último, en lugar de ejecutar las tareas de forma secuencial como hace por defecto grunt, gulp utiliza un sistema de orquestación de tareas basado en dependencias. Esto nos permite definir cada tarea indicando de qué otras tareas depende, y gulp es capaz de ejecutar todo el conjunto de tareas que le digamos con el máximo grado de paralelismo posible. Esta gestión de dependencias entre tareas es muy típica en la mayoría de sistemas de compilación, como veíamos hace tiempo con MSBuild.
Un ejemplo con Gulp, Browserify y ReactJS
Para ver un ejemplo completo (aunque simple) de gulp, vamos a reescribir el sistema de compilación que usamos para ver cómo trabajar con grunt, browserify y reactjs.
Lo primero que necesitamos es instalar unas cuantas dependencias:
npm install --save-dev gulp npm install --save-dev vinyl-source-stream npm install --save-dev browserify npm install --save-dev reactify
De todos ellos, el más extraño es vinyl-source-stream, que forma parte de un conjunto de utilidades que nos permiten convertir entre distintos formatos de streams (más sobre eso luego) y manejarlos como una especie de «sistema de archivos virtual».
Teniendo esto instalado, podemos crear nuestro script de compilación, gulpfile.js
:
/*global require */ var gulp = require('gulp'), source = require('vinyl-source-stream'), browserify = require('browserify'), reactify = require('reactify'); gulp.task('browserify', function() { var bundle = browserify({entries: './lib/index.js' }) .transform(reactify) .bundle(); return bundle .pipe(source('app.js')) .pipe(gulp.dest('./dist/)')); }); gulp.task('watch', ['browserify'], function() { gulp.watch('./lib/**/*.js', ['browserify']) }); gulp.task('default', ['browserify']); gulp.task('dev', ['watch']);
Vemos que podemos definir tareas con gulp.task
, que recibe el nombre de la tarea, un array con los nombres de las tareas de las que depende (opcional) y una función con la implementación de la tarea (también opcional).
La primera tarea, browserify
se encarga de generar el javascript unificado listo para utilizar en el browser, y como véis se define mediante código ejecutable, en lugar de mediante un objeto de configuración como haríamos en grunt.
La segunda tarea, watch
, se encarga de monitorizar cambios en lo ficheros originales para lanzar la tarea browserify
. Para que antes de ejecutarse esta tarea se ejecute browserify
, la incluimos como dependencia en la definición de watch
con el array ['browserify']
.
Por último, definimos dos tareas basadas en las otras, default
y dev
, que nos simplifiquen el lanzamiento de las opciones más habituales.
Es importante tener en cuenta que las dependencias sólo indican eso, dependencia, no orden de ejecución. Si hiciésemos que fuese dev
quien lanza el primer browserify
antes del watch
, podríamos tener algo así:
gulp.task('browserify', function() {...}); gulp.task('watch', function() {...}); gulp.task('dev', ['browserify', 'watch']);
El problema de este código es que, al no haber una dependencia entre watch
y browserify
, al ejecutar dev
se ejecutarán ambas tareas en paralelo, que no exactamente lo que queremos (queremos que se ejecute primero una vez browserify
entero, luego ya que empiece el watch
).
Si lo comparáis con el script original que teníamos de grunt, éste es algo más corto (~25 LOC vs ~35 LOC) y, según gustos, tal vez algo más fácil de seguir. Tenéis el código completo en mi cuenta de github para comparar uno y otro.
Streams, Vinyl Streams, y Buffers
Hemos pasado de puntillas sobre el código de la tarea browserify
porque merece echarle un vistazo con detenimiento.
En esa tarea estamos usando directamente el módulo de browserify
normal (no es un plugin específico de gulp) y generar el bundle con nuestros ficheros.
Este bundle es un stream de node.js, pero gulp trabaja con streams de vinyl, por lo que necesitamos convertirlo a un stream de vinyl, el sistema de archivos virtual del que hablábamos antes. Para eso podemos usar un pipe
con source('app.js')
, que se encargará de encapsular el stream de node.js en un pseudo-fichero de vinyl con nombre ‘app.js’.
Una vez que hemos pasado a streams del mundo gulp, podemos pasarlo por gulp.dest('./build/')
que guardará el stream en la carpeta indicada. Aquí termina el proceso, pero en realidad gulp.dest
devuelve el stream original, por lo que podríamos seguir haciendo cosas con él, por ejemplo minificarlo y guardarlo con otro nombre en otra carpeta.
Si todo esto te ha parecido un lío entre un tipo de stream y otro, tienes razón. A veces resulta muy confuso saber qué tipo de stream tienes en cada momento y si necesitas convertirlo a una cosa u otra.
Para acabar de arreglarlo, existen otros plugins (como gulp-uglify) que trabajan con otra estructura de datos diferente, los Buffers, que se pueden generar con vinyl-buffer.
Entonces, ¿Grunt o Gulp?
Si nos basamos en popularidad, todavía gana grunt, aunque gulp le está recortando terreno rápido y probablemente en menos de un año le iguale o le supere, siempre que no aparezca otro sistema que se coma a estos dos, que hablando de javascript todo es posible.
El uso de código (gulp) o configuración (grunt) para definir las tareas, es un poco cuestión de gustos. A mi hay tareas que me resulta más natural definir usando código, y otras que me parece más sencillo gestionar mediante configuración. Lo cierto es que en grunt siempre has podido definir tareas basadas en funciones, por lo que técnicamente podrías pasar de la configuración y hacerlo todo a partir de grunt.registerTask('myTask', function() {...})
. En ese sentido, quizá grunt esa más flexible porque puedes manejarte con las dos opciones (aunque una sea más idiomática que otra).
Con los streams de gulp resulta cómodo manejar el proceso de información a través de pipes, y si estás acostumbrado a programación funcional (o aunque sea a LINQ), la idea de ir transformando la entrada paulatinamente es natural y agradable. Eso sí, como veíamos antes puede resultar complicado saber qué tipo de stream tienes en cada momento y a qué tienes que convertirlo para enlazarlo con la siguiente operación.
Una ventaja del uso de los streams es que muchas veces puedes ahorrarte el usar un plugin, y puedes recurrir directamente al paquete original, lo que evita tener plugins desactualizados con respecto a los paquetes que encapsulan. Esto podrías hacerlo en grunt, pero es algo que en el ecosistema de gulp es más habitual.
El sistema de dependencias entre tareas de gulp me gusta. Es algo a lo que estaba acostumbrado de MSBuild, y me gusta poder recuperarlo. Aunque a veces parezca más fácil definir la ejecución de forma imperativa (primero haz esto, luego haz esto y termina con esto), cuando tienes un script con muchos puntos de entrada (el del servidor de integración, el de desarrollo, el de tirar los tests…) viene bien poder definir las dependencias entre tareas y que el sistema decida automáticamente qué tiene que ejecutar para llegar a donde tú quieres.
Si a estas alturas te sientes un poco estafado porque no te he dicho claramente que elijas grunt o gulp, lamento decepcionarte, pero no es mi estilo. En realidad no creo que ninguno sea mucho mejor que el otro, y cada uno tiene cosas que me gustan más y cosas que me gustan menos, como he ido contando en el post.
Si vas a empezar un nuevo proyecto elige el que más te apetezca y pruébalo. Si tienes un proyecto que ya esta usando el que sea, no creo que merezca la pena cambiarlo al otro a menos que tengas un motivo muy concreto para hacerlo.
Hola Juan. Mi más sincera enhorabuena por tu blog. Lo descubrí hace poquito y me está encantando :)
En cuanto al tema del post, mi experiencia es que Grunt lo recomendaría para gente que no está acostumbrada a Node y los streams. Con Grunt controlas las dependencias mejor y creo que, al menos de momento, tiene más comunidad y plugins detrás.
Por otra parte Gulp es más rápido y en mi opinión más legible, pero en cuanto tienes unas cuantas dependencias en cascada se sigue peor.
Un saludo
Hola Emilio,
Muchas gracias por aportar tu opinión y tu experiencia de primera mano.
Sobre el tema de velocidad, ¿has medido la mejora?
En un proyecto con el que estuve probando en el que se procesan un par de megas de javascript con broserify, jsx-transform y uglify, la diferencia era pequeña (1.90s Grunt vs 1.68s Gulp). Sí, es un 20%, pero sobre unos tiempos donde tampoco se nota mucho.
Supongo que dependerá del proyecto, pero por el tipo de procesos que yo suelo hacer, en los que no hay resultados intermedios en disco, me da la impresión de que no voy a notar gran mejora (y mira que me gustaría).
Un saludo,
Juanma
Pues no me he tomado el tiempo de medirlo, pero al mencionarlo tú me ha dado la curiosidad y he hecho una pequeña prueba, compilando un buen montón de ficheros en Stylus, y este ha sido el resultado:
/tmp/prueba
▶ grunt
Running «stylus:compile» (stylus) task
>> 1 file created.
Done, without errors.
Execution Time (2015-06-04 18:06:00 UTC)
loading tasks 90ms ▇▇▇▇▇▇▇▇▇ 19%
stylus:compile 391ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 81%
Total 483ms
/tmp/prueba
▶ gulp
[20:06:03] Using gulpfile /tmp/prueba/Gulpfile.js
[20:06:03] Starting ‘default’…
[20:06:03] Finished ‘default’ after 8.57 ms
Después, buscando un poco he encontrado este post http://jaysoo.ca/2014/01/27/gruntjs-vs-gulpjs/ que parece que corrobora mi impresión.
De todas formas, es lo que tu dices. En un proceso pequeño te da igual que tarde un segundo más o uno menos. En un proceso de más envergadura la cosa cambia.
Un saludo :)
Gracias por hacer la prueba :-)
Donde yo creo que sí se puede notar más la mejora de rendimiento es en el paralelismo que introduce gulp. Si tienes que pasar los css por less, el javascript por browserify y ejecutar los tests, puedes hacerlo todo a la vez y seguramente sí que se note más mejora.
Muchas gracias por este post, estoy empezando con JavaScript y la verdad que me ha servido de mucho.