MarcosBL

Aprendiz de todo, maestro de nada

Compresión y gestión de JS/CSS con Grunt

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:

  1. Concatenar los CSS -> Genera un todo.css temporal
  2. Minificar los CSS -> Genera el todo.css final
  3. Concatenar los JS -> Genera un todo.js temporal
  4. 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 descarga
  • grunt: sin parámetros, ejecutará lo mismo que indicando default
  • grunt cssmin:css: si queremos invocar una de las funciones directamente, también podemos
  • grunt 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

  1. Pedro Peña
    26 junio, 2014 at 09:37

    Muy interesante, gracias por al apunte, no domino demasiado estas cosas de grunt y node.js y esto me ayuda a aprender.

    Saludos.

  2. Nicolás Carrascal
    6 febrero, 2017 at 03:47

    Fantástico, he mirado muchas web para implementar grunt y está ha sido la mejor con diferencia, completa, sencilla y funcional, me he quedado muy sorprendido cuando lo he probado y ha funcionado a la primera. Excelente!!!!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *