Potencia tu gruntfile

Cómo aprovechar al máximo la configuración de compilación

Introducción

Si el mundo de Grunt es nuevo para ti, un lugar ideal para empezar es el excelente artículo de Chris Coyier "Grunt for People Who Think Things Like Grunt". Después de la introducción de Chris, habrás configurado tu propio proyecto de Grunt y habrás probado una parte del poder que ofrece Grunt.

En este artículo, no nos centraremos en lo que hacen varios complementos de Grunt en el código real de tu proyecto, sino en el proceso de compilación de Grunt. Te daremos ideas prácticas sobre lo siguiente:

  • Cómo mantener tu Gruntfile ordenado y ordenado,
  • Cómo mejorar drásticamente el tiempo de compilación
  • y cómo recibir una notificación cuando se produce una compilación.

Es hora de una breve renuncia de responsabilidad: Grunt es solo una de las muchas herramientas que puedes usar para realizar la tarea. Si Gulp es más tu estilo, genial. Si, después de consultar las opciones que existen, aún quieres compilar tu propia cadena de herramientas, no te preocupes: Decidimos enfocarnos en Grunt para este artículo debido a su ecosistema sólido y base de usuarios de larga data.

Cómo organizar tu Gruntfile

Ya sea que incluyas muchos complementos de Grunt o tengas que escribir muchas tareas manuales en tu archivo Gruntfile, rápidamente puede volverse muy difícil de manejar y difícil de mantener. Por suerte, hay varios complementos que se centran exactamente en ese problema: hacer que tu archivo Gruntfile esté ordenado de nuevo.

El Gruntfile, antes de la optimización

Así se ve nuestro Gruntfile antes de hacerle una optimización:

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      dist: {
        src: ['src/js/jquery.js','src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
        dest: 'dist/build.js',
      }
    },
    uglify: {
      dist: {
        files: {
          'dist/build.min.js': ['dist/build.js']
        }
      }
    },
    imagemin: {
      options: {
        cache: false
      },

      dist: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['**/*.{png,jpg,gif}'],
          dest: 'dist/'
        }]
      }
    }
  });

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

  grunt.registerTask('default', ['concat', 'uglify', 'imagemin']);

};

Si ahora dices “¡Hola! ¡Esperaba mucho peor! ¡Eso se puede mantener!", es probable que tengas razón. Para simplificar, solo incluimos tres complementos sin mucha personalización. El uso de un Gruntfile de producción real para compilar un proyecto de tamaño moderado requeriría desplazamiento infinito en este artículo. ¡Veamos qué podemos hacer!

Cómo cargar automáticamente tus complementos de Grunt

Cuando agregues un complemento de Grunt nuevo que quieras usar a tu proyecto, deberás agregarlo a tu archivo package.json como una dependencia npm y, luego, cargarlo en Gruntfile. Para el complemento "grunt-contrib-concat", podría verse de la siguiente manera:

// tell Grunt to load that plugin
grunt.loadNpmTasks('grunt-contrib-concat');

Si ahora desinstalas el complemento a través de npm y actualizas tu package.json, pero olvidas actualizar tu Gruntfile, tu compilación fallará. Aquí es donde el ingenioso complemento load-grunt-tasks te ayuda.

Antes, tuvimos que cargar manualmente nuestros complementos de Grunt de la siguiente manera:

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

Con las tareas de carga pesada, puedes contraerlas en la siguiente línea:

require('load-grunt-tasks')(grunt);

Después de solicitar el complemento, analizará el archivo package.json, determinará cuáles de las dependencias son complementos de Grunt y las cargará todas automáticamente.

Divide tu configuración de Grunt en diferentes archivos

load-grunt-tasks redujo un poco el código y la complejidad del archivo Gruntfile, pero a medida que configures una aplicación grande, se convertirá en un archivo muy grande. Es aquí donde entra en juego load-grunt-config. load-grunt-config le permite dividir la configuración del archivo Gruntfile por tarea. Además, encapsula load-grunt-tasks y su funcionalidad.

Sin embargo, es importante que la división del Gruntfile no siempre funcione en todas las situaciones. Si tienes mucha configuración compartida entre tus tareas (es decir, usas muchas de las plantillas de Grunt), debes tener un poco de cuidado.

Con load-grunt-config, tu archivo Gruntfile.js se verá de la siguiente manera:

module.exports = function(grunt) {
  require('load-grunt-config')(grunt);
};

¡Sí! Eso es todo. ¡Todo el archivo! ¿Dónde se encuentran nuestras configuraciones de tareas?

Crea una carpeta llamada grunt/ en el directorio de tu Gruntfile. De forma predeterminada, el complemento incluye archivos dentro de esa carpeta que coinciden con el nombre de la tarea que quieres usar. Nuestra estructura de directorios debería verse de la siguiente manera:

- myproject/
-- Gruntfile.js
-- grunt/
--- concat.js
--- uglify.js
--- imagemin.js

Ahora, pongamos la configuración de cada una de nuestras tareas directamente en los archivos correspondientes (verás que, en su mayoría, solo se copian y pegan desde el Gruntfile original en una nueva estructura):

grunt/concat.js

module.exports = {
  dist: {
    src: ['src/js/jquery.js', 'src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
    dest: 'dist/build.js',
  }
};

grunt/uglify.js

module.exports = {
  dist: {
    files: {
      'dist/build.min.js': ['dist/build.js']
    }
  }
};

grunt/imagemin.js

module.exports = {
  options: {
    cache: false
  },

  dist: {
    files: [{
      expand: true,
      cwd: 'src/',
      src: ['**/*.{png,jpg,gif}'],
      dest: 'dist/'
    }]
  }
};

Si los bloques de configuración de JavaScript no son lo tuyo, load-grunt-tasks incluso te permite usar la sintaxis YAML o CoffeeScript. Escribamos nuestro último archivo obligatorio en YAML: el archivo “aliases”. Este es un archivo especial que registra alias de tareas, algo que tuvimos que hacer antes como parte del archivo Gruntfile mediante la función registerTask. Esta es la nuestra:

grunt/aliases.yaml

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

Eso es todo. Ejecuta el siguiente comando en la terminal:

$ grunt

Si todo funcionó, ahora se examinará la tarea “predeterminada” y se ejecutará todo en orden. Ahora que redujimos el Gruntfile principal a tres líneas de código que nunca necesitamos tocar ni externalizamos cada configuración de tareas, terminamos aquí. Pero, hombre, todavía está bastante lento para construir todo. Veamos qué podemos hacer para mejorar eso.

Cómo minimizar el tiempo de compilación

Si bien el rendimiento del tiempo de ejecución y de carga de tu app web es mucho más crítico para el negocio que el tiempo necesario para ejecutar una compilación, una compilación lenta sigue siendo problemática. ya que dificultará la ejecución de compilaciones automáticas con complementos como grunt-contrib-watch o después de una confirmación de Git lo suficientemente rápido. Además, ingresará una “sanción” para ejecutar la compilación: cuanto más rápido sea el tiempo de compilación, más ágil será el flujo de trabajo. Si tu compilación de producción tarda más de 10 minutos en ejecutarse, solo lo harás cuando sea necesario y te perderás para tomar café mientras se ejecuta. Esto mejora la productividad. Tenemos mucho que acelerar.

Solo se compilan los archivos que realmente cambiaron: grunt-newer

Después de la compilación inicial de tu sitio, es probable que solo hayas modificado algunos archivos del proyecto cuando comiences a compilar de nuevo. Supongamos que, en nuestro ejemplo, cambiaste una imagen en el directorio src/img/ (tiene sentido ejecutar imagemin para volver a optimizar las imágenes, pero solo para esa imagen) y, por supuesto, volver a ejecutar concat y uglify es solo desperdiciar ciclos de CPU preciados.

Por supuesto, siempre puedes ejecutar $ grunt imagemin desde tu terminal en lugar de $ grunt para realizar una tarea solo de forma selectiva, pero existe una manera más inteligente. Se llama grunt-newer.

Grunt-newer tiene una caché local en la que almacena información sobre los archivos que cambiaron, y solo ejecuta tus tareas para los archivos que sí cambiaron. Veamos cómo activarlo.

¿Recuerdas nuestro archivo aliases.yaml? Cámbialo de esta forma:

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

a esto:

default:
  - 'newer:concat'
  - 'newer:uglify'
  - 'newer:imagemin'

Simplemente anteponer “newer:” a cualquiera de tus tareas canaliza primero tus archivos fuente y de destino a través del complemento grunt-más nuevo, que luego determina para qué archivos, si los hay, se debe ejecutar la tarea.

Ejecución de varias tareas en paralelo: grunt-concurrent

grunt-concurrent es un complemento que se vuelve muy útil cuando tienes muchas tareas que son independientes entre sí y consumen mucho tiempo. Utiliza la cantidad de CPU en el dispositivo y ejecuta múltiples tareas en paralelo.

Lo mejor de todo es que su configuración es muy simple. Suponiendo que usas load-grunt-config, crea el siguiente archivo nuevo:

grunt/concurrent.js

module.exports = {
  first: ['concat'],
  second: ['uglify', 'imagemin']
};

Simplemente configuramos los segmentos de ejecución en paralelo con los nombres “first” y “second”. La tarea concat debe ejecutarse primero y, mientras tanto, en nuestro ejemplo, no hay nada más que ejecutar. En la segunda pista, colocamos uglify y también imagemin, ya que son independientes entre sí y tardan mucho tiempo.

Esto por sí solo no hace nada todavía. Debemos cambiar nuestro alias de tarea default para que apunte a los trabajos simultáneos en lugar de a los directos. Este es el nuevo contenido de grunt/aliases.yaml:

default:
  - 'concurrent:first'
  - 'concurrent:second'

Si ahora vuelves a ejecutar tu compilación grunt, el complemento simultáneo ejecutará primero la tarea de concat y, luego, generará dos subprocesos en dos núcleos de CPU diferentes para ejecutar imagemin y uglify en paralelo. ¡Bien!

No obstante, te daremos un consejo: en nuestro ejemplo básico, grunt-concurrent no hará que tu compilación sea mucho más rápida. Esto se debe a la sobrecarga que se genera al generar diferentes instancias de Grunt en diferentes subprocesos: en mi caso, el generador profesional de al menos +300 ms.

¿Cuánto tiempo tomó? No hay mucho tiempo.

Ahora que estamos optimizando cada una de nuestras tareas, sería muy útil comprender cuánto tiempo requiere la ejecución de cada tarea individual. Por suerte, también existe un complemento para eso: time-grunt.

time-grunt no es un complemento de grunt clásico que cargas como tarea npm, sino un complemento que incluyes directamente, similar a load-grunt-config. Agregaremos una solicitud de tiempo grueso a nuestro Gruntfile, al igual que hicimos con load-grunt-config. Nuestro Gruntfile debería verse de la siguiente manera:

module.exports = function(grunt) {

  // measures the time each task takes
  require('time-grunt')(grunt);

  // load grunt config
  require('load-grunt-config')(grunt);

};

Lamento decepcionarte, pero eso es todo. Prueba volver a ejecutar Grunt desde tu Terminal y, por cada tarea (y, además, la compilación total), deberías ver un panel de información con el formato correcto en el tiempo de ejecución:

Hora de gruñido

Notificaciones automáticas del sistema

Ahora que tienes una compilación de Grunt altamente optimizada que se ejecuta rápidamente y que la compiles automáticamente de alguna manera (es decir, observando archivos con grunt-contrib-watch o después de confirmaciones), ¿no sería genial si tu sistema pudiera notificarte cuando tu compilación nueva esté lista para consumir o cuando ocurra algo malo? Conoce a grunt-notify.

De forma predeterminada, grunt-notify proporciona notificaciones automáticas para todos los errores y advertencias de Grunt usando cualquier sistema de notificaciones disponible en tu SO: Growl para OS X o Windows, Centro de notificaciones de Mountain Lion y Mavericks, y Notificar-send. Sorprendentemente, todo lo que necesitas para obtener esta funcionalidad es instalar el complemento de npm y cargarlo en tu Gruntfile (recuerda que, si estás usando grunt-load-config arriba, este paso es automático).

A continuación, te mostramos cómo se verá según tu sistema operativo:

Notificar

Además de los errores y las advertencias, vamos a configurarlo para que se ejecute después de que la última tarea termine de ejecutarse. Suponiendo que usas grunt-load-config para dividir las tareas entre archivos, este es el archivo que necesitaremos:

grunt/notify.js

module.exports = {
  imagemin: {
    options: {
      title: 'Build complete',  // optional
        message: '<%= pkg.name %> build finished successfully.' //required
      }
    }
  }
}

En el primer nivel de nuestro objeto de configuración, la clave debe coincidir con el nombre de la tarea a la que queremos conectarlo. En este ejemplo, el mensaje aparecerá justo después de que se ejecute la tarea imagemin, que es la última de nuestra cadena de compilación.

Resumen

Si lo seguiste desde la parte superior, ahora eres el orgulloso propietario de un proceso de compilación que es muy ordenado y organizado, es increíblemente rápido debido a la paralelización y el procesamiento selectivo, y te notifica cuando algo sale mal.

Si descubres otra gema que mejora Grunt y sus complementos, infórmanos al respecto. Hasta entonces, ¡feliz gruñido!

Actualización (14/2/2014): Para obtener una copia del proyecto de Grunt de ejemplo completo y funcional, haz clic aquí.