Organizzazione del codice

Introduzione
Quando si intraprende il compito di realizzare applicazioni complesse sul lato client, è necessario considerare come organizzare il codice. Questo capitolo è dedicato ad analizzare alcuni modelli di organizzazione del codice da utilizzare in un’applicazione realizzata con jQuery ed esplorare il sistema di gestione di dipendenze di RequireJS.
Concetti chiave
Prima di iniziare con i modelli organizzativi di codice, è importante capire alcuni concetti chiave.

  • Il codice deve essere suddiviso in unità funzionali – moduli, servizi, ecc. E devono evitare la tentazione di avere tutto in un unico blocco $(document).ready(). Questo concetto è noto come incapsulamento.
  • Non ripetere il codice. Identificare pezzi simili e utilizzare tecniche di eredità.
  • Sebbene la natura di jQuery, non tutte le applicazioni JavaScript si trovano nel DOM e non tutte le parti di funzionalità hanno la necessità di avere un rappresentanzio nel DOM.
  • Le unità di funzionalità devono avere un giunto flessibile (in inglese loosely coupled) – una unità di funzionalità dovrebbe essere in grado di esistere da sé e la comunicazione con altre unità dovrebbe avvenire attraverso un sistema di messaggi come eventi personalizzati o pub/sub. E per quanto possibile, tenere lontana la comunicazione diretta le unità funzionali.

Il concetto di giunto (o articolazione) flessibile può essere particolarmente problematico per gli sviluppatori che fanno la loro prima incursione in applicazioni complesse, quindi se siete di partenza, cercate di essere consapevoli di questo concetto.

Incapsulamento
Il primo passo per l’organizzazione del codice è di separare l’applicazione in diversi pezzi; anche se a volte, questo sforzo è sufficiente.
L'Oggetto Letterale
Un oggetto letterale è forse il modo più semplice per incapsulare il codice correlato. Questo non fornisce alcuna privacità per proprietà o metodi, ma è utile per eliminare le funzioni anonime, centralizzare le opzioni di configurazione, e facilitare la via per il riutilizzo e il rifattorizzazione.
10.1: Un oggetto letterale
var myFeature = {
myProperty : 'ciao',

myMethod : function() {
    console.log(myFeature.myProperty);
},

init : function(settings) {
    myFeature.settings = settings;
},

readSettings : function() {
    console.log(myFeature.settings);
}
};

myFeature.myProperty; // 'ciao'
myFeature.myMethod(); // registra 'ciao'
myFeature.init({ foo : 'bar' });
myFeature.readSettings(); // registra { foo : 'bar' }
L’oggetto ha una proprietà e vari metodi, i quali sono pubblici, in modo che ogni parte dell’applicazione li può vedere.

Come è possibile applicare questo schema nel codice jQuery? Per esempio, nel codice seguente scritto nello stile tradizionale di jQuery:

// Facendo clic su un item della lista 
// si carica un certo contenuto.
// Utilizzando l'ID dell'item e a sua 
// volta nasconde gli altri item fratelli.
$(document).ready(function() {
$('#myFeature li')
.append('<div/>')
.click(function() {
  var $this = $(this);
  var $div = $this.find('div');
  $div.load('foo.php?item=' +
    $this.attr('id'),
    function() {
      $div.show();
      $this.siblings()
        .find('div').hide();
    }
  );
});
});
Se l’esempio illustrato rappresenta il 100% della richiesta, si consiglia di lasciare così com’è. Tuttavia, se il pezzo è solo una parte di una applicazione più grande, sarebbe bene separare questa funzionalità dalle altre non correlate. Per esempio è conveniente spostare l’URL a cui viene fatta la richiesta al di fuori del codice e passarla all’area di configurazione. Anche spezzare la catena di metodi per rendere dopo più facile la sua modifica o manutensione.
10.2: Utilizzare un oggetto letterale per una funzionalità jQuery
var myFeature = {
init : function(settings) {
    myFeature.config = {
        $items : $('#myFeature li'),
        $container : $('<div class="container"></div>'),
        urlBase : '/foo.php?item='
    };

    // permete di sovrascrivere 
    // la configurazione predefinita
    $.extend(myFeature.config, settings);

    myFeature.setup();
},

setup : function() {
    myFeature.config.$items
        .each(myFeature.createContainer)
        .click(myFeature.showItem);
},

createContainer : function() {
    var $i = $(this),
        $c = myFeature.config.$container.clone()
                 .appendTo($i);

    $i.data('container', $c);
},

buildUrl : function() {
    return myFeature.config.urlBase +
           myFeature.$currentItem.attr('id');
},

showItem : function() {
    var myFeature.$currentItem = $(this);
    myFeature.getContent(myFeature.showContent);
},

getContent : function(callback) {
    var url = myFeature.buildUrl();
    myFeature.$currentItem
        .data('container').load(url, callback);
},

showContent : function() {
    myFeature.$currentItem
        .data('container').show();
    myFeature.hideContent();
},

hideContent : function() {
    myFeature.$currentItem.siblings()
        .each(function() {
            $(this).data('container').hide();
        });
}
};

$(document).ready(myFeature.init);
La prima caratteristica da notare è che il codice è più lungo da quello originale – come affermato in precedenza, se questo è il campo dell’applicazione, utilizzare un oggetto letterale sarebbe probabilmente una esagerazione.

I vantaggi sono:

  • Separazione di ogni funzionalità in piccoli metodi. In futuro, se si desidera cambiare la modo di visualizzare il contenuto, ci sarà chiaro dove farlo. Nel codice originale, questo passaggio è molto più difficile da individuare.
  • Si rimuovono gli usi di funzioni anonime.
  • Le opzioni di configurazione ssi sono spostate in una posizione centrale.
  • Si eliminano le limitazioni che hanno catene di metodi, rendendo il codice più facile da rifattorare, mescolare e riordinare.

Grazie alle sue caratteristiche, l’uso di oggetti letterali consente un netto miglioramento per lunghi tratti di codice inseriti in un blocco $(document).ready(). Tuttavia, no sono più avanzati che avere diverse dichiarazioni di funzioni all’interno di un blocco $(document).ready().

Il Modello Modulare
Il modello modulare supera alcune delle limitazioni dell’oggetto letterale, offrendo privacità a variabili e funzioni, esponendo a sua volta un API pubblica se lo si desidera.
10.3: Il modello modulare
var feature =(function() {

// variabili e funzioni private
var privateThing = 'segreto',
    publicThing = 'non segreto',

    changePrivateThing = function() {
        privateThing = 'super segreto';
    },

    sayPrivateThing = function() {
        console.log(privateThing);
        changePrivateThing();
    };

// API pubblica
return {
    publicThing : publicThing,
    sayPrivateThing : sayPrivateThing
}

})();

feature.publicThing; // registra 'non segreto'

feature.sayPrivateThing();
// registra 'segreto' e cambia il valore
// di privateThing a 'super segreto'
Nell’esempio, si autoesegue un funzione anonima che restituisce un oggetto. All’interno della funzione, si definiscono alcune variabili. Visto che sono definite all’interno della funzione, dall’esterno non hanno accesso a meno che non si mettano dentro l’oggetto che viene restituito. Questo significa che nessun codice al di fuori della funzione ha accesso alla variabile privateThing o alla funzione sayPrivateThing. Tuttavia, sayPrivateThing ha accesso a privateThing e changePrivateThing perche sono definiti nello stesso ambito.

Il modello è potente perché permette di avere variabili e funzioni private, esponendo una API limitata che consente la retituzione di proprietà e metodi di un oggetto.

Di seguito è riportato una revisione dell’esempio visto prima, con le stesse caratteristiche, ma esponendo un unico metodo pubblico del modulo, showItemByIndex().
10.4: Utilizzare il modello modulare per una funzionalità jQuery

$(document).ready(function() {
var feature = (function() {

    var $items = $('#myFeature li'),
        $container = $('<div class="container"></div>'),
        $currentItem,

        urlBase = '/foo.php?item=',

        createContainer = function() {
            var $i = $(this),
                $c = $container.clone().appendTo($i);

            $i.data('container', $c);
        },

        buildUrl = function() {
            return urlBase + $currentItem.attr('id');
        },

        showItem = function() {
            var $currentItem = $(this);
            getContent(showContent);
        },

        showItemByIndex = function(idx) {
            $.proxy(showItem, $items.get(idx));
        },

        getContent = function(callback) {
            $currentItem.data('container').load(buildUrl(), callback);
        },

        showContent = function() {
            $currentItem.data('container').show();
            hideContent();
        },

        hideContent = function() {
            $currentItem.siblings()
                .each(function() {
                    $(this).data('container').hide();
            });
        };

    $items
        .each(createContainer)
        .click(showItem);

    return { showItemByIndex : showItemByIndex };
})();

feature.showItemByIndex(0);
});
Gestione delle dipendenze
Questa sezione si basa sulla eccellente documentazione di RequireJS http://requirejs.org/docs/jquery.html ed è utilizzato con il permesso di James Burke, autore di RequireJS.

Quando un progetto raggiunge una certa dimensione, cominicia a diventare difficile la gestione dei moduli in un’applicazione, giacché è necessario sapere ordinarli in modo corretto, e cominciare a combinarli in un unico file per ottenere il minor numero di richieste. È anche possibile che si voglia caricare codice “al volo” dopo il caricamento della pagina.

RequireJS è uno strumento per la gestione delle dipendenze creato da James Burke, che aiuta a gestire i moduli, caricarli nel giusto ordine e combinarli con facilità, senza dover apportare alcuna modifica. A sua volta, fornisce un modo semplice per caricare il codice una volta caricata la pagina, permettendo di minimizzare il tempo di download.

RequireJS possiede un sistema modulare, ma non c’è bisogno di seguirlo per ottenere i sui benefici. Il formato modulare di RequireJS permette la scrittura del codice incapsulato, incorporazione dell’internazionalizzazione (i18n) ai pacchetti (per consentirne l’uso in diverse lingue) e anche l’uso dei servizi JSONP come dipendenze.
Ottenere RequireJS

Il modo più semplice per usare il pacchetto RequireJS con jQuery è quello di scaricare el paquete de jQuery con RequireJS già incorporato. Questo pacchetto esclude porzioni di codice che duplicano funzioni di jQuery.
È anche utile scaricare un esempio di progetto jQuery che utilizza RequireJS.
Utilizzare RequireJS con jQuery
Utilizzare RequireJS è semplice, basta inserire nella pagina la versione jQuery che ha costruito RequireJS includendo e quindi richiedere i files dell’applicazione. Di seguito, l’esempio presuppone che sia jQuery come gli altri files siano all’interno della cartella scripts/.
10.5: Usare RequireJS: un semplice esempio
<!DOCTYPE html>
<html>
<head>
    <title>jQuery+RequireJS pagina di esempio</title>
    <script src="scripts/require-jquery.js"></script>
    <script>require(["app"]);</script>
</head>
<body>
    <h1>jQuery+RequireJS pagina di esempio</h1>
</body>
</html>
La chiamata a require([“app”]) comunica a RequireJS che carichi il file scripts/app.js. RequireJS caricherà qualsiasi dipendenza passata a require() senza l’estensione .js dalla stessa cartella in cui si trova il file require-jquery.js, ma è anche possibile specificare il percorso così come segue:
<script>require(["scripts/app.js"]);</script>
Il file app.js è un’altra chiamata al file require.js per caricare tutti i file necessari per l’applicazione. Nel seguente esempio, app.js si richiedono due estensioni jquery.alpha.js e jquery.beta.js (non sono estensioni reali, solo esempi). Queste estensioni sono nella stessa cartella del file require-jquery.js:
10.6: Un semplice file JavaScript con dipendenze
require(["jquery.alpha", "jquery.beta"], function() {
// le estensioni  jquery.alpha.js y jquery.beta.js 
// sono state caricate.
$(function() {
    $('body').alpha().beta();
});
});
Creare moduli riutilizzabili con RequireJS
RequireJS rende facile definire moduli riutilizzabili attraverso require.def(). Un modulo RequireJS può avere dipendenze che possono essere utilizzati per definire un modulo così come per restituire un valore – un oggetto, una funzione o qualcos’altro – che può essere utilizzato anche su altri moduli.

Se il modulo non ha dipendenze, solo si deve specificare il il nome come primo argomento di require.def(). Il secondo argomento è un oggetto letterale che definisce le proprietà del modulo. Per esempio:
10.7: Definizione di un modulo RequireJS che non ha dipendenze

require.def("my/simpleshirt",
{
    color: "black",
    size: "unisize"
}
);
L’esempio deve essere salvato nel file my/simpleshirt.js.

Se il modulo ha dipendenze, si può specificare nel secondo argomento di require.def() attraverso un array e poi passare una funzione come terzo argomento. Questa funzione sarà chiamata per definire il modulo una volta caricate tutte le dipendenze. Questa funzione riceve i valori restituiti dalle dipendenze come argomento (nello stesso ordine in cui sono dichiarate nell’array), allora la stessa deve restituire un oggetto che definisca il modulo.
10.8: Definizione di un modulo RequireJS con dipendenze

require.def("my/shirt",
["my/cart", "my/inventory"],
function(cart, inventory) {
    // restituisce un oggetto che definisce 
    // il modulo "my/shirt".
    return {
        color: "blue",
        size: "large"
        addToCart: function() {
            inventory.decrement(this);
            cart.add(this);
        }
    }
}
);
In questo esempio, il modulo my/shirt viene creato. Questo dipende da my/cart e my/inventory. Sul disco, i files sono strutturati come segue:
my/cart.js
my/inventory.js
my/shirt.js
La funzione che definisce my/shirt non viene chiamata fino a che my/cart e my/inventory siano stati caricati, e questa funzione riceve come argomenti i moduli cart e inventory. L’ordine degli argomenti della funzione deve corrispondere all’ordine in cui le dipendenze si richiedono nell’array. L’oggetto restituito definisce il modulo my/shirt. Definendo i moduli in questo modo, my/shirt non esiste come un oggetto globale, giacché più moduli possono esistere nella pagina allo stesso tempo.

I moduli non devono per forza restituire un oggetto, qualsiasi tipo di valore è permesso.
10.9: Definizione di un modulo RequireJS che restituisce una funzione

require.def("my/title",
["my/dependency1", "my/dependency2"],
function(dep1, dep2) {
    // restituisce una funzione per definire "my/title". 
    // Questo restituisce o imposta 
    // il titolo della finestra
    return function(title) {
        return title ? (window.title = title) : window.title;
    }
}
);
Un solo modulo dovrebbe essere richiesto per il file JavaScript.
Ottimizzare il codice con gli strumenti di RequireJS
Una volta incorporato RequireJS per la gestione delle dipendenze, ottimizzazione del codice è semplice. Scarica il pacchetto RequireJS e salvarlo, preferibilmente al di fuori dell’area di sviluppo web. Ai fini del presente esempio, il pacchetto RequireJS si trova in una cartella parallela alla cartella webapp (che contiene la pagina HTML e tutti i file JavaScript dell’applicazione). La struttura della cartella è la seguente:
requirejs/ (utilizzayo per eseguire gli strumenti)
webapp/app.html
webapp/scripts/app.js
webapp/scripts/require-jquery.js
webapp/scripts/jquery.alpha.js
webapp/scripts/jquery.beta.js
Poi, nella cartella degli script che ha require-jquery.js e app.js creare un file chiamato app.build.js o con il seguente contenuto:
10.10: File di configurazione per gli strumenti di ottimizzazione di RequireJS
{
appDir: "../",
baseUrl: "scripts/",
dir: "../../webapp-build",
// Commentare la riga seguente se volete 
// minimizzare il codice dal compilatore 
// in modalità "semplice"
optimize: "none",

modules: [
    {
        name: "app"
    }
]
}
Per utilizzare lo strumento, è necessario installare Java 6. Closure Compiler viene utilizzato per minimizzare il codice (nel caso optimize: “none” non sia commentato).

Per avviare l’elaborazione dei files, aprire una finestra di comando, andare alla cartella webapp/scripts ed eseguire:

# per sistemi non-Windows
../../requirejs/build/build.sh app.build.js

# per sistemi Windows
..\..\requirejs\build\build.bat app.build.js
Una volta eseguito, il file app.js nella cartella webapp-build conterrà tutto il codice di app.js più quello di jquery.alpha.js e jquery.beta.js. Se viene aperto il file app.html (anche nella cartella webapp-build) si noterà che nessuna richiesta si realizza per caricare jquery.alpha.js e jquery.beta.js.
Esercizi
Creare un modulo Portlet
Aprire il file /exercises/portlets.html nel browser web. Eseguire l’esercizio utilizzando il file /exercises/js/portlets.js. L’esercizio è quello di creare una funzione che crea portlet che utilizza il modello modulare, in modo che il codice che segue funzioni:
var myPortlet = Portlet({
title : 'Curry',
source : 'data/html/curry.html',
initialState : 'open' // or 'closed'
});

myPortlet.$element.appendTo('body');
Ogni portlet deve essere un div con un titolo, uno spazio per il contenuto, un pulsante per aprire/chiudere il portlet, un pulsante per rimuoverlo e un altro per aggiornarlo. Il portlet restituito dalla funzione deve avere la seguente API pubblicha:
myPortlet.open();    // forza per aprire
myPortlet.close();   // forza per chiudere
myPortlet.toggle();  // alterna gli stati 
                     // tra aperto e chiuso
myPortlet.refresh(); // aggiorna il contenuto
myPortlet.destroy(); // rimuove il porlet dalla pagina
myPortlet.setSource('data/html/onions.html');
                     // cambia il codice