Gulp: el arma secreta de un desarrollador web para maximizar la velocidad del sitio
Publicado: 2022-03-11Muchos de nosotros tenemos que manejar proyectos basados en web que se utilizan en producción, que brindan diversos servicios al público. Cuando se trata de proyectos de este tipo, es importante poder compilar e implementar nuestro código rápidamente. Hacer algo rápidamente a menudo conduce a errores, especialmente si un proceso es repetitivo, por lo tanto, es una buena práctica automatizar dicho proceso tanto como sea posible.
En esta publicación, veremos una herramienta que puede ser parte de lo que nos permitirá lograr dicha automatización. Esta herramienta es un paquete npm llamado Gulp.js. Para familiarizarse con la terminología básica de Gulp.js utilizada en esta publicación, consulte "Introducción a la automatización de JavaScript con Gulp" que Antonios Minas, uno de nuestros compañeros desarrolladores de Toptal, publicó anteriormente en el blog. Asumiremos una familiaridad básica con el entorno npm, ya que se usa ampliamente en esta publicación para instalar paquetes.
Sirviendo activos front-end
Antes de continuar, retrocedamos unos pasos para obtener una descripción general del problema que Gulp.js puede resolver por nosotros. Muchos proyectos basados en web cuentan con archivos JavaScript front-end que se entregan al cliente para proporcionar varias funcionalidades a la página web. Por lo general, también hay un conjunto de hojas de estilo CSS que también se entregan al cliente. A veces, al mirar el código fuente de un sitio web o una aplicación web, podemos ver un 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>Hay algunos problemas con este código. Tiene referencias a dos hojas de estilo CSS separadas y cuatro archivos JavaScript separados. Esto significa que el servidor debe realizar un total de seis solicitudes al servidor, y cada solicitud debe cargar un recurso por separado antes de que la página esté lista. Este es un problema menor con HTTP/2 porque HTTP/2 introduce paralelismo y compresión de encabezado, pero sigue siendo un problema. Aumenta el volumen total de tráfico que se requiere para cargar esta página y reduce la calidad de la experiencia del usuario porque lleva más tiempo cargar los archivos. En el caso de HTTP 1.1, también acapara la red y reduce la cantidad de canales de solicitud disponibles. Hubiera sido mucho mejor combinar los archivos CSS y JavaScript en un solo paquete para cada uno. De esa manera, solo habría un total de dos solicitudes. También hubiera sido bueno ofrecer versiones reducidas de estos archivos, que suelen ser mucho más pequeños que los originales. Nuestra aplicación web también podría fallar si alguno de los activos se almacena en caché y el cliente recibiría una versión desactualizada.
Un enfoque primitivo para resolver algunos de estos problemas es combinar manualmente cada tipo de activo en un paquete usando un editor de texto y luego ejecutar el resultado a través de un servicio de minimización, como http://jscompress.com/. Esto demuestra ser muy tedioso de hacer continuamente durante el proceso de desarrollo. Una mejora leve pero cuestionable sería alojar nuestro propio servidor minificador, utilizando uno de los paquetes disponibles en GitHub. Entonces podríamos hacer cosas que se verían algo similares a lo siguiente:
<script src="min/f=js/site.js,js/module1.js"></script>Esto serviría archivos minificados a nuestro cliente, pero no resolvería el problema del almacenamiento en caché. También causaría una carga adicional en el servidor, ya que nuestro servidor esencialmente tendría que concatenar y minimizar todos los archivos de origen de forma repetitiva en cada solicitud.
Automatización con Gulp.js
Seguramente podemos hacerlo mejor que cualquiera de estos dos enfoques. Lo que realmente queremos es automatizar la agrupación e incluirla en la fase de construcción de nuestro proyecto. Queremos terminar con paquetes de activos preconstruidos que ya están minimizados y listos para servir. También queremos obligar al cliente a recibir las versiones más actualizadas de nuestros activos incluidos en cada solicitud, pero aún queremos aprovechar el almacenamiento en caché si es posible. Afortunadamente para nosotros, Gulp.js puede manejar eso. En el resto del artículo, crearemos una solución que aprovechará el poder de Gulp.js para concatenar y minimizar los archivos. También usaremos un complemento para romper el caché cuando haya actualizaciones.
Crearemos la siguiente estructura de directorios y archivos en nuestro ejemplo:
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 El archivo gulpfile.js es donde definiremos las tareas que Gulp realizará por nosotros. package.json utiliza el archivo package.json para definir el paquete de nuestra aplicación y rastrear las dependencias que instalaremos. El directorio público es lo que se debe configurar para hacer frente a la web. El directorio de activos es donde almacenaremos nuestros archivos fuente. Para usar Gulp en el proyecto, necesitaremos instalarlo a través de npm y guardarlo como una dependencia de desarrollador para el proyecto. También querremos comenzar con el complemento concat para Gulp, que nos permitirá concatenar varios archivos en uno.
Para instalar estos dos elementos, ejecutaremos el siguiente comando:
npm install --save-dev gulp gulp-concatA continuación, querremos comenzar a escribir el contenido 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']);Aquí, estamos cargando la biblioteca gulp y su complemento concat. A continuación, definimos tres tareas.
La primera tarea ( pack-js ) define un procedimiento para comprimir varios archivos fuente de JavaScript en un solo paquete. Enumeramos los archivos de origen, que se agruparán, leerán y concatenarán en el orden especificado. Lo canalizamos al complemento concat para obtener un archivo final llamado bundle.js . Finalmente, le decimos a Gulp que escriba el archivo en public/build/js .
La segunda tarea ( pack-css ) hace lo mismo que la anterior, pero para las hojas de estilo CSS. Le dice a Gulp que almacene la salida concatenada como stylesheet.css en public/build/css .
La tercera tarea ( default ) es la que ejecuta Gulp cuando la invocamos sin argumentos. En el segundo parámetro, pasamos la lista de otras tareas a ejecutar cuando se ejecuta la tarea predeterminada.
Peguemos este código en gulpfile.js usando cualquier editor de código fuente que usemos normalmente, y luego guardemos el archivo en la raíz de la aplicación.
A continuación, abriremos la línea de comando y ejecutaremos:
gulp Si observamos nuestros archivos después de ejecutar este comando, encontraremos dos archivos nuevos: public/build/js/bundle.js y public/build/css/stylesheet.css . Son concatenaciones de nuestros archivos fuente, lo que soluciona parte del problema original. Sin embargo, no están minificados y todavía no se ha eliminado la memoria caché. Agreguemos minificación automatizada.
Optimización de activos construidos
Necesitaremos dos complementos nuevos. Para agregarlos ejecutaremos el siguiente comando:
npm install --save-dev gulp-clean-css gulp-minifyEl primer complemento es para minimizar CSS y el segundo es para minimizar JavaScript. El primero usa el paquete clean-css y el segundo usa el paquete UglifyJS2. Primero cargaremos estos dos paquetes en nuestro gulpfile.js:
var minify = require('gulp-minify'); var cleanCss = require('gulp-clean-css');Luego necesitaremos usarlos en nuestras tareas justo antes de escribir la salida en el disco:
.pipe(minify()) .pipe(cleanCss())El gulpfile.js ahora debería verse así:
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 a ejecutar trago de nuevo. Veremos que el archivo stylesheet.css se guarda en formato minificado, y el archivo bundle.js aún se guarda como está. Notaremos que ahora también tenemos bundle-min.js, que está minimizado. Solo queremos el archivo minimizado y queremos que se guarde como bundle.js , por lo que modificaremos nuestro código con parámetros adicionales:

.pipe(minify({ ext:{ min:'.js' }, noSource: true }))Según la documentación del complemento gulp-minify (https://www.npmjs.com/package/gulp-minify), esto establecerá el nombre deseado para la versión minificada y le indicará al complemento que no cree la versión que contiene la fuente original. Si eliminamos el contenido del directorio de compilación y volvemos a ejecutar gulp desde la línea de comandos, terminaremos con solo dos archivos minificados. Acabamos de terminar de implementar la fase de minificación de nuestro proceso de compilación.
Destrucción de caché
A continuación, querremos agregar la prevención de caché, y necesitaremos instalar un complemento para eso:
npm install --save-dev gulp-revY solicitarlo en nuestro archivo gulp:
var rev = require('gulp-rev');Usar el complemento es un poco complicado. Primero tenemos que canalizar la salida minimizada a través del complemento. Luego, tenemos que volver a llamar al complemento después de escribir los resultados en el disco. El complemento cambia el nombre de los archivos para que estén etiquetados con un hash único y también crea un archivo de manifiesto. El archivo de manifiesto es un mapa que nuestra aplicación puede utilizar para determinar los nombres de archivo más recientes a los que debemos hacer referencia en nuestro código HTML. Después de modificar el archivo gulp, debería quedar así:
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']); Eliminemos el contenido de nuestro directorio de compilación y ejecutemos Gulp nuevamente. Descubriremos que ahora tenemos dos archivos con etiquetas adheridas a cada uno de los nombres de archivo y un archivo manifest.json guardado en public/build . Si abrimos el archivo de manifiesto, veremos que solo tiene una referencia a uno de nuestros archivos minimizados y etiquetados. Lo que sucede es que cada tarea escribe un archivo de manifiesto separado y una de ellas termina sobrescribiendo a la otra. Tendremos que modificar las tareas con parámetros adicionales que les indicarán que busquen el archivo de manifiesto existente y fusionen los nuevos datos en él, si existe. La sintaxis para eso es un poco complicada, así que veamos cómo debería verse el código y luego repasemos:
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 la salida a rev.manifest() primero. Esto crea archivos etiquetados en lugar de los archivos que teníamos antes. Proporcionamos la ruta deseada de nuestro rev-manifest.json y le indicamos a rev.manifest() que se fusione con el archivo existente, si existe. Luego le estamos diciendo a Gulp que escriba el manifiesto en el directorio actual, que en ese momento será public/build. El problema de la ruta se debe a un error que se analiza con más detalle en GitHub.
Ahora tenemos minificación automatizada, archivos etiquetados y un archivo de manifiesto. Todo esto nos permitirá entregar los archivos más rápidamente al usuario y romper su caché cada vez que hagamos nuestras modificaciones. Sin embargo, solo quedan dos problemas.
El primer problema es que si hacemos alguna modificación a nuestros archivos fuente, obtendremos archivos recién etiquetados, pero los antiguos también permanecerán allí. Necesitamos alguna forma de eliminar automáticamente los archivos minificados antiguos. Resolvamos este problema usando un complemento que nos permitirá eliminar archivos:
npm install --save-dev delLo requeriremos en nuestro código y definiremos dos nuevas tareas, una para cada tipo de archivo fuente:
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' ]); });Luego nos aseguraremos de que la nueva tarea termine de ejecutarse antes que nuestras dos tareas principales:
gulp.task('pack-js', ['clean-js'], function () { gulp.task('pack-css', ['clean-css'], function () { Si volvemos a ejecutar gulp después de esta modificación, solo tendremos los últimos archivos minificados.
El segundo problema es que no queremos seguir corriendo cada vez que hacemos un cambio. Para resolver esto, necesitaremos definir una tarea de observador:
gulp.task('watch', function() { gulp.watch('assets/js/**/*.js', ['pack-js']); gulp.watch('assets/css/**/*.css', ['pack-css']); });También cambiaremos la definición de nuestra tarea por defecto:
gulp.task('default', ['watch']); Si ahora ejecutamos gulp desde la línea de comando, encontraremos que ya no construye nada al invocarlo. Esto se debe a que ahora llama a la tarea del observador que observará nuestros archivos de origen en busca de cambios y compilará solo cuando detecte un cambio. Si intentamos cambiar cualquiera de nuestros archivos fuente y luego miramos nuestra consola nuevamente, veremos que las tareas pack-js y pack-css se ejecutan automáticamente junto con sus dependencias.
Ahora, todo lo que tenemos que hacer es cargar el archivo manifest.json en nuestra aplicación y obtener los nombres de archivo etiquetados de eso. La forma en que hacemos eso depende de nuestra pila de tecnología y lenguaje back-end particular, y sería bastante trivial de implementar, por lo que no lo analizaremos en detalle. Sin embargo, la idea general es que podemos cargar el manifiesto en una matriz o un objeto, y luego definir una función de ayuda que nos permitirá llamar a los activos versionados desde nuestras plantillas de una manera similar a la siguiente:
gulp('bundle.js')Una vez que hagamos eso, no tendremos que preocuparnos por las etiquetas cambiadas en nuestros nombres de archivo nunca más, y podremos concentrarnos en escribir código de alta calidad.
El código fuente final de este artículo, junto con algunos recursos de muestra, se puede encontrar en este repositorio de GitHub.
Conclusión
En este artículo, analizamos cómo implementar la automatización basada en Gulp para nuestro proceso de compilación. Espero que esto le resulte útil y le permita desarrollar procesos de compilación más sofisticados en sus propias aplicaciones.
Tenga en cuenta que Gulp es solo una de las herramientas que se pueden usar para este propósito, y hay muchas otras, como Grunt, Browserify y Webpack. Varían en sus propósitos y en el alcance de los problemas que pueden resolver. Algunos pueden resolver problemas que Gulp no puede, como agrupar módulos de JavaScript con dependencias que se pueden cargar a pedido. Esto se conoce como "división de código" y es una mejora sobre la idea de servir un archivo grande con todas las partes de nuestro programa en cada página. Estas herramientas son bastante sofisticadas pero podrían cubrirse en el futuro. En una publicación siguiente, repasaremos cómo automatizar la implementación de nuestra aplicación.
