¡Hola! Me parece que andas usando un bloqueador de anuncios =( ¿Nos desbloqueas? ¿Por qué?
F13

Funcion 13

Introducción a Grunt

Hace no mucho tiempo, el cumplir con las buenas prácticas de código JavaScript era bastante tedioso. ¿Cómo compilar todo en un único archivo? ¿Cómo transformar archivos Sass en CSS? ¿Cómo...?

Empezaron a surgir algunas herramientas como Codekit, que hacían todo este trabajo por ti y, durante algún tiempo, estaba bien.

No obstante, con la llegada de Node.js y su adopción por parte de la comunidad, comenzaron a surgir herramientas que evitaban la tediosa repetición de ejecutar tareas y aquí es donde llegó Grunt. Por favor, no lo confundáis con Groot.

¿Qué es Grunt?

Como he adelantado, Grunt es una herramienta construida sobre Node.js. Es una herramienta de línea de comandos que permite ejecutar tareas desde la misma. Piensa en un asistente que se encarga de hacer todo el trabajo sucio por ti.

La comunidad de Grunt es bastante vibrante y hay tareas para todos los gustos y colores desde compilar CSS desde LESS y Sass a ejecutar tests. Por ejemplo tengo un conjunto de tareas para el blog que me permiten, tras realizar un cambio, compilar el CSS y el JavaScript, versionar todos los archivos y subirlos al servidor.

¿Por qué Grunt y no _?

La respuesta es altamente personal y principalmente basada en mi experiencia. Hay gran comunidad con Gulp o Broccoli por ejemplo.

Lo que me atrae de Grunt es su consistencia y lo sencillo de entender que es para todo el equipo.

Gulp es genial si sabes JavaScript, sabes cómo funcionan los Streams en Node y quieres enfangarte un poco escribiendo código. No obstante, dado que trabajo con gente que no tiene por qué saber JavaScript pero pueden necesitar hacer algún ajuste temporal o permanente a las tareas, Grunt parece más sencillo para toda la familia.

Comenzando con Grunt

Lo primero que hay que hacer para usar Grunt es instalar Node.js si no lo tenemos instalado ya.

¿Todo esto de Node.js te suena a Chino? ¿Te suena pero no sabes por dónde meterle mano? ¡Hazte con mi libro de introducción a Node.js y dejarás de sentirte perdido!

Una vez instalado Node, tenemos que instalar Grunt:

$ npm install -g grunt-cli

Este paso solo tendremos que hacerlo una vez en nuestra vida (por cada ordenador, claro). El siguiente paso es crear un archivo package.jsony otro archivo gruntfile.js en la raíz del directorio de tu proyecto.

El archivo package.json

Una vez tengas creado el archivo, pon algo así dentro del mismo:

{
    "name" : "PruebaGrunt",
    "version" : "1.0.0",
    "author" : "Querido Lector",
    "private" : true,
    "devDependencies" : {
        "grunt" : "~0.4.0"
    }
}

Lo más destacable es devDependencies en la que indicamos grunt. Básicamente le estamos diciendo a Node que, para poder desarrollar nuestro proyecto nos hace falta Grunt. Ahora Node podrá instalar la dependencia:

$ npm install

Una vez terminada la instalación deberías tener una carpeta node_modules en el directorio de tu proyecto, donde se almacena una copia de grunt. ¡Fantástico!

Creando el archivo Gruntfile.js

El archivo Gruntfile.js debe ir también en la carpeta raíz de nuestro proyecto. Vamos a colocar esto dentro del archivo:

module.exports = function(grunt){  
  grunt.registerTask('default', function(){
    grunt.log.writeln('Hola Mundo!');
  });
};

Ahora ejecuta lo siguiente desde la raíz de tu proyecto en el terminal:

$ grunt

Si todo va bien, deberías ver en tu terminal, Hola mundo!.

Pero, ¿cómo funciona? Bueno, no voy a explicar cada una de las funciones que tiene Grunt ya que queda un poco fuera del alcance del artículo, aun así creo que, si has llegado a leer hasta aquí, te mereces una explicación, ¿no?

Básicamente Grunt carga este archivo e invoca la función que pasamos en module.exports pasando grunt como único parámetro.

Dado que queremos que grunt haga cosas por nosotros, usamos registerTask para registrar una nueva tarea, llamada default. default es una tarea especial que se ejecuta cuando se llama a grunt sin ningún parámetro adicional. Podríamos haber usado registerTask('hola' pero en vez de solo grunt, tendríamos que escribir grunt hola para obtener el mismo resultado.

En este caso, la tarea contiene una llamada a la función log.writeln que lo que hace es escribir una línea en la consola, que es lo que nosotros vemos. ¡Bien!

Pues hasta aquí todo lo de Grunt.

Pero Antonio... Esto no es para nada lo que me habías prometido.

Tienes razón. Profundizaré un poco más.

Parámetros y configuración de Grunt

Grunt nos permite pasar parámetros también, que pasa a la tarea que estamos ejecutando. Yo no conozco a nadie que se llame mundo así que vamos a arreglar esa tarea:

module.exports = function(grunt){  
  grunt.registerTask('default', function(nombre){
    grunt.log.writeln('Hola ' + nombre + '!');
  });
};

Hasta aquí nada nuevo, solo hemos añadido el parámetro nombre y lo añadimos tal cual a la función. ¿Cómo le pasamos el parámetro ahora a Grunt?

$ grunt default:'Funcion 13'

Y veremos el saludo que esperábamos:

Running "default:Funcion 13" (default) task  
Hola Funcion 13!  

¡Bien! La lástima es que necesitamos ahora pasar el nombre de la tarea ya que si solo pusiéramos el parámetro, Grunt pensaría que el nombre de la tarea es Funcion 13.

Nuestra humilde tarea funciona bien hasta ahora pero con esta modificación necesitamos pasar un parámetro o mostrará:

Hola undefined!  

¿Y si guardamos un parámetro en algún sitio para usarlo por defecto? Veamos cómo podemos lograr esto:

module.exports = function(grunt){  
  grunt.initConfig({
    valores: {
      saludo: 'Mundo'
    }
  });

  grunt.registerTask('default', function(nombre){
    nombre = nombre || grunt.config.get('valores').saludo;
    grunt.log.writeln('Hola ' + nombre + '!');
  });
};

La función initConfig nos permite darle valores a grunt para que los recuerde. En este caso, hacemos uso de este valor en caso de que no se nos de un nombre para saludar. undefined después de todo no es nadie, ¿no? ¡Saludemos con propiedad!

Si ejecutamos esto ahora con o sin parámetros obtendremos resultados satisfactorios. No obstante, hay otro uso para los valores de configuración y es, el de usarlo en plantillas. Veamos:

module.exports = function(grunt){  
  grunt.initConfig({
    valores: {
      saludo: 'Mundo'
    }
  });

  grunt.registerTask('holaMundo', function(){
    grunt.log.writeln(
      grunt.template.process('Hola <%= valores.saludo %>!')
    );
  });
};

Ahora hemos creado una nueva tarea llamada hola-mundo que, si la ejecutamos volveremos a ver el famoso mensaje. Pero, ¿qué está pasando aquí?

Gracias a la función template.process, podemos cambiar una cadena por los valores almacenados en la configuración de Grunt. Como ves es realmente útil. En este caso hace falta usar template.process pero, como veremos más adelante, hay sitios en los que no hace falta usar esa función y esta utilidad será aun más versátil.

Antes de pasar a otro tema, me gustaría mostraros una nueva funcionalidad que podemos combinar con todo lo que hemos aprendido. Ya que hemos visto que podemos asignar valores para guardarlos como configuración, veamos cómo podemos guardar los valores en otro sitio, por ejemplo en un fichero. Vamos a crear un archivo llamado config.json que crearemos en el mismo directorio. El contenido no es nada nuevo:

{
  "valores" : {
    "saludo" : "Mundo"
  }
}

Lo único que hemos hecho es cambiarlo a JSON válido. Ahora veamos cómo podemos hacer que Grunt use este fichero:

module.exports = function(grunt){  
  grunt.initConfig(grunt.file.readJSON('config.json'));

  grunt.registerTask('hola-mundo', function(){
    grunt.log.writeln(
      grunt.template.process('Hola <%= valores.saludo %>!')
    );
  });
};

La función file.readJSON recibe una ruta de fichero y devuelve un objeto JavaScript. De hecho, si ejecutamos la tarea nuevamente, el mismo mensaje volverá a aparecer. ¡Fantástico!

Un caso de uso real

Ahora usaremos Grunt como lo usaremos normalmente. No obstante estas funciones que hemos visto anteriormente, son realmente útiles en algunos casos.

Pongámonos en situación. Tenemos una página fantástica con dos ficheros JavaScript: funciones.js y app.js. Por lo que hemos leído por ahí, sería bueno juntarlos los dos en uno solo y comprimir el código.

La estructura que tenemos es esta:

├── Gruntfile.js
├── config.json
├── node_modules
│   └── grunt
└── web
    ├── index.html
    └── js
        ├── app.js
        └── funciones.js

Separamos la web de Grunt y compañía como buena práctica, ya que si no estaría todo mezclado y sería algo más engorroso y menos claro, ¿no crees?

Como ya comenté al inicio del artículo, el ecosistema de tareas para Grunt es bastante elevado así que veamos las tareas que necesitamos:

Así que vamos a instalar las tareas:

$ npm install grunt-contrib-concat grunt-contrib-uglify --save-dev

Esto nos permite instalar las dos tareas con un solo comando y además actualiza nuestro fichero package.json.

Ahora tenemos que cargar las tareas para que Grunt sepa que existen, para que te hagas una idea, es similar a la idea de registerTask excepto que, en vez de cargar código que nosotros escribamos, usará el de la tarea que hemos instalado.

Así es como quedaría nuestro Gruntfile.js:

module.exports = function(grunt) {  
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
};

Con loadNpmTasks estamos diciéndole a Grunt, ey, hay dos tareas llamadas grunt-contrib-concat y grunt-contrib-uglify que he instalado con npm, ¿me las cargas?. Y obedientemente lo hará.

Configuración

Es buena idea añadir una configuración que indique dónde están las cosas. Ahora mismo te parecerá algo tonto ya que solo tenemos 3 directorios pero, después de todo, esto es solo un ejemplo, ¿no? De esta forma podrás usar las mismas configuraciones en nuevos proyectos que compartan estructuras similares o incluso distintas, con tan solo actualizar la configuración.

Vamos a ello. Añadimos lo siguiente justo arriba de las tareas:

grunt.initConfig({  
  rutas: {
    raiz: 'web',
    js: '<%= rutas.raiz %>/js'
  }
});

Aquí creamos el objeto rutas que apunta a las dos carpetas que tenemos. Como ves, hacemos uso de las plantillas que explicamos antes. En este caso no hace falta el uso de ninguna función en particular para que Grunt entienda de lo que le estamos hablando.

Ahora que ya tenemos todo listo, vamos a crear la configuración de cada tarea.

Antes de comprimir, necesitamos que los ficheros estén todos juntos. Así que configuremos la tarea concat:

module.exports = function(grunt) {  
  grunt.initConfig({
    rutas: {
      raiz: 'web',
      js: '<%= rutas.raiz %>/js'
    },
    concat: {
      app: {
        src: [
          '<%= rutas.js %>/funciones.js', 
          '<%= rutas.js %>/app.js'
        ],
        dest: '<%= rutas.js %>/app.min.js'
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
};

¡Vaya, seguimos usando loadInitConfig! Todo lo que tenemos que hacer es crear un objeto con el nombre de la tarea (concat) crear un objetivo (app) y definir las fuentes (primero funciones y luego app) y dónde queremos que nos deje el archivo, app.min.js. Cabe destacar que el nombre del objetivo podría haber sido cualquiera, hemos escogido app por ser un nombre sensible pero podríamos haber escogido caramelo y también habría valido. Lo interesante es que podríamos definir dos objetivos:

concat: {  
  app: {
    src: [
      '<%= rutas.js %>/funciones.js',
      '<%= rutas.js %>/app.js'
    ],
    dest: '<%= rutas.js %>/app.min.js'
  },
  fracaso: {
    src: [
      '<%= rutas.js %>/app.js',
      '<%= rutas.js %>/funciones.js'
    ],
    dest: '<%= rutas.js %>/fracaso.min.js'
  }
}

En este otro ejemplo definimos dos objetivos distintos, el que ya conocemos y fracaso ya que hemos colocado app antes que funciones ¡y eso no puede ser! Para ejecutar un objetivo concreto debemos usar la siguiente sintaxis:

$ grunt [nombre-tarea]:[nombre-objetivo]

Por ejemplo: $ grunt concat:fracaso y ejecutaría únicamente fracaso. Si ejecutáramos $ grunt concat se ejecutarían todos los objetivos uno detrás de otro.

Uglify nos permite hacer la tarea de concatenar también pero concat no deja de ser una tarea interesante y quería demostrar el uso de varias tareas y alias.

Después de esta pequeña desviación, configuremos uglify:

uglify: {  
  app: {
    src: '<%= concat.app.dest %>',
    dest: '<%= concat.app.dest %>'
  }
}

He aquí un buen truco. Configuramos concat y ahora usamos su fichero que, recordemos iba a estar ubicado en web/js/app.min.js. Como todo es una misma configuración, podemos usar el sistema de plantillas para resolver la ruta del fichero.

Ahora nos queda un último paso. Vamos a crear una tarea que englobe a las dos que acabamos de crear:

grunt.registerTask('scripts', ['concat:app', 'uglify:app']);  

Ya estamos familiarizados con registerTask, lo bueno es que esta función nos permite pasar una matriz de tareas, con o sin objetivo, para ejecutar. De esta forma, al ejecutar $ grunt scripts se ejecutará todo lo que acabamos de hacer. De esta forma acabamos de crear un alias que agrupa varias tareas.

Es recomendable crear alias separados por temáticas, de esta forma es sencillo llamar a las tareas en un determinado orden y es fácil reusarlas en el futuro. Hay tareas que usan otras tareas así que, ¡sé ordenado!

Por último, podríamos añadir scripts al alias especial default para que con solo ejecutar $ grunt, se complete nuestra tarea. Así es como queda nuestro Gruntfile.js completo.

module.exports = function(grunt) {  
  grunt.initConfig({
    rutas: {
      raiz: 'web',
      js: '<%= rutas.raiz %>/js'
    },
    concat: {
      app: {
        src: [
          '<%= rutas.js %>/funciones.js',
          '<%= rutas.js %>/app.js'
        ],
        dest: '<%= rutas.js %>/app.min.js'
      }
    },
    uglify: {
      app: {
        src: '<%= concat.app.dest %>',
        dest: '<%= concat.app.dest %>'
      }
    }
  });

  grunt.registerTask('scripts', ['concat:app', 'uglify:app']);
  grunt.registerTask('default', ['scripts']);

  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
};

A estas alturas deberías tener una base sólida para comenzar a trabajar con Grunt tú mismo. Como ya he dicho, Grunt tiene una comunidad muy activa de programadores que crean tareas para diferentes necesidades.

En el futuro escribiré de algunas técnicas más avanzadas que uso con Grunt.

Programador Front-end en First + Third y Potato. Trabajando con JavaScript y HTML5 desde el corazón de Sevilla.

Comentarios ¡Únete a la conversación!