Negli ultimi anni l'importanza di ottimizzare le risorse frontend (JavaScript, CSS e immagini) ha raggiunto una grande visibilità, soprattutto alla luce della tendenza diffusa a separare le logiche backend (implementate come Backend as a Service) da quelle di frontend (Web Application).
Con il crescere delle dimensioni dei file di progetto e senza poter contare sui sistemi di ottimizzazione lato server come le pipeline o gli asset manager offerti dai principali sistemi lato server, è diventato critico usare tool di build che possano permetterci di sviluppare e debuggare i sorgenti in sviluppo per poi servirne una versione ottimizzata in produzione.
Grunt è ad oggi uno dei tool di build per il frontend più accreditati e fornisce tutti gli strumenti per realizzare un workflow efficiente per la realizzazione di web applications.
In questo articolo vedremo come configurare Grunt per ottenere una struttura di base per il build dei nostri progetti.
In particolare ci focalizzeremo su:
- concatenazione e minificazione di JavaScript
- minificazione CSS
- ottimizzazione delle immagini
Preparazione dell'ambiente
Rispetto alle vecchie versioni, la struttura di Grunt è stata ulteriormente modularizzata, rendendolo più leggero ed offrendo più libertà di scelta per quanto riguarda le funzionalità da implementare. Inoltre il tool viene installato per project permettendo di utilizzarne diverse versioni o comunque garantendo la funzionalità su vecchi progetti.
Una volta installato Nodejs dobbiamo installare a livello di sistema il tool di Grunt da riga di comando:
npm install -g grunt-cli
Il secondo passo è quello di creare, se non già presente, un file package.json
nella radice del progetto che verrà usato per specificare i dati del progetto stesso (nome, autori, versione) nonché la lista dei plugin di Grunt da utlizzare in fase di build:
cd mio-progetto
npm init
Dopo aver risposto alle domande del sistema, il vostro file package.json
dovrebbe essere simile a questo:
{
name: "mio-progetto",
version: "0.0.1",
description: "Il mio progetto",
author: "Marco Solazzi",
license: "MIT"
}
A questo punto installiamo la libreria principale di Grunt.
npm install grunt --save-dev
Da notare che, per questo e tutti i plugin che aggiungeremo, utilizzeremo il flag --save-dev
, in modo da salvare un riferimento alla versione installata nel file package.json
. Potremo così evitare di dover condividere con gli altri sviluppatori la cartella node_modules
con le dipendenze del progetto perché gli basterà lanciare npm install
(senza altri argomenti) per scaricare tutti i pacchetti necessari.
Struttura del progetto
Nell'immagine seguente viene mostrata l'alberatura del nostro progetto.
In questa struttura di esempio modificheremo solo i file nella cartella src
, mentre la cartella dist
verrà popolata con il risultato del processo di build.
Gruntfile di base
I file di configurazione di Grunt sono denominati Gruntfile e si trovano solitamente nella radice del progetto.
Per questo tutorial andremo a creare un file Gruntfile.js
(attenzione alla prima lettera maiuscola) con questo contenuto:
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json')
});
};
La prima riga module.exports
rivela come un Gruntfile sia un modulo nodejs, che viene richiamato da Grunt in fase di inizializzazione.
Fra le funzionalità aggiuntive di Grunt c'è il metodo grunt.file.readJSON
che permette di leggere file JSON e ne restituisce il contenuto come oggetto JavaScript. Nel nostro caso andremo ad associare il contenuto di package.json
alla proprietà pkg
. Vedremo più avanti come potremo utilizzarla.
Subito dopo pkg
andremo a configurare i nostri task. In molti casi è possibile anche definire dei sotto task o target per diversificare le opzioni ed i file su cui lavorare. Generalmente la stintassi utilizzata è la seguente:
nome_task: {
nome_target: {
options: { /* opzioni del target */ },
files: {
src: [/* lista dei file sorgente */],
dest: /* file di destinazione */
}
}
}
Tutti i percorsi dei file sono relativi alla posizione del Gruntfile. In realtà esistono varie opzioni per definire la lista dei file da usare ed è anche possibile impostare una proprietà options
direttamente sul task con le opzioni comuni a tutti i target.
Ora siamo pronti ad esaminare il Workflow che ci permetterà di automatizzare l'ottimizzazione del codice delle nostre pagine
Configurazione del workflow
A parte la libreria principale, tutti i plugin installati andranno caricati nel Gruntfile. Per fare ciò, utilizzeremo il metodo grunt.loadNpmTasks
appena dopo la chiusura del metodo grunt.initConfig
:
grunt.loadNpmTasks('nome-plugin');
grunt.loadNpmTasks('nome-altro-plugin');
Un po' di pulizia
Anzitutto, per evitare di lasciare file non più utilizzati nella cartella di build (ad esempio immagini che abbiamo rimosso dal progetto), la prima operazione da fare è assicurarsi di rimuovere la cartella dist
che verrà così ricreata da zero dai task successivi. Per tale scopo utilizzeremo il plugin grunt-contrib-clean:
npm install --save-dev grunt-contrib-clean
Aggiungendo il task nell'oggetto di configurazione dopo la proprietà pkg
:
clean: {
dist: ['dist']
}
Concatenare e Minificare i JavaScript
Per ottimizzare i file JavaScript utilizzeremo due plugin grunt-contrib-concat (per ottenere un file unico) e grunt-contrib-uglify (per minificarlo con uglify2).
npm install --save-dev grunt-contrib-concat grunt-contrib-uglify
i due task saranno configurati in questo modo:
concat: {
dist: {
files: {
src: ['src/javascripts/*.js'],
dest: 'dist/javascripts/main.js'
}
}
},
uglify: {
dist: {
options: {
banner: '/*! <%= pkg.name %> - <%= pkg.version %> - <%= pkg.author %> */n'
},
files: {
src: ['<%= concat.dist.dest %>'],
dest: 'dist/javascripts/main.min.js'
}
}
}
Come avrete notato il file sorgente del target uglify.dist
è in realtà un riferimento alla configurazione di concat.dist
. Questa sintassi erb-like è generalmente supportata da tutti i plugin e può essere molto utile per realizzare riferimenti incrociati fra i task ed evitare ripetizioni nel file di configurazione. Oltre a cià abbiamo utilizzato i valori di pkg
per aggiungere un banner, cioè un'intestazione, al file minificato. In questo modo i dati saranno sempre in linea con il progetto.
Ottimizzare i CSS
La minificazione dei CSS prevede due passaggi: la rimozione degli spazi e la compressione del codice utilizzando, ove possibile, le notazioni shorthand (un po' come fanno i minificatori JavaScript). Per questo task ci appoggeremo a grunt-contrib-cssmin, un wrapper per clean-css:
npm install --save-dev grunt-contrib-cssmin
La configurazione sarà:
cssmin: {
dist: {
files: {
src: ['src/stylesheets/*.css'],
dest: 'dist/stylesheets/application.min.css'
}
}
}
Preprocessori CSS
Al giorno d'oggi un workflow di sviluppo moderno non può più prescindere dall'uso di preprocessori CSS. Grunt offre plugin per Sass e Compass, ma nel caso non vogliate aggiungere ulteriori dipendenze potete usare Less e compilarlo con grunt-contrib-less:
less
dist: {
options: {
cleancss: true
},
files: {
src: ['src/stylesheets/*.less'],
dest: 'dist/stylesheets/application.min.css'
}
}
}
L'opzione cleancss
produce un CSS già minificato con clean-css, quindi non avrete bisogno del task cssmin
.
Compressione delle immagini
Una delle best practices nel campo delle performance di un sito web è ottimizzare le immagini.
Con il supporto a features CSS3 come gradienti e bordi arrotondati il numero di immagini utilizzate per comporre l'interfaccia grafica di un sito è diminuito drasticamente, tuttavia in alcuni casi dobbiamo ancora farvi ricorso.
Una prima parte del lavoro di ottimizzazione può essere fatta quando le ritagliamo dal file sorgente, andando a scegliere il formato migliore e il livello di compressione, ma possiamo guadagnare ancora qualcosa utilizzando tool come OptiPNG e jpegtran che vanno a limare byte dalle immagini eliminando dati inutili ed ottimizzandone il livello di compressione.
Esistono vari plugin per automatizzare queste operazioni, quello supportato direttamente dal team di Grunt è grunt-contrib-imagemin, che funge da wrapper per jpegtran, gifsicle, OptiPNG e pngquant:
npm install --save-dev grunt-contrib-imagemin
La configurazione prevede varie opzioni per i singoli tool, comunque non saranno necessari target diversi per ogni tipo di immagine poiché sarà il plugin a determinare come comportarsi in base all'estensione dei file:
imagemin: {
dist: {
files: [{
expand: true, // mappatura dinamica dei file
cwd: 'src/', // i percorsi di src sono relativi a questa cartella
src: ['**/*.{png,jpg,gif}'], // file sorgenti
dest: 'dist/' // cartella di destinazione
}]
}
}
In questo caso abbiamo utilizzato un particolare formato dell'oggetto files
, che permette di realizzare una mappatura dinamica dei file uno a uno poiché, diversamente dagli altri task, non abbiamo un unico file di destinazione.
Lanciare i task
Una volta conclusa la configurazione possiamo eseguire Grunt nella directory in cui si trova il Gruntfile. Il formato del comando è:
grunt task[:target]
Quindi per eseguire l'ottimizzazione delle immagini eseguiremo:
grunt imagemin:dist
Omettendo il target, Grunt eseguirà tutti quelli configurati sul task. Possiamo anche omettere il nome del task, in tal caso il sistema cercherà di lanciare un task denominato default
che possiamo definire con il metodo grunt.registerTask
:
grunt.registerTask('default', ['concat', 'uglify', 'cssmin', 'imagemin']);
In questo modo abbiamo creato un task che eseguirà in sequenza tutti i task definiti nel secondo parametro.
Seguendo questa logica possiamo definire altri task, per organizzare e raggruppare più task:
grunt.registerTask('dist_js', ['concat', 'uglify']);
grunt.registerTask('dist', ['dist_js', 'cssmin', 'imagemin']);
Task aggiuntivi
La configurazione mostrata in questo articolo può essere considerata come il punto di partenza da estendere ed adattare a seconda dello stack frontend utilizzato nei vostri progetti.
- Ad esempio potreste utilizzare grunt-contrib-watch e grunt-contrib-connect per abilitare un semplice server statico con funzionalità live-reload al salvataggio dei file;
- oppure ancora realizzare build ottimizzate di Modernizr con solo i test che avete effettivamente utilizzato.
Per tutte le altre necessità potete fare riferimento al repository ufficiale dei plugin di Grunt.