Vai al contenuto principaleVai al footer
javascript
|
09 gennaio 15

BazarJS: La diaspora dei task runners Node.js

Prima puntata della nostra serie BazarJS dedicata ad esplorare il mondo delle SPA (single-page applications)... oggi parliamo di build tools e task runners!

Stefano VernaHead of DatoCMS

Come già accennato nel post precedente, la nascita di Node.js — ovvero di un runtime-environment Javascript in grado di vivere in maniera indipendente rispetto al browser — è stato indubbiamente un punto di svolta importante per il mondo Javascript. Node.js non è stato certo il primo esperimento tentato in questo senso (Rhino nasce nel lontano 1997), ma sicuramente è stato il primo a decollare e fare la differenza.

Rake, Make, Gradle, Ant... non c'è linguaggio di programmazione che non si porti dietro il suo build tool di riferimento, e Node.js è stato proprio l'ambiente che ha sbloccato la possibilità di creare strumenti analoghi nel mondo Javascript. Si tratta di un obiettivo importante, che ha finalmente permesso la creazione di task di processamento sui nostri file Javascript front-end capaci di raggiungere livelli di analisi ed introspezione non raggiungibili con le tecnologie precedenti.

Come è ovvio in un bazar, Javascript offre una quantità spropositata di differenti task runner tra cui scegliere. Il primo a nascere nel 2010 è stato Jake, poco dopo il primo rilascio di Node.js stesso. Da lì a poco Grunt, poi, Brunch, Mimosa, Gulp, Broccoli... Assurdo? Folle? You betcha.

Per mantenere un minimo livello di sensatezza, cerchiamo di limitarci ai tre concorrenti che in questo momento sembrano godere della maggiore popolarità.

Analisi

##### Grunt

* **Homepage:** http://gruntjs.com/
* **Numero di task disponibili:** 3.989
* **Data di creazione:** Settembre 2011
* **Github stars:** ★ 8.929
##### Gulp

* **Homepage:** http://gulpjs.com
* **Numero di task disponibili:** 1.136
* **Data di creazione:** Luglio 2013
* **Github stars:** ★ 10.657
##### Broccoli

* **Homepage:** https://github.com/broccolijs/broccoli
* **Numero di task disponibili:** ~200
* **Data di creazione:** Maggio 2013
* **Github stars:** ★ 1.787

Come possiamo notare dalle stats, Grunt è il primo task runner "di seconda generazione" arrivato e proprio per questo gode del database di plugin e task più ampio. Gulp, d’altra parte, è il sistema più seguito e supportato in questo momento. Broccoli è stato menzionato in quanto relativamente popolare, ma, sebbene sia nato in parallelo a Gulp, continua a non brillare: il numero di task disponibili è di un ordine di grandezza inferiore ai due competitor, dunque è stato scartato dalla competizione per troppo divario.

Torniamo quindi ai sopravvissuti, Grunt e Gulp. I due si distinguono pesantemente sia nella filosofia che nella modalità di scrittura dei task.

Il motto di Grunt è "configuration over code"... ecco come si presenta un classico Gruntfile.js (per capirci, l'equivalente del Rakefile nel mondo Ruby) nel caso in cui si voglia prima concatenare, e in seguito comprimere una serie di file Javascript:

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

  grunt.initConfig({
    concat: {
      scripts: {
        src: ['src/**/*.js'],
        dest: 'temp/all.js'
      }
    },
    uglify: {
      scripts: {
        src: 'temp/all.js',
        dest: 'build/all.js'
      }
    },
  });

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

Come si può notare, abbiamo due task: il primo concatena i file sorgenti e li scrive in un file temporaneo; il secondo prende il file temporaneo, lo comprime e salva il risultato nella destinazione finale.

Già da questo esempio banale, ci possiamo rendere conto di come in realtà non sia stata scritta una singola riga di codice per il setup dei task: tutto ciò che è stato fatto è fornire i parametri necessari alla configurazione dei due task sotto forma di hash al metodo grunt.initConfig().

Confrontiamolo con l'equivalente gulpfile.js:

// gulpfile.js
var gulp   = require('gulp');
var uglify = require('gulp-uglify');
var concat = require('gulp-concat');

gulp.task('default', function() {
  return gulp
         .src('src/**/*.js')
         .pipe(concat('all.js'))
         .pipe(uglify())
         .pipe(gulp.dest('build/'));
});

Per quanto riduttivo sia l'esempio, si possono già notare alcune differenze fondamentali nei due approcci:

  • Gulp sfrutta a suo vantaggio una delle forze di Node.js: gli stream. A differenza di Grunt, invece che generare file temporanei ad ogni step intermedio, i vari processamenti ai file vengono messi in pipe gli uni agli altri. Ci sono ovvi miglioramenti in termini di performance, ma il punto più importante forse è quello che non dobbiamo più preoccuparci di dare un nome a questi file temporanei, ne' di cancellarli una volta terminato il task. Un problema in meno che semplifica di molto la comprensione del codice.
  • I task Gulp non si configurano, si programmano. Importiamo i plugin Gulp con delle idiomatiche require() Node.js, ed il "corpo" del task viene espresso con codice Javascript. A differenza di Grunt, se volessimo aggiungere un if all'interno del task per decidere a runtime se aggiungere un determinato task in pipe o meno, saremmo liberissimi di farlo. Vogliamo riutilizzare parte della pipeline in un secondo task? Andiamo a rifattorizzare il codice mediante method extraction.
  • Utilizzando Grunt, all'aumentare dei task diventa estremamente complicato seguire la logica di concatenazione dei vari sotto-task. È una sfortunatissima conseguenza della scelta di Grunt di esprimere l'hash di configurazione raggruppando per plugin, piuttosto che per task. Questa scelta stravolge completamente il principio di località, costringendoci a dover saltare continuamente da una parte all'altra del file per ricostruire un task nella sua interezza.

La scelta

Leggendo l'analisi probabilmente la nostra scelta risulterà evidente: Gulp [^bemo].

[^bemo]: BEMO, il nostro frontend project-starter include ahimè un plugin Grunt.. errori di gioventù :) Switcheremo a breve verso un equivalente plugin per Gulp, nel frattempo è possibile usare gulp-grunt per interfacciare i due sistemi.

Abbiamo utilizzato entrambi i task runner su differenti progetti, e sebbene Grunt sia risultato estremamente solido, ha portato su progetti di media complessità ad hash di configurazione di più di 500 righe, praticamente impossibili da maneggiare considerato il numero di file temporanei da generare negli step intermedi ed i salti di contesto ai quali costringeva.

Esistono, ed abbiamo utilizzato con profitto, plugin Grunt come load-grunt-configs che permettono quantomeno di migliorare la situazione, splittando le configurazioni su più file... ma rimane comunque invariato ed irrisolto il raggruppamento per plugin tipico di Grunt.

Gulp, a parità di task configurati, ci ha permesso di ridurre drammaticamente la lunghezza e la complessità del codice, e dunque il suo mantenimento nel tempo.

Coming next: module loaders e package managers :)

Con i task runner in realtà abbiamo solo grattato lo strato più periferico riguardante il magico mondo delle single-page applications... la prossima settimana ci intrufoleremo nei meandri più interni con una carrellata dei principali meccanismi di module loading (AMD, CommonJS), e relative librerie e package managers (Npm, Bower).

Seguici su Twitter o tramite feed RSS per rimanere aggiornato sulla prossima uscita!