In questo articolo voglio approfondire alcuni accorgimenti utili a migliorare le prestazioni, l’utilizzo e l’organizzazione di un plugin jQuery.
Qualunque tipo di funzione/effetto e/o trasformazione del DOM di una pagina web può essere incorporata in un plugin jQuery con lo scopo di migliorare l’organizzazione del proprio codice Javascript.
Invece di un’accozzaglia di istruzioni generiche avremo il riferimento ad un set ordinato di plugins.
http://jsbin.com/apojah/14/edit/
Wrapper jQuery:
Il primo accorgimento utile è incorporare il codice del plugin in un oggetto wrapper il cui scopo è duplice:
- isolare il codice del plugin dallo spazio degli script della pagina
- utilizzare $ come riferimento interno a jQuery in modo sicuro
0 1 2 | ;(function($){ ... qui va il codice del plugin ... })(jQuery); |
Tutte le variabili dichiarate mediante la sintassi “var” saranno locali al plugin mentre rimane possibile accedere a variabili globali dichiarate esternamente.
Passare la variabile “jQuery” al wrapper rende possibile l’utilizzo della funzione $ tipica di jQuery all’interno del codice del nostro plugin anche in presenza di altri framework esterni (es MooTools).
Struttura base del plugin:
Un plugin è un’estensione di jQuery che agisce su di un contesto formato da un elenco di nodi DOM identificati da una query xPath.
$('p').hide();
La precedente istruzione identifica un elenco di nodi DOM composto da tutti i paragrafi (p) presenti nel documento HTML e passa tale contesto alla funzione hide() di jQuery. Tale funzione restituisce il contesto stesso rendendo possibile la concatenazione di più plugins:
$('p').hide().fadeIn();
Per questo motivo il codice base del nostro plugin sarà:
0 1 2 3 4 5 6 7 8 | $.fn.customPlugin = function() { $(this).each(function(){ ... azioni sul singolo elemento DOM ... }); return this; }; |
Viene eseguito un ciclo applicando una funzione ad ogni elemento del contesto. Successivamente il plugin restituisce il contesto stesso per consentire la concatenazione dei plugins come accennato sopra.
Ottimizziamo il ciclo sul contesto:
E’ importante capire che in Javascript ogni function() è trattata come un oggetto.
Il codice di questo esempio, seppur molto diffuso, crea un nuovo oggetto per ogni elemento di contesto quindi incide significativamente sulla memoria e sulla prestazioni del plugin stesso.
Possiamo ottimizzare il tutto spostando la logica applicata al singolo elemento in una funzione esterna locale al wrapper del plugin:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | ;(function($){ /** * Metodo locale utilizzato dal plugin jQuery sugli elementi DOM * */ var __loop = function() { $(this).fadeOut().fadeIn(); }; /** * Plugin - Estensione jQuery */ $.fn.customPlugin = function() { // Applico la logica agli elementi del contesto (DOM) $(this).each(__loop); // Mantengo la possibilità di concatenare plugins. return this; }; // EndOf: "customPlugin()" ### })(jQuery); |
In riga #19 applichiamo la funzione __fn() che è definita esterna al plugin ma localmente al wrapper creato. In questo modo Javascript utilizzerà sempre il medesimo oggetto passando di volta in volta dati differenti.
Questo concetto è estremamente importante in quanto con jQuery è utilizzo comune passare nuove funzioni come parametri di metodi applicati ad elementi DOM. Aver cura di definire tali funzioni (solitamente callback) in modo esplicito nel wrapper del plugin è fondamentale per migliorare le prestazioni del nostro codice.
Configurazione del Plugin:
Praticamente il 100% dei plugins jQuery utilizzano il primo parametro passato come oggetto di configurazione interna.
0 1 2 3 | $('p').customPlugin({ speed:'fast', color:'red' }); |
Una configurazione classica consiste in un oggetto le cui proprietà sono utilizzate dalla logica interna al plugin stesso per personalizzare le funzionalità erogate secondo diverse esigenze.
L’utilizzo di un oggetto di configurazione fa nascere alcune problematiche di organizzazione del codice quali:
- necessità di applicare dei valori di default per opzioni non rese esplicite
- necessità di passare l’oggetto di configurazione alla funzione __loop()
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | $.fn.customPlugin = function() { // Proprietà di configurazione interna. var cfg = false; // Estendo la proprietà di configurazione con l'oggetto di // configurazione contenuto nel primo parametro del plugin. if ( !arguments.length || $.isPlainObject(arguments[0]) ) { cfg = $.extend({},{ color: 'black' },arguments[0]); } // Applico la logica agli elementi del contesto (DOM) $(this).each(function(){ __loop.call( this, cfg ); }); // Mantengo la possibilità di concatenare plugins. return this; }; |
Come prima cosa si noti l’aggiunta di una proprietà locale al plugin cfg la quale è instanziata a false e successivamente estesa da un oggetto contenente la configurazione di default del plugin stesso. (riga #3 -> #13)
La configurazione è contenuta nel primo parametro tra quelli passati al plugin, inoltre il valore di cfg viene modificato solamente se tale parametro è un oggetto. Questo condizionale è la base di una semplice tecnica per utilizzare il medesimo plugin con tipi di input differenti come avviene in quasi tutti i widget di jQuery UI.
$('#foo').dialog({ .. config .. }).dialog('open');
Utilizziamo le Closures:
Anche la riga #16 è cambiata: abbiamo dovuto aggiungere una closure per consentire il passaggio della configurazione alla funzione locale __loop().
L’utilizzo della closure è un compromesso di ottimizzazione.
Cerchiamo di isolare tutta la logica possibile in una funzione esterna che verrà invocata con parametri diversi dal codice interno alla closure. La closure verrà invece creata per ogni elemento DOM del contesto causando un leggero spreco di memoria.
Mantenere essenziale il codice delle closure demandando tutta la logica a funzioni esterne è importantissimo per garantire buone performance al nostro codice!
Rispondere a diversi tipi di input:
Abbiamo accennato a jQueryUI ed a come metta a disposizione una serie di widget la cui caratteristica comune è erogare comportamenti diversi in base a tipi di chiamate diverse:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | (function($){ /** * Logic methods */ var __loop = function( cfg ) { $(this).data( 'customPlugin', cfg ); }; var __colorize = function() { var cfg = $(this).data('customPlugin'); $(this).css( 'background-color', cfg.color ); }; /** * Plugin - Estensione jQuery */ $.fn.customPlugin = function() { // Proprietà di configurazione interna. var cfg = false; // Estendo la proprietà di configurazione con l'oggetto di // configurazione contenuto nel primo parametro del plugin. if ( !arguments.length || $.isPlainObject(arguments[0]) ) { cfg = $.extend({},{ color: 'black' },arguments[0]); } // Inizializzazione Plugin: // Applico la logica agli elementi del contesto (DOM) if ( cfg !== false ) { $(this).each(function(){ __loop.call( this, cfg ); }); // Azioni su plugin intanziato: } else if ( arguments.length ) { switch ( arguments[0] ) { case 'colorize': $(this).each(function(){ __colorize.call( this ); }); break; } } // Mantengo la possibilità di concatenare plugins. return this; }; // EndOf: "customPlugin()" ### })(jQuery); /** * Codice eseguito al caricamento della pagina */ $(document).ready(function(){ // Inizializzazione del plugin $('p').customPlugin({ color:'red' }); // Esempio di utilizzo delle azioni del plugin $('input').click(function(){ $('p').customPlugin('colorize'); }); }); |
In questo esempio applichiamo il nostro plugin a tutti paragrafi (p) del documento (riga #69). Questa operazione andrà ad attivare il metodo locale __loop() la cui logica è salvare la configurazione del plugin all’interno del DOM del nodo interessato.
Successivamente sfruttiamo il click su di un bottone per invocare il metodo “colorize” del plugin stesso. In questo caso entra in scena il metodo local __colorize(). Questo è reso possibile dal controllo sul tipo di dato effettuato sul primo parametro passato al plugin. Rispettivamente in riga #30 e poi in riga #46.
Seguendo la struttura esposta nell’articolo il codice del plugin è diviso in due macro sezioni: un “router” di azioni ed un certo numero di metodi interni.
Il router è gestito nella funzione che estende jQuery. Controlla il tipo di input e si occupa di stabile che tipo di azione è richiesta.
I metodi interni si occupano di dar seguito alla decisione del router e devono “fare la magia”. Solitamente i metodi interni sono invocati per ogni elemento DOM identificato dal contesto del plugin come nei precedenti esempi.
Il passo successivo è massimizzare l’ottimizzazione dei metodi interni!
Ottimizzare con le Variabili Cache:
Ogni metodo locale di logica riceve come contesto il riferimento ad un preciso nodo DOM facente parte dell’xPath su cui è stato lanciato il plugin. Nel nostro esempio significa che il codice $(this) all’interno dei metodi __loop() o __colorize() è il riferimento ad un singolo paragrafo.
Sempre all’interno di questi metodi potremo aver bisogno di effettuare nuove ricerche su nodi:
$(this).find('strong')
a cui applicare vari metodi jQuery oppure altri plugin a cascata.
Ogni volta che si utilizza jQuery per effettuare una ricerca viene impegnata memoria e CPU. Noi possiamo agire e salvare i risultati di una query in una variabile locale così da alleggerire il carico del computer client. Vediamo il codice di __colorize():
0 1 2 3 4 5 6 7 8 | var __colorize = function() { var $this = $(this); var cfg = $this.data('customPlugin'); $this.css( 'background-color', cfg.color ); }; |
In riga #2 viene creata la variabile locale $this con riferimento al nodo DOM.
Nelle righe #4 e #6 riutilizziamo questa variabile per applicare metodi jQuery.
Questo è ovviamente un esempio banale ma possiamo immaginare l’impatto positivo di questo accorgimento quando la logica comincia a complicarsi un po’!
Conclusioni:
In questo articolo abbiamo raccolto le basi per la costruzione di un plugin jQuery versatile e prestazionale.
Grazie a questa struttura diventa semplice realizzare strumenti complessi che contengano configurazione e azioni.
Nel prossimo articolo studiamo in dettaglio alcune linee guida utili alla realizzazione di un “widget” ovvero un plugin complesso che mette a disposizione un controllo di interfaccia facilmente riutilizzabile in progetti differenti.
Al momento in cui scrivo il progetto di “widget collection” jQuery più affermato è sicuramente jQueryUI il quale mette a disposizione controlli di interfaccia evoluti quali dialog window, date picker, sortable list, etc.
Noi impareremo a creare componenti “custom” il cui utilizzo sia però ispirato alle linee guida di jQueryUI, fino ad arrivare alla creazione di nuovi widget completamente compatibili con jQueryUI e le sue linee guida.








