Supercharging your Gruntfile

How to squeeze the most out of your build configuration.

HTML5 Rocks

Introduction

Se l'universo di Grunt ti é nuovo, un buon punto di inizio é l'eccellente articolo “Grunt for People Who Think Things Like Grunt are Weird and Hard” di Chris Coyier. Dopo aver letto la sua introduzione, potrai aver impostato il tuo progetto Grunt e avuto un assaggio delle potenzialitá offerte da Grunt.

In questo articolo non ci concentreremo su quali tra i numerosi plugin di Grunt applicare al tuo progetto, bensí proprio sul processo di build di Grunt. Verranno forniti dei concetti pratici su:

  • come mantenere il Gruntfile pulito e in ordine,
  • come migliorare il tempo di buil in maniera considerevole,
  • come ricevere una notifica per ogni evento di build.

C'é peró bisogno di una piccola nota: Grunt é solo uno dei tanti tool che posso essere usati per questo scopo. Se Gulp rispecchia maggiormente il tuo stile, perfetto! Se dopo aver valutato tutte le possibilitá a disposizione vuoi comunque crearti i tuoi strumenti personali, ok! Noi abbiamo scelto di concentrarci su Grunt in questo articolo per via del suo ecosistema robusto e del suo bacino di utenti di lunga data.

Organizzare il Gruntfile

Se il Gruntfile include tanti plugin Grunt o un gran numero di task manuali, questo puó diventare pesante e di difficile manutenzione. Per fortuna, alcuni plugin si concentrano proprio su questo problema: far tornare il Gruntfile pulito e ordinato.

Il Gruntfile, prima dell'ottimizzazione

Ecco come appare il nostro Gruntfile prima di aver apportato su di esso alcuna ottimizzazione:

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']);

};

Se stai pensando "Ehi! Mi aspettavo qualcosa di molto peggio! Questo file in realtá sembra mantenibile!", forse hai ragione. Per ragioni di semplicitá, abbiamo incluso solo tre plugin senza troppe personalizzazioni. Un Gruntfile reale usato in produzione per un progetto di dimensioni moderate avrebbe richiesto uno scroll infinito in questo articolo. Vediamo quindi cosa si puó fare!

Autoload dei plugin Grunt

Suggerimento: load-grunt-config include load-grunt-tasks, quindi se non sei interessato a imparare nel dettaglio quello che fa e vuoi saltare questo paragrafo, non ferirai i miei sentimenti.

Quando aggiungi un nuovo plugin Grunt per utilizzarlo nel tuo progetto, devi includerlo sia nel file package.json come dipendenza npm, sia nel Gruntfile. Per il plugin grunt-contrib-concat, questa inclusione sará del tipo:

// dici a Grunt di caricare il plugin
grunt.loadNpmTasks('grunt-contrib-concat');

Ora, se disinstalli il plugin tramite npm e aggiorni il package.json, ma dimentichi di aggiornare il Gruntfile, il build verrá interrotto. Ecco che in questa circostanza l'ingegnoso plugin load-grunt-tasks ci viene in aiuto.

Mentre prima dovevamo caricare manualmente i plugin Grunt, in questo modo:

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

con load-grunt-tasks questi comandi collassano in una singola riga:

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

Dopo aver richiesto il plugin, questo analizza il file package.json e determina quali tra le dipendenze sono plugin Grunt per caricarli automaticamente.

Dividere le configurazioni di Grunt in piú file

load-grunt-tasks riduce un poco la complessitá e il codice del Gruntfile, ma se ti trovi a configurare un'applicazione grande, questo sará comunque un file molto esteso. Qui entra in gioco load-grunt-config. load-grunt-config consente di dividere le configurazioni del Gruntfile per task. Inoltre, questo plugin incapsula load-grunt-tasks e le sue funzionalitá!

Una cosa importante: dividere il Gruntfile non é sempre la cosa migliore. Se lavori con tante configurazioni condivise tra diversi task (ad esempio, se usi parecchio il templating Grunt), dovresti essere cauto nel farlo.

Con load-grunt-config, il Gruntfile avrá questo aspetto:

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

Esatto, proprio questo, l'intero file! Ma dove risiedono le nostre configurazioni?

Crea una cartella chiamata grunt/ nella cartella del Gruntfile. Di default, il plugin ricerca in questa cartella i file di configurazione i cui nomi corrispondono ai nomi dei task che si vogliono utilizzare. Nel nostro caso, la struttura della cartella é di questo tipo:

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

A questo punto inseriamo la configurazione di ciascun task nel file corrispondente (si tratta essenzialmente di lavori di copia-incolla dal Gruntfile originale in questa nuova struttura).

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/'
    }]
  }
};

Se le configurazioni in JavaScript non sono proprio il tuo forte, load-grunt-tasks consente addirittura di usare la sintassi YAML o CoffeeScript. Il nostro ultimo file richiesto lo scriviamo in YAML - il file aliases. Si tratta di un file speciale che registra gli alias per i task, compito che avevamo assolto nel Gruntfile tramite la funzione registerTask. Ecco il nostro:

grunt/aliases.yaml

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

E questo é tutto! Esegui questo comando nel terminale:

$ grunt

Se tutto funziona, questo cerca il task di “default” ed esegue tutto in ordine. Ora che abbiamo ridotto il Gruntfile a tre linee di codice che non toccheremo piú, ed estratte le configurazioni di tutti i task. abbiamo finito. Ma caspita, completare un build é ancora parecchio lento. Vediamo cosa si puó fare per migliorare.

Minimizzare il tempo di build

Anche se le performance di runtime e caricamento della tua applicazione web sono molto piú critiche in termini di business rispetto al tempo richiesto per completare un build, avere un tempo di build troppo lento é comunque un problema. Questo rende infatti difficile eseguire build automatici sufficientemente veloci con plugin come grunt-contrib-watch o dopo un commit Git, e introduce una “penalitá” nell'esecuzione effettiva del build - piú veloce é il tempo di build, piú agile é il workflow. Se un build di produzione impiega piú di 10 minuti per essere eseguito, lo farai solo quando devi per forza farlo, e nel mentre ti allontanerai per un caffé. Tutto questo é estremamente controproducente. Abbiamo bisogno di velocizzare le cose.

Build per i soli file effettivamente modificati: grunt-newer

Dopo un primo build iniziale per il tuo sito, é probabile che ti troverai a lanciare di nuovo un bild dopo aver modificato solo pochi file all'interno del progetto. Diciamo, secondo il nostro esempio, che sia stata modificata un'immagine nella cartella src/img/ - lanciare imagemin per ottimizzare di nuovo le immagini avrebbe senso, ma non per una singola immagine - e ovviamente, un'altra esecuzione di concat e uglify é solo una perdita di preziosi cicli di CPU.

Certo, si puó sempre eseguire da terminale $ grunt imagemin invece di $ grunt per l'esecuzione selettiva di un task, ma esiste una strada migliore. Si chiama grunt-newer.

Grunt-newer dispone di una cache locale nella quale mantiene informazioni su quali file sono effettivamente cambiati, e solo per questi file esegue i task opportuni. Vediamo come attivarlo.

Ricordi il file aliases.yaml? Modificalo da cosí:

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

a cosí:

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

La semplice anteposizione di “newer:” per ciascuno dei task indirizza i file sorgente e destinazione verso il plugin grunt-newer, il quale determina per quali file, se ce ne sono, il task debba essere eseguito.

Eseguire piú task in parallelo: grunt-concurrent

grunt-concurrent é un plugin che torna molto utile quando si ha abbondanza di task indipendenti l'uno dall'altro e dispendiosi in termini di tempo. Questo plugin sfrutta il numero di CPU del tuo dispositivo ed esegue task multipli in parallelo.

La cosa piú bella é che la configurazione di questo plugin é estremamente semplice. Ipotizzando di stare usando load-grunt-config, crea questo nuovo file:

grunt/concurrent.js

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

Abbiamo cosí impostato per l'esecuzione parallela delle tracce con i nomi “first” and “second”. Il task concat deve essere eseguito per primo, e nel nostro esempio non c'é nient'altro che debba essere eseguito nel frattempo. Nella seconda traccia abbiamo unito uglify e imagemin, dal momento che questi due task sono indipendenti tra di loro e richiedono entrambi un tempo di esecuzione considerevole.

Questo file da solo non fa ancora nulla. Dobbiamo modificare l'alias per il task default affinché punti ai processi concorrenti piuttosto che a quelli sequenziali. Ecco il nuovo contenuto di grunt/aliases.yaml:

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

Se si esegue di nuovo un build grunt, il plugin concorrente esegue per primo il task concat, e dopo genera due thread su due core differenti della CPU per eseguire in parallelo imagemin e uglify. Wow!

Un piccolo avvertimento: c'é il rischio che nel nostro esempio basilare grunt-concurrent non renda il build significativamente piú veloce. Questo é dovuto all'overhead introdotto dalla creazione di istanze multiple di Grunt per thread differenti: nel mio caso, almeno +300ms per thread.

Quanto tempo ci ha messo? time-grunt

Ora che abbiamo ottimizzato ognuno dei nostri task, sarebbe molto utile capire quanto tempo ciascun task impiega per essere eseguito. Per fortuna, esiste un plugin anche per questo: time-grunt.

time-grunt non é uno dei classici plugin grunt da caricare come task npm, ma piuttosto un plugin da includere direttamente, come load-grunt-config. Aggiungiamo un require per time-grunt nel Gruntfile, proprio come abbiamo fatto con load-grunt-config. Il Gruntfile dovrebbe a questo punto essere cosí:

module.exports = function(grunt) {

  // misura il tempo di ciascun task
  require('time-grunt')(grunt);

  // carica le configurazioni di grunt
  require('load-grunt-config')(grunt);

};

Mi dispiace deluderti, ma si tratta solo di questo - prova ad eseguire di nuovo Grunt da terminale, e per ciascun task (compreso l'intero processo di build) dovresti vedere un pannello di controllo ben formattato contenente i tempi di esecuzione:

Notifiche di sistema automatiche

Ora che abbiamo apportato pesanti ottimizzazioni al processo di build di Grunt, velocizzando la sua esecuzione, nel caso in cui tu abbia automatizzato tale processo (ad esempio, controllando i file con grunt-contrib-watch, o dopo ciascun commit), non sarebbe grandioso se il tuo sistema potesse inviarti una notifica quando un nuovo build é pronto per essere eseguito, o quando succede qualcosa di brutto? Ecco a te grunt-notify.

Di default, grunt-notify fornisce notifiche automatiche per tutti gli errori e i warning di Grunt, usando qualunque sistema di notifiche disponibile sul tuo sistema operativo: Growl per OS X o Windows, il Centro Notifiche per Mountain Lion e Mavericks, e Notify-send. Sorprendentemente, l'unica cosa da fare per per ottenere questa funzionalitá é installare il plugin da npm e caricarlo nel Gruntfile (ricordati che se stai usando grunt-load-config, l'inclusione é automatica!).

Ecco come appare una notifica, a seconda del sistema operativo:

Configuriamo il plugin in modo tale che, assieme ad errori e warning, venga eseguito al termine dell'esecuzione del nostro ultimo task. Assumendo di stare usando grunt-load-config per suddividere i task tra diversi file, questo é il file di cui abbiamo bisogno:

grunt/notify.js

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

Nel primo livello di questa configurazione, la chiave deve corrispondere al nome del task cui ci vogliamo connettere. L'esempio mostrerá un messaggio subito dopo l'esecuzione del task imagemin, l'ultimo della nostra catena di build.

Tiriamo le somme

Se hai seguito tutto dall'inizio, sei adesso il fiero possessore di un processo di build super ordinato e organizzato, estremamente veloce grazie al parallelismo e ai task selettivi, e in grado di inviare notifiche ogni volta che qualcosa va storto.

Se ti capita di scoprire qualche altra perla in grado di migliorare ulteriormente Grunt e i suoi plugin, per favore faccelo sapere! Fino ad allora, buon grunting!

Aggiornamento (14/2/2014): se vuoi ottenere una copia completa e funzionante del progetto Grunt di esempio, clicca qui.

Comments

0

Next steps

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the Apache 2.0 License.