Potenzia il tuo file grunt

Come ottenere il massimo dalla configurazione della build

Paul Bakaus
Paul Bakaus

Introduzione

Se il mondo di Grunt è nuovo per te, un punto di partenza ideale è l'eccellente articolo di Chris Coyier "Grunt for People Who Think Things Like Grunt are Weird and Hard". Dopo l'introduzione di Chris, creerai il tuo progetto Grunt e avrai assaggiato una parte dei vantaggi offerti da Grunt.

In questo articolo, non ci concentreremo su ciò che molti plug-in Grunt fanno sul codice del tuo progetto, ma sul processo di compilazione Grunt stesso. Ti forniremo idee pratiche su:

  • Come mantenere il tuo Gruntfile ordinato e in ordine
  • Come migliorare drasticamente i tempi di creazione,
  • e come ricevere una notifica quando viene effettuata una creazione.

Ecco un breve disclaimer: Grunt è solo uno dei tanti strumenti che potresti utilizzare per portare a termine l'attività. Se Gulp è più il tuo stile, benissimo! Se dopo aver esaminato le opzioni disponibili, vorresti comunque creare la tua toolchain, va bene anche questo. Per questo articolo abbiamo scelto di concentrarci su Grunt grazie al suo solido ecosistema e alla sua base utenti di lunga data.

Organizzare il Gruntfile

Indipendentemente dal fatto che tu includa molti plug-in Grunt o devi scrivere molte attività manuali nel tuo Gruntfile, questo può diventare rapidamente molto complesso e difficile da gestire. Fortunatamente, esistono diversi plug-in che si concentrano proprio su questo problema: rendere di nuovo ordinato e ordinato il Gruntfile.

Il Gruntfile, prima dell'ottimizzazione

Ecco l'aspetto del nostro Gruntfile prima di essere ottimizzato:

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 ora stai dicendo "Ehi! Mi aspettavo molto di peggio! È davvero gestibile!", probabilmente hai ragione. Per semplicità, abbiamo incluso solo tre plug-in senza molta personalizzazione. L'utilizzo di un Gruntfile di produzione reale per creare un progetto di dimensioni moderate richiederebbe lo scorrimento continuo in questo articolo. Vediamo cosa possiamo fare.

Carica automaticamente i plug-in Grunt

Quando aggiungi un nuovo plug-in Grunt da utilizzare al progetto, dovrai aggiungerlo al file package.json come dipendenza npm e quindi caricarlo all'interno del file Gruntfile. Il plug-in "grunt-contrib-concat" potrebbe avere il seguente aspetto:

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

Se ora disinstalli il plug-in tramite npm e aggiorni il file package.json, ma dimentichi di aggiornare il gruntfile, la build si interromperà. È qui che viene utile il plug-in nifty load-grunt-tasks.

In precedenza, dovevamo caricare manualmente i nostri plug-in Grunt, in questo modo:

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

Con load-grunt-tasks, puoi comprimerlo in una riga di riepilogo seguente:

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

Dopo aver richiesto il plug-in, il plug-in analizza il file package.json, determina quali delle dipendenze sono plug-in Grunt e le carica tutte automaticamente.

Suddividere la configurazione Grunt in diversi file

load-grunt-tasks ha ridotto un po' il Gruntfile in termini di codice e complessità, ma quando configuri un'applicazione di grandi dimensioni, questo file diventerà comunque un file di dimensioni molto grandi. È qui che entra in gioco load-grunt-config. load-grunt-config ti consente di suddividere la configurazione di Gruntfile per attività. Inoltre, include load-grunt-tasks e le sue funzionalità.

Importante, tuttavia: la suddivisione del file Gruntfile potrebbe non funzionare sempre in tutte le situazioni. Se le tue attività hanno molte configurazioni condivise (ad es. utilizzi molti dei modelli Grunt), devi fare un po' di attenzione.

Con load-grunt-config, il tuo Gruntfile.js avrà il seguente aspetto:

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

È tutto. Dove si trovano le configurazioni delle nostre attività?

Crea una cartella denominata grunt/ nella directory del tuo Gruntfile. Per impostazione predefinita, il plug-in include i file all'interno della cartella che corrispondono al nome dell'attività che vuoi utilizzare. La struttura della directory dovrebbe avere l'aspetto seguente:

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

Ora mettiamo la configurazione dell'attività di ognuna delle nostre attività direttamente nei rispettivi file (vedrai che per lo più si tratta solo di operazioni di copia e incolla dal Gruntfile originale in una 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 non ti interessano i blocchi di configurazione JavaScript, load-grunt-tasks ti consente persino di utilizzare la sintassi YAML o CoffeeScript. Scriviamo il file finale richiesto in YAML: il file "aliases". Questo è un file speciale che registra gli alias delle attività. In precedenza era necessario eseguire la funzione registerTask nell'ambito del Gruntfile. Ecco i nostri:

grunt/aliases.yaml

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

E questo è tutto. Esegui questo comando nel tuo terminale:

$ grunt

Se tutto ha funzionato, ora verrà visualizzata l'attività "predefinita" ed eseguito tutto in ordine. Ora che abbiamo ridotto il nostro gruntfile principale a tre righe di codice non abbiamo mai bisogno di toccare ed esternalizzare ogni configurazione di attività, abbiamo finito qui. Ma cavoli, è ancora abbastanza lento costruire tutto. Vediamo cosa possiamo fare per migliorare questa situazione.

Ridurre al minimo il tempo di compilazione

Anche se le prestazioni del tempo di caricamento e di runtime dell'app web sono molto più importanti per l'attività rispetto al tempo necessario per eseguire una build, una build lenta è comunque problematica. Rende difficile eseguire build automatiche con plug-in come grunt-contrib-watch o dopo un commit Git abbastanza veloce e introduce una "sanzione" per eseguire effettivamente la build: più rapido è il tempo di compilazione, più agile è il flusso di lavoro. Se l'esecuzione della build di produzione richiede più di 10 minuti, eseguirai la build solo quando assolutamente necessario e uscirai per prendere il caffè mentre è in esecuzione. È un vero punto di forza della produttività. Abbiamo cose da velocizzare.

Crea solo file che sono davvero cambiati: grunt-newer

Dopo la creazione iniziale del sito, è probabile che tu abbia toccato solo pochi file del progetto quando tornerai a costruire. Supponiamo che nel nostro esempio tu abbia modificato un'immagine nella directory src/img/ (l'esecuzione di imagemin per riottimizzare le immagini avrebbe senso, ma solo per quella singola immagine) e, ovviamente, l'esecuzione di concat e uglify sta semplicemente sprecando cicli della CPU preziosi.

Ovviamente, puoi sempre eseguire $ grunt imagemin dal terminale anziché $ grunt per eseguire solo selettivamente un'attività, ma c'è un modo più intelligente. Si chiama grunt-newer.

Grunt-newer ha una cache locale in cui memorizza le informazioni sui file che sono stati effettivamente modificati ed esegue le attività solo per i file che sono stati effettivamente modificati. Diamo un'occhiata a come attivarlo.

Ti ricordi il nostro file aliases.yaml? Cambia da questo:

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

a questo:

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

È sufficiente anteporre "più recente:" a una qualsiasi delle attività per instradare i file di origine e di destinazione attraverso il plug-in grunt-newer, che a sua volta determina quali file, se presenti, eseguire l'attività.

Esegui più attività in parallelo: grunt-concurrent

grunt-concurrent è un plug-in che diventa davvero utile quando hai molte attività indipendenti l'una dall'altra e che richiedono molto tempo. Utilizza il numero di CPU del tuo dispositivo ed esegue più attività in parallelo.

La cosa migliore è che la sua configurazione è estremamente semplice. Supponendo di utilizzare load-grunt-config, crea il seguente nuovo file:

grunt/concurrent.js

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

Abbiamo semplicemente configurato le tracce di esecuzione parallela con i nomi "first" e "second". L'attività concat deve essere eseguita per prima e non c'è altro da eseguire nel frattempo nel nostro esempio. Nella nostra seconda traccia, mettiamo sia uglify sia imagemin, dato che sono indipendenti l'uno dall'altro ed entrambi richiedono molto tempo.

Questa operazione da sola non ha ancora alcun effetto. Dobbiamo cambiare l'alias delle attività predefinito in modo che puntino ai job simultanei anziché a quelli diretti. Ecco i nuovi contenuti di grunt/aliases.yaml:

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

Se ora rieseguirai la build grunt, il plug-in simultaneo eseguirà prima l'attività di concat, quindi genererà due thread su due diversi core della CPU per eseguire imagemin e uglify in parallelo. Benissimo!

Un consiglio però: è probabile che nel nostro esempio base, la combinazione di grugniti in parallelo non renderà la tua build notevolmente più veloce. Il motivo di ciò è l'overhead creato generando diverse istanze di Grunt in diversi thread: nel mio caso, almeno +300 ms pro spawn.

Quanto tempo ci è voluto? grugnito del tempo

Ora che stiamo ottimizzando ogni nostra attività, sarebbe davvero utile capire quanto tempo è necessario per svolgere ogni singola attività. Fortunatamente, esiste anche un plug-in per fare questo: time-grunt.

time-grunt non è un classico plug-in di grunt che carichi come attività npm, ma un plug-in che includi direttamente, simile a load-grunt-config. Aggiungeremo un grunt richiesto per il tempo al nostro Gruntfile, proprio come abbiamo fatto con load-grunt-config. Ora il nostro Gruntfile dovrebbe avere il seguente aspetto:

module.exports = function(grunt) {

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

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

};

Mi dispiace deluderti, ma il gioco è fatto: prova a eseguire di nuovo Grunt dal tuo terminale e per ogni attività (e oltre alla build totale) dovresti vedere un riquadro informativo con dati ben formattati sul tempo di esecuzione:

Ora del grugnito

Notifiche di sistema automatiche

Ora che hai una build Grunt fortemente ottimizzata che viene eseguita rapidamente e a condizione di crearla automaticamente in qualche modo (ad esempio guardando i file con grunt-contrib-watch o dopo i commit), non sarebbe bello se il tuo sistema potesse avvisarti quando la tua nuova build è pronta per essere utilizzata o quando è accaduto qualcosa di brutto? Scopri le grunt-notify.

Per impostazione predefinita, grunt-notify fornisce notifiche automatiche per tutti gli errori e gli avvisi di Grunt utilizzando qualsiasi sistema di notifica disponibile sul tuo sistema operativo: Growl per OS X o Windows, Mountain Lion's e Mavericks' Notification Center e Notify-send. Sorprendentemente, tutto ciò che serve per ottenere questa funzionalità è installare il plug-in da npm e caricarlo nel tuo Gruntfile (ricorda che, se utilizzi grunt-load-config sopra, questo passaggio è automatizzato).

Ecco come apparirà a seconda del sistema operativo:

Notify

Oltre a errori e avvisi, configuriamola in modo che venga eseguita al termine dell'ultima attività. Supponendo che tu stia utilizzando grunt-load-config per suddividere le attività tra i file, ti servirà questo file:

grunt/notify.js

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

Nel primo livello dell'oggetto config, la chiave deve corrispondere al nome dell'attività a cui vogliamo connetterla. In questo esempio, il messaggio verrà visualizzato subito dopo l'esecuzione dell'attività imagemin, che è l'ultima nella nostra catena di build.

Riepilogo

Se hai seguito l'attività dall'alto verso il basso, ora sei orgogliosa di possedere un processo di creazione super ordinato e organizzato, incredibilmente veloce grazie al parallelizzazione e all'elaborazione selettiva e che ti avvisa se qualcosa va storto.

Se scopri un'altra gemma che migliora ulteriormente Grunt e i suoi plug-in, faccelo sapere. Fino ad allora, buon ronzio!

Aggiornamento (14/02/2014): per ottenere una copia del progetto Grunt completo di esempio funzionante, fai clic qui.