Gestire moduli javascript con Browserify

Quando ci troviamo di fronte alla necessità di dover processare files javascript in modo da poterne minificare e concatenare il contenuto abbiamo un ampio set di tools e pattern a nostra disposizione per ottenere il risultato desiderato. Per progetti poco complessi, dove magari le dipendenze si limitano a jQuery e qualche suo plugin, si possono semplicemente concatenare gli scripts manualmente, badando però di farlo seguendo il corretto ordinamento richiesto, per esempio creando un task con Gulp:

gulp.task('scripts', function() {  
    gulp.src([
        'scripts/vendor/jquery/jquery.js',
        'scripts/vendor/jquery-plugin-alfa/jquery-plugin-alfa.js',
        'scripts/vendor/jquery-plugin-beta/jquery-plugin-beta.js',
        'scripts/my-script.js'
    ])
    .pipe(concat('all.min.js'))
    .pipe(uglify())
    .pipe(gulp.dest('build/scripts'));
});

Questa soluzione è perfettamente valida ma tende a diventare tediosa e prona ad errori durante lo sviluppo. Cosa succederebbe per esempio se il plugin jquery alfa dipendesse anche da un altra liberia per alcune sue funzionalità? E se volessimo sostituire il plugin jquery beta con un'altro con caratteristiche simili ma che richiede altre dipendenze? Non andrebbe quindi modificato solo il codice dello script in questione, ma anche quello del task che si occupa di processarlo per la pubblicazione.

Un altro problema potrebbe essere quello di riuscire ad organizzare razionalmente il nostro codebase. Javascript non è provvisto di un sistema di classi o di librerie per gestire l'autoloading facilmente accessibili (come per esempio Composer per PHP), quindi ci si potrebbe trovare un po' smarriti inizialmente nel tentativo di modularizzare il codice senza star ad impazziare tra context, variabili globali etc.

Modularizzazione in Javascript

Javascript introdurrà il supporto integrato ai moduli solo con la sua prossima versione (ES6). Negli anni passati sono emersi però due standard principali per sopperire a questa mancanza, AMD (Asynchronous Module Definition) e CommonJS.

CommonJS è lo standard implementato da NodeJS, mentre AMD ha trovato utilizzo più ampio nei browser, anche perchè permette il caricamento asincrono delle dipendenze.

Le sintassi di questi due standard non sono però compatibili, quindi ci troviamo di fronte ad una scelta, e il mio consiglio a riguardo è il seguente: se vi trovate a dover modularizzare un'applicazione molto complessa, che potrebbe beneficiare del caricamento asincrono di alcune sue parti, allora la scelta migliore sono i moduli AMD, (magari utilizzando la libreria RequireJS), altrimenti optate per lo standard CommonJS che ha una sintassi più semplice e richiede meno tempo per impostare una corretta configurazione.

Primo contatto con Browserify

Tra le varie librerie disponibili che adottano lo standard CommonJS la più utilizzata è sicuramente Browserify.

In parole povere Browserify si occupa di creare un "pacchetto" (bundle) a partire da un singolo file (entry point), includendo (anche ricorsivamente) tutte le dipendenze richieste.

La sintassi da utilizzare per richiedere un modulo (o dipendenza) all'interno di un altro script è molto semplice e vi risulterà familiare se avete già lavorato con librerie che si appoggiano a NodeJS:

// main.js
var greet = require('./greeter.js');

greet.hello('John');  

Invece per definire cosa esportare, o meglio, cosa rendere disponibile di un determinato modulo a chi lo richiede come dipendenza, utilizziamo la parola chiave module.exports:

// greeter.js
var greeter = {  
    hello: function(name) {
        console.log('Hello ' + name);
    },
    bye: function(name) {
        console.log('Goodbye, ' + name);
    }
};

module.exports = greeter;  

Ora possiamo richiedere il modulo "greeter.js" all'interno di altri moduli e assegnarlo ad una variabile locale, che equivarrà alla funzione (o all'oggetto) esportarta.

Prima di poter utilizzare Browserify è necessario installarlo globalmente tramite NPM:

npm install -g browserify  

Ora possiamo eseguire Browserify da riga di comando:

browserify main.js > bundle.js  

Lanciato questo comando, Browserify analizzerà l'entry point specificato (main.js) in cerca di tutte le chiamate di richiesta a moduli esterni (require()) e si occuperà di creare un file, bundle.js, che conterrà al suo interno tutte le dipendenze richieste per il funzionamento del nostro script.

Utilizzare i transformers

Uno dei punti di forza di Browserify è il fatto di poter usare dei "Transformer" per elaborare in fase di compilazione i files sorgenti senza bisogno di passaggi intermedi. Per esempio è possibile richiedere dei moduli scritti in Coffeescript che saranno compilati in Javascript al volo, oppure richiedere template HTML scritti nel vostro linguaggio di templating preferito e precompilarli in modo che possano essere inclusi direttamente nel nostro pacchetto e così via.

Ad esempio supponiamo di voler scrivere il nostro modulo greeter in Coffeescript:

// greeter.coffee
greeter =  
    hello: (name) ->
        console.log "Hello Coffe-#{name}"
    bye: (name) ->
        console.log "Goodbye, Coffee-#{name}"

module.exports = greeter  

Ora possiamo modificare il nostro main.js per richiedere la nuova versione del nostro greeter al posto della vecchia scritta in javascript:

// main.js
var greet = require('./greeter.coffee');

greet.hello('John'); // Hello Coffee-John  

Installiamo il transformer apposito, coffeeify, come dipendenza del nostro progetto:

npm install coffeeify --save-dev  

Ora per creare il nostro pacchetto dovremo chiamare browserify nel modo seguente, indicando dopo il flag -t quali transformers vogliamo utilizzare, e il gioco è fatto:

browserify -t coffeeify main.js > bundle.js  

Automatizzare il processo

Non è però molto comodo dover lanciare manualmente il comando da shell ogni volta che si effettuano delle modifiche ad uno dei files che compongono il nostro pacchetto. E' possibile automatizzare il processo in vari modi, sia sfruttando plugin appositi come Watchify, sia integrando Browerify all'interno del nostro task runner preferito, come ad esempio Grunt o Gulp.

L'integrazione con Gulp non è delle più semplici dato che il plugin omonimo, Gulp-Browserify, è stato messo in "black-list" in quanto non aderisce alle linee guida suggerite dai creatori di Gulp.

Fortunatamente esistono delle "ricette" più o meno ufficiali per celebrare in maniera corretta il matrimonio tra questi due tools. Seguendole è possibile creare un task per automatizzare la creazione del nostro pacchetto bundle.js:

var browserify = require('browserify');  
var watchify = require('watchify');  
var coffeeify = require('coffeeify');  
var gulp = require('gulp');  
var source = require('vinyl-source-stream');  
var buffer = require('vinyl-buffer');  
var uglify = require('gulp-uglify');  
var gutil = require('gulp-util');

gulp.task('browserify', function() {

    var bundler = browserify({
        // Richiesti da Watchify, ottenibili con watchify.args
        cache: {}, packageCache: {}, fullPaths: true,
        // Array dei files che rappresentano i nostri entry-points
        entries: ['./main.js'],
        // Necessario solo dobbiamo utilizzare Sourcemaps
        debug: true
    });

    // Funziona di notifica successo
    var notifySuccess = function() {
        gutil.log('Finished', gutil.colors.green("'bundle'"));
    };

    // Funzione di notifica errori
    var notifyError = function() {
        gutil.log(gutil.colors.magenta("Browserify Error!"));
    };

    var bundle = function() {
        return bundler
          // Eseguo browserify con i parametri impostati
          .bundle()
          // Notifico eventuali errori durante la creazione del bundle
          .on('error', notifyError)
          // Rendo lo stream risultante compatibile con gulp
          // e specifico il nome del file desiderato
          .pipe(source('bundle.js'))
          .pipe(buffer())
          // Minifico il file con Uglify
          .pipe(uglify())
          // Lo salvo nella directory selezionata
          .pipe(gulp.dest('./'))
          // Notifico l'avvenuta creazione del bundle
          .on('end', notifySuccess);
    };

    // Wrappo il bundler con Watchify
    var watcher = watchify(bundler);
    // Imposto i transformer necessari
    watcher.transform('coffeeify');
    // Rieseguo la funzione "bundle" ogni volta che
    // watchify notifica un aggiornamento di uno dei file
    // coinvolti nella creazione del bundle
    watcher.on('update', bundle);

    return bundle();
});

Questo è un task particolarmente complesso, pertanto l'ho commentato abbondantemente sperando di renderlo più chiaro. Comunque lanciando da shell il comando

gulp browserify  

verrà creato il nostro pacchetto bundle.js, e watchify si occuperà di tenere d'occhio i files che partecipano alla sua formazione per rigenerarlo, se necessario, in maniera più veloce però grazie all'utilizzo di un sistema di cache.

Tirando le somme

Questo articolo riporta solo una breve panoramica delle possibilità che ci apre l'utilizzo di Browserify. Per esempio, per un utilizzo più avanzato, è possibile addirittura importare alcuni noduli base di NodeJS, come Crypto, Url o Path, nel browser! Informazioni più dettagliate sulle funzionalità di Browserify sono disponibili sulla guida ufficiale.

Emanuele Ingrosso

Read more posts by this author.

Subscribe to Ingruz's Blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!
comments powered by Disqus