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 indicandodefaultgrunt 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”