A la hora de desarrollar un sitio web, podemos encontrarnos con la necesidad de ir probando de forma rápida diferentes librerías o plugins Javascript/CSS. Una forma habitual suele ser el descargarlas en local, editar el html enlazándolas, y a base de ensayo y error, ir dejándolas como definitivas en nuestro código o eliminándolas según corresponda.
Vamos a ver como realizar esto de forma cómoda y sencilla mediante tareas Grunt, manteniendo al mismo tiempo la limpieza y optimización de nuestros assets CSS/JS. Grunt.js es una librería JavaScript que nos permite configurar tareas automáticas y así ahorrarnos tiempo en nuestro desarrollo y despliegue.
Antes que os asuste la extensión del post, indicar que el código está duplicado, primero completo, y luego explicado paso a paso, y que me ha llevado escribir este post unas 20 veces más que implementar todo el sistema.
1.- Planteamiento
En nuestro caso de ejemplo vamos a suponer que nos interesa disponer de:
- Algunos contenidos CSS y Javascript desde CDNs, con una URL genérica de última versión
- Cargamos otros con versiones concretas también desde CDNs
- Por último, cargamos otros más desde disco, sean propios o de terceros
La estructura de carpeta de estos estáticos será tal que:
/s /vendor /librerias.css /librerias.js /css /mi.css /todo.css /js /mi.js /todo.js
2.- Dependencias
Dando por supuesto que disponemos de NodeJS
, y tras instalar grunt utilizando npm con un simple sudo npm install -g grunt-cli
, empezaremos por definir los módulos de NodeJS que nos interesa importar creando en el root de nuestro proyecto un fichero package.json
similar a este:
{ "name" : "PuerLugoMin", "title" : "PuerLugoMin", "version" : "1.0.0", "devDependencies": { "grunt": "0.4.5", "grunt-contrib-concat": "0.4.0", "grunt-contrib-cssmin" : "0.10.0", "grunt-contrib-watch" : "0.6.1", "grunt-contrib-uglify" : "0.5.0", "grunt-curl" : "2.0.2" } }
Una vez hecho esto, instalaremos estas dependencias con el comando
npm install
3.- La Tarea Grunt
Solucionadas las dependencias, definiremos una tarea Grunt de ejemplo Gruntfile.js
también en el root de nuestro proyecto:
module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), "curl-dir": { 's/vendor/': [ "http://cdn.jsdelivr.net/g/jquery,bootstrap", "http://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.2.0/bootbox.min.js", "http://cdn.jsdelivr.net/bootstrap/3.1.1/css/bootstrap.min.css", "http://cdn.jsdelivr.net/bootstrap/3.1.1/css/bootstrap-theme.min.css", ] }, concat: { options: { separator: '\n', }, css: { src: ['s/vendor/bootstrap.min.css', 's/vendor/bootstrap-theme.min.css', 's/css/mi.css'], dest: 's/css/todo.css' }, js : { src : ['s/vendor/jquery,bootstrap', 's/vendor/bootbox.min.js', 's/js/mi.js'], dest : 's/js/todo.js' } }, cssmin : { css:{ src: 's/css/todo.css', dest: 's/css/todo.css' } }, uglify : { js: { files: { 's/js/todo.js' : [ 's/js/todo.js' ] } } }, watch: { files: ['s/css/mi.css', 's/js/mi.js'], tasks: ['default'] } }); grunt.loadNpmTasks('grunt-curl'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.registerTask('default', ['concat:css', 'cssmin:css', 'concat:js', 'uglify:js']); grunt.registerTask('update', ['curl-dir', 'concat:css', 'cssmin:css', 'concat:js', 'uglify:js']); };
4.- La Tarea Grunt paso a paso
Vamos a ver este fichero por partes; para empezar, curl-dir
(entrecomillado debido al -), define una función que descarga archivos de CDN en una carpeta de nuestra elección, en este caso lo configuro para descargar varias librerías populares en la carpeta relativa s/vendor
, la primera con las últimas versiones de Javascript de jQuery y Twitter Bootstrap en un único archivo gracias a JSDelivr, y las demás con versiones concretas de otras librerías, indicando tanto las dependencias JS como CSS.
"curl-dir": { 's/vendor/': [ "http://cdn.jsdelivr.net/g/jquery,bootstrap", "http://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.2.0/bootbox.min.js", "http://cdn.jsdelivr.net/bootstrap/3.1.1/css/bootstrap.min.css", "http://cdn.jsdelivr.net/bootstrap/3.1.1/css/bootstrap-theme.min.css", ] },
Por su parte, concat
define una función que concatena dichos archivos, utilizando el separador «\n» (salto de línea), para evitar conflictos con los finales de linea de cada archivo a unir. Tanto para el CSS como para el JS, definimos los archivos fuente como una colección en src
, y el fichero destino en dest
.
En mi caso llamo a los archivos finales dest
: todo.css
y todo.js
respectivamente, y le paso a la función los nombres finales que tienen los archivos previamente descargados en disco en src
; Aquí es donde viene parte de la gracia de este sistema, ya que no tengo que limitarme a los archivos descargados, si no que puedo indicarle archivos en disco que yo haya creado, o que haya descargado manualmente antes. Basta con indicar el orden de las dependencias de forma correcta para evitar problemas. En mi caso, mi.css
y mi.js
son los archivos sobre los que trabajo y escribo mi propio código, extendiendo el Javascript y CSS de las librerías previas, con lo que simplemente, lo añado al final de cada lista para que sobreescriban cualquier estilo o función definido por estas.
Una vez llamemos a esta función, los archivos serán concatenados en el orden especificado en el mencionado src
y guardados en dest
. Como adelanto, estos nombres de archivo todo.css
y todo.js
serán los que luego enlazaré desde mi HTML.
concat: { options: { separator: '\n', }, css: { src: ['s/vendor/bootstrap.min.css', 's/vendor/bootstrap-theme.min.css', 's/css/mi.css'], dest: 's/css/todo.css' }, js : { src : ['s/vendor/jquery,bootstrap', 's/vendor/bootbox.min.js', 's/js/mi.js'], dest : 's/js/todo.js' } },
Una vez hemos descargado los archivos que nos interesan, procedemos a su Minificado, reducción de peso por el método de ofuscación o eliminación de elementos innecesarios, tales como saltos de línea, comentarios, etc… Para ello basta con indicar una función cssmin
a la que indicaremos el fichero fuente con src
y el destino con dest
. En mi caso la elección es obvia, indico la ruta de mi fichero previamente concatenado en ambas variables, para que realice la compresión del mismo y lo sobreescriba.
cssmin : { css:{ src: 's/css/todo.css', dest: 's/css/todo.css' } },
Uglify
es al Javascript lo que cssmin
al CSS, asi que procedemos exactamente de la misma manera, utilizando su propia sintaxis para obtener un resultado más compacto:
uglify : { js: { files: { 's/js/todo.js' : [ 's/js/todo.js' ] } } },
El módulo watch
merece mención aparte: se encarga de quedarse en espera controlando cambios en disco sobre archivos que le indiquemos, y en caso de detectar alguno, realiza la tarea que a su vez le especifiquemos. Esto es muy cómodo si queremos vigilar, por ejemplo, los fichero mi.css
y mi.js
(los únicos que modificaré regularmente) y en caso de cambios, lanzar automáticamente la concatenación y minificado de los mismos. De esta forma, nada más guardar cambios en nuestro editor, watch
salta de forma automática y actualiza nuestros ficheros finales todo.css
y todo.js
con los últimos cambios.
Para ello, deberemos definir una tarea, a la que llamaré default
y que definiremos más tarde, que es la que le indica qué debe hacer al detectar cambios.
watch: { files: ['s/css/mi.css', 's/js/mi.js'], tasks: ['default'] }
Una vez definidas todas las funciones que necesitamos, debemos indicar a Grunt con qué paquete de NodeJS debe ejecutar cada una, así que incluimos los paquetes definidos al principio en packages.json
grunt.loadNpmTasks('grunt-curl'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-cssmin');
Dependencias instaladas, funciones creadas, dependencias cargadas, es el momento de decirle a Grunt
qué comandos debe aceptar desde consola
, o desde watch
, y los definiremos registrando dos tareas, una para que watch
regenere los ficheros locales ante cualquier cambio, y otra que refresque versiones de librerías de cuando en cuando, para mantenernos actualizados. Recordemos que a watch
le indicamos que cuando hubiese cambios en mi.css
o mi.js
, llamase a la tarea default
, asi que esa será la primera que crearemos:
grunt.registerTask('default', [ 'concat:css', 'cssmin:css', 'concat:js', 'uglify:js' ]);
Sencillo y eficiente, indicamos el nombre default
, e indicamos qué acciones debe realizar según las funciones que creamos previamente:
- Concatenar los CSS -> Genera un todo.css temporal
- Minificar los CSS -> Genera el
todo.css final
- Concatenar los JS -> Genera un todo.js temporal
- Minificar los JS -> Genera el
todo.js final
El nombre de función default
no ha sido definido al azar: si llamamos a grunt
sin parámetros desde la consola, esa será la función que ejecute por defecto, salvo que se le indique otra.
Y ahora veremos como indicar otra tarea diferente, la que llamaremos update
y que se encargará de descargar las últimas versiones de las librerías:
grunt.registerTask('update', [ 'curl-dir', 'concat:css', 'cssmin:css', 'concat:js', 'uglify:js' ]);
Como veis, es exactamente igual a la anterior, solo que antes de todo el proceso de concatenación y minificado, descarga las últimas versiones utilizando la función curl-dir
que especificamos al principio.
5.- Utilizando nuestro automatismo Grunt
¡ Ya estamos listos para empezar a trabajar con Grunt !
Tenemos dos formas de hacerlo, la estrictamente manual, muy flexible, donde podemos invocar a Grunt de múltiples formas desde consola:
grunt update
: ejecutará descarga, concatenación y minificados a través de la función «update»grunt default
: lo mismo, pero sin descargagrunt
: sin parámetros, ejecutará lo mismo que indicandodefault
grunt cssmin:css
: si queremos invocar una de las funciones directamente, también podemosgrunt curl-dir
: otro ejemplo de lo anterior
¡ También en modo automático !
Por otra parte, para utilizar la modalidad de actualización directa basada en watch
, bastará con ejecutar grunt watch
para obtener algo como esto:
marcos@nabovalley:~/test$ sudo grunt watch Running "watch" task Waiting...
En este momento dejamos esa consola abierta, y comenzamos nuestro desarrollo. Cada vez que editemos el fichero mi.css
o mi.js
de nuestro proyecto, watch
lanzará la tarea default
especificada, que realizará la concatenación y minificado, entregando los ficheros finales todo.css
y todo.js
. Como estos ficheros son los que enlazo desde mi HTML, basta con guardar en el editor y recargar la web para apreciar los cambios, con todo el CSS y JS listos para pasar directamente a producción si es necesario.
Además, watch
es capaz de hacer livereload
, una configuración especial que levanta un servidor propio, y es capaz de recargar el navegador por nosotros en el mismo instante que hagamos cambios a uno de los archivos monitorizados. Para ello, basta con configurarlo:
options: { livereload: true, },
Y añadir a nuestro HTML la etiqueta <script src="//localhost:35729/livereload.js"></script>
que es la que se encargará de recargar cuando sea necesario.
6.- Tips adicionales
No olvidemos proteger nuestros archivos de dependencias y grunt añadiendo a nuestro .htaccess
de Apache o similar las líneas:
RedirectMatch 404 /Gruntfile\\.js(/|$) RedirectMatch 404 /package\\.json(/|$)
Y del mismo modo, si utilizamos un sistema de control de versiones, excluyamos la carpeta node_modules
del mismo, por ejemplo mediante .hgignore
node_modules/
Conclusión
Hasta aquí este ladrillo, Grunt es una herramienta de automatización fantástica, con multitud de posibilidades, y de la que apenas hemos arañado la superficie. No dejes de dar un buen vistazo al enorme listado oficial de plugins para Grunt, seguro que se te ocurren nuevas e imaginativas formas de ponerlo a trabajar a tu servicio. No en vano, no hay ser más vago sobre la tierra que un programador.
2 comentarios en “Compresión y gestión de JS/CSS con Grunt”