Questa è la traduzione dell'articolo Advanced Debugging with JavaScript di Chris Mills e Hallvord R. M. Steen pubblicato originariamente su A List Apart il 03 Febbraio 2009. La traduzione viene qui presentata con il consenso dell'editore e dell'autore.
Se usati bene i debugger Javascript possono aiutarvi a trovare e risolvere gli errori presenti nel codice. Per diventare esperti in questo tipo di operazione dovrete conoscere gli strumenti per il debug che avete a disposizione, il flusso di lavoro tipico nell'attività di debugging e i requisiti di codice per un debug efficace. In questo articolo discuteremo alcune tecniche avanzate per diagnosticare e risolvere i bug usando un'applicazione web di esempio.
Una nota sull'accessibilità
L'articolo mette in evidenza i punti di forza e le differenze tra i principali strumenti di debug e mostra come eseguire compiti di debug avanzato su Javascript. I nostri metodi sono spesso basati sull'uso del mouse; se preferite la tastiera o se usate tecnologie assistive come gli screen reader per interagire con questi strumenti, dovreste consultare la loro documentazione per capire come e se questi tool possono funzionare per voi.
I debugger
Con un numero crescente di buoni debugger disponibili, il programmatore Javascript può guadagnare molto imparando ad usarli bene. Le interfacce dei debugger Javascript stanno diventando più pulite, più standardizzate tra i vari prodotti e più facili da usare: ciò rende più semplice per i novizi e per gli utenti più esperti imparare le tecniche di debugging.
Al momento ci sono strumenti di debug disponibili per i principali browser:
- Firefox può contare sulla nota estensione Firebug
- IE8 (al momento in cui è stato scritto questo articolo è in versione beta) offre dei Developer Tools integrati nel browser
- Opera (dalla versione 9.5) supporta il debugger Opera Dragonfly
- Safari può sfruttare sia il debugger Drosera JS, sia un tool per l'esplorazione del DOM chiamato WebInspector (nelle versioni più recenti del browser il debugger è stato integrato proprio nel Web Inspector)
Al momento Firebug e Dragonfly sono le opzioni più stabili. La versione beta di IE8 a volte ignora i breakpoint e al momento in cui scriviamo questo articolo, il WebInspector sembrava avere delle incompatibilità con le nightly build di Webkit.
Familiarizzate con più strumenti di debug, perché non saprete mai in quale browser si verificherà il prossimo errore. Dal momento, poi, che i vari tool sono tutto sommato comparabili per le funzionalità che offrono, è facile passare da uno all'altro una volta che si sappia come usarne uno.
Il flusso di lavoro del debug
Quando si investiga su un problema specifico, seguirete in genere questo procedimento:
- Trovate il codice che vi interessa nel pannello per la vista del codice del debugger
- Impostate uno o più breakpoint dove pensate che possano accadere cose interessanti
- Eseguite lo script ricaricando la pagina nel caso di script inline o cliccando su un pulsante in caso lo script sia attivato da un gestore di eventi
- Aspettate fin quando il debugger mette in pausa l'esecuzione rendendo possibile procedere nel codice passo a passo
- Investigate i valori delle variabili. Per esempio, cercate le variabili che non sono definite quando invece dovrebbero contenere un valore o quelle che restituiscono "false" quando vi aspettate invece che restituiscano "true"
- Se necessario usate la linea di comando per valutare il codice o modificare la variabile per fare dei test
- Trovate il problema imparando quale pezzo di codice o quale input hanno causato le condizioni di errore
Per creare un breakpoint, potete anche aggiungere questa dichiarazione per il debug al codice:
function frmSubmit(event){ event = event || window.event; debugger; var form = this; }
Requisiti per l'uso dei debugger
La maggior parte dei debugger richiede codice ben formattato. Gli script scritti su una sola riga rendono difficile la scoperta di errori nei debugger basati sulla lettura riga per riga del codice. Il codice offuscato può anch'esso essere difficile da debuggare, specialmente il codice che è stato compresso (packed) e ha bisogno di essere di espanso usando eval ()
. Molte librerie Javascript vi consentono di scegliere tra versioni compresse/offuscate e ben formattate: queste versioni non compresse, seppure più 'pesanti' sono quelle idealmente da usare per il debug perché hanno codice ben formattato.
Una dimostrazione di debug
Iniziamo con un piccolo esempio, riempito appositamente di bug, per imparare a diagnosticare e trattare i problemi nel codice Javascript. Il nostro esempio è rappresentato dalla schermata di login di un'applicazione web.
Immaginate di essere al lavoro su questa favolosa applicazione e che i vostri tester vi chiedano di investigare su questa lista di bug:
- Il messaggio "Loading..." sulla barra di stato non scompare quando l'applicazione è completamente caricata
- La lingua di default è il norvegese anche sulle versioni in inglese di Firefox e IE
- Una variabile globale chiamata
prop
viene creata da qualche parte - Nel DOM viewer tutti gli elementi hanno un attributo "
clone
" - La validazione dei form rispetto alla lunghezza minima del contenuto inserito nei campi non funziona: inviando il form con uno username di un solo carattere dovrebbe essere generato e visualizzato un messaggio di errore
- È possibile lasciare vuoto il campo della password: al momento di inviare il form, invece, dovrebbe comparire un messaggio che avvisa dell'impossibilità di lasciare quel campo non compilato
- Il login non funziona e viene mostrato un messaggio di errore che parla di un attacco di tipo cross-site scripting individuato
Lanciare i debugger
In Firefox dovrete essere sicuri di aver installato l'estensione Firebug. Per iniziare selezionate dal menu Tools (Strumenti) > Firebug > Open Firebug (Apri Firebug) per iniziare.
Su Opera 9.5 e successivi, scegliete dal menu Tools (Strumenti) > Advanced (Avanzate) > Developer Tools (Strumenti per gli sviluppatori).
Su IE8 selezionate Tools > Developer Tools.
Su Safari o WebKit, prima abilitate il menu di debug se non è abilitato, poi selezionate Debug (Sviluppo) > Show Web Inspector (Mostra...).
È il momento di accendere i debugger. Per seguire la demo, prestate attenzione alle istruzioni passo per passo che offriremo. Dal momento che alcune istruzioni richiedono modifiche sul codice, potete salvare la pagina della demo in locale e caricarla dal vostro disco prima di iniziare.
Bug 1: il messaggio "Loading..." sulla barra di stato
Se date un'occhiata alle applicazioni di debug in Firebug e Dragonfly, vi troverete inizialmente davanti a queste schermate (attivate in entrambi i casi la tab 'Scripts'):
Scorrendo il codice sorgente nel debugger, notate che c'è una funzione clearLoadingMessage()
definita all'inizio del codice stesso. Sembra essere un buon punto per impostare un breakpoint.
Ecco come fare:
- Cliccate sul numero di riga alla sinistra per impostare un breakpoint in corrispondenza della prima riga all'interno della funzione
clearLoadingMessage()
- Ricaricate la pagina
Nota: il breakpoint deve essere impostato su una riga con codice che sarà eseguito quando la funzione viene attivata. La riga che contiene la funzione clearLoadingMessage() {
è solo l'intestazione della funzione. Impostando un breakpoint in quel punto non farà andare in pausa il debugger. Impostate il breakpoint, invece, sulla prima riga all'interno della funzione.
Quando la pagina viene ricaricata, l'esecuzione dello script si blocca in corrispondenza del breakpoint e vedrete qualcosa come quello mostrato in figura 2 (Dragonfly è in alto, Firebug in basso):
Procediamo passo per passo all'interno della funzione. Vedrete che essa aggiorna i due elementi DOM e che su una riga immediatamente successiva a quella dove abbiamo impostato il breakpoint è presente la parola statusbar
. Sembra importante. È probabile che getElements( 'p', {'class':'statusbar'} )[0]
trovi l'elemento statusbar
nel DOM. C'è un modo per testare questa supposizione rapidamente?
Incollate lo snippet di codice che vi interessa nella linea di comando per verificare. La figura 3 mostra tre screenshot (nell'ordine Dragonfly, Firebug e IE8) dopo la lettura di innerHTML
o outerHTML
dell'elemento restituito dal comando su cui state investigando:
Procediamo così nel test:
- Trovate la linea di comando:
Su Firebug selezionate la tab "Console"
Su Dragonfly cercate sotto il pannello con il codice sorgente Javascript
Nei Developer Tools di IE8 trovate la tab sulla destra con l'etichetta "Console" - Incollate
getElements( 'p', {'class':'statusbar'} )[0].innerHTML
nel pannello della linea di comando - Premete Invio
La linea di comando è uno strumento molto utile che consente di testare piccoli snippet di codice rapidamente. L'integrazione della console di Firebug è anche molto utile dal canto suo: se il vostro comando restituisce come output un oggetto, avrete a disposizione una vista molto intelligente del tutto. Per esempio, otterrete una rappresentazione in forma simile a quella del markup se si tratta di un oggetto DOM.
Potete usare la linea di comando per esplorare il problema più in profondità. La riga di Javascript che stiamo cercando fa le seguenti tre cose:
- Cattura un riferimento all'elemento usato nella pagina come barra di stato. Nella vista offerta dal DOM Inspector, vedrete che il markup che corrisponde a questo elemento è
<p class="statusbar">
- Cerca poi il suo
firstChild
, in altre parole, il primo nodo all'interno del paragrafo - Imposta la proprietà
innerText
Proviamo a eseguire qualcosa di più del comando dalla linea di comando. Per esempio, potreste chiedervi qual è il valore corrente della proprietà innerText
di questo elemento prima che sia impostato. Per verificare potete inserire questo intero comando nella console:
getElements( 'p', {'class':'statusbar'} )[0].firstChild.innerText
Sorprendentemente, l'output non restituisce... nulla. Così l'espressione getElements( 'p', {'class':'statusbar'} )[0].firstChild
punta a qualcosa nel DOM che non contiene testo, o che non ha una proprietà innerText
.
Dunque, la prossima domanda è: qual è esattamente il primo nodo figlio del paragrafo? Chiediamo alla linea di comando (osservate i risultati nell'immagine qui sotto) inserendo :
getElements( 'p', {'class':'statusbar'} )[0].firstChild
L'output di Dragonfly [object Text] mostra che questo è un nodo di testo DOM. Firebug ci mostra il contenuto del nodo di testo come un link al DOM inspector. Avete ora trovato il problema che causa il primo bug: un nodo di testo non ha una proprietà innerText
, solo nodi di elemento ce l'hanno. Dunque, impostando p.firstChild.innerText
non produce nulla. Questo bug può essere risolto sostituendo innerText
con nodeValue
, che è una proprietà del W3C DOM con cui si definiscono i nodi di testo.
Avete la possibilità ora di rivedere l'esempio:
- Premete F5 o premete il pulsante 'run' per terminare lo script, ora che avete trovato il problema
- Ricordate di eliminare i vecchi breakpoint cliccando di nuovo sul numero di riga
Termina qui la prima parte dell'articolo. Vedremo la settimana prossima come affrontare gli altri bug.
Bug 2: problemi nell'individuazione della lingua
Avrete notato nel codice la riga con var lang; /*language*/
più o meno in cima alla parte Javascript del codice. Il codice che imposta questa variabile è probabilmente responsabile del secondo bug. Potete trovare le cose nel codice rapidamente usando la comoda funzionalità di ricerca fornita da entrambi i debugger. In Dragonfly è proprio sopra il pannello di vista del codice; in Firebug si trova in alto, a destra (figura 5):
Per trovare il punto che ha a che fare con la localizzazione dell'applicazione:
- Scrivete
lang =
nel campo di ricerca - Impostate un breakpoint sulla riga in cui alla variabile lang è assegnato un valore
- Ricaricate la pagina
Il WebInspector di Safari offre anche una potente funzionalità di ricerca. WebInspector vi consente di cercare in tutto il codice nello stesso tempo, compreso il markup, i CSS e Javascript. Il risultato viene mostrato in un pannello dedicato dovete fare doppio click sui risultati per saltare alla riga corrispondente, come mostrato nello screenshot.
Per capire cosa fa questa funzione:
- Usate il pulsante "step into" (su Firebug è una freccia con la punta rivolta verso il basso, n.d.t) per entrare nella funzione
getLanguage
- Cliccate ancora ripetutamente sul pulsante "step into" per procedere nel codice una riga alla volta
- Date un'occhiata all'overview delle variabili locali per vedere come cambiano mentre si procede passo a passo in questa funzione
Procedendo nel contesto della funzione getLanguage
, vedrete che prova a leggere il valore per la lingua dalla stringa dello user agent. L'autore di questo codice ha notato che nel contesto di questa stringa sono incluse, in certi browser, alcune informazioni sulla lingua e pertanto cerca di parsare navigator.userAgent
per estrarre questa informazione:
var str1 = navigator.userAgent.match( /((.*))/ )[1]; var ar1 = str1.split(/s*;s*/), lang; for (var i = 0; i < ar1.length; i++){ if (ar1[i].match(/^(.{2})$/)){ lang = ar1[i]; } }
Procedendo all'interno di questo codice con i debugger, potete usare l'overview delle variabili locali. La figura 6 mostra Firebug e i Developer Tools di IE8 con l'array ar1
espanso per visualizzare gli elementi al suo interno:
La dichiarazione ar1[i].match(/^(.{2})$/)
cerca una stringa che è lunga due caratteri, aspettando di trovare un codice per la lingua di due caratteri come en
o no
. Tuttavia, come potete vedere dagli screenshots, l'informazione sulla lingua nella stringa dello user agent di Firefox è al momento nn-NO
. IE non ha informazioni sulla lingua in questa parte della stringa dello user agent.
Abbiamo trovato il secondo bug. La funzione per individuare la lingua cerca un codice di due lettere, ma Firefox ha una stringa con 5 caratteri e IE non presenta nessuna informazione. Il codice per individuare la lingua dovrebbe essere probabilmente parsato diversamente e sostituito con codice lato server che usi l'header HTTP Accept-Language
o che vada a leggere navigator.language. Oppure navigator.userLanguage su IE. Un esempio di come potrebbe apparire questa funzione è questo:
function getLanguage() { var lang; if (navigator.language) { lang = navigator.language; } else if (navigator.userLanguage) { lang = navigator.userLanguage; } if (lang && lang.length > 2) { lang = lang.substring(0, 2); } return lang; }
Bug 3: la misteriosa variabile prop
Nella figura 7 potete vedere chiaramente la misteriosa variabile prop:
prop
Le applicazioni scritte bene mantengono al minimo il numero di variabili globali perché esse possono causare confusione quando differenti sezioni dell'applicazione provano a usare lo stesso nome di variabile. Immaginate che domani un altro team di sviluppatori aggiunga una nuova funzionalità alla nostra applicazione e nomini una variabile come prop. Avremmo due parti dell'applicazione che provano a usare lo stesso nome di variabile per fare cose differenti. Questa pratica può essere l'origine per conflitti e bug. Dunque, dobbiamo verificare dove è impostata questa variabile e vedere se c'è un modo per renderla locale. Potete iniziare facendo una ricerca come abbiamo fatto per il bug 2, ma forse c'è un modo più intelligente...
I debugger per molti altri linguaggi di programmazione adottano il concetto di "watch" che può interrompere il debugger quando una variabile cambia. Né Dragonfly né Firebug supportano il "watch", ma è facile ottenere lo stesso effetto aggiungendo la seguente riga di codice di debug all'inizio del codice dello script che ci sta dando problemi:
__defineSetter__('prop', function() { debugger; });
Per aggiungere questa funzionalità che emula il "watch" allo script:
- Aggiungete il codice di debug all'inizio del primo script
- Ricaricate la pagina
- Notate come si interrompe quando si incontra un problema
Usando getters e setters possiamo quindi emulare la funzionalità "watch" e si possono definire dei breakpoint intelligenti.
I Developer Tools di IE8 hanno un pannello "watch", ma non è possibile interrompere l'esecuzione quando una variabile viene modificata. Considerato il supporto incompleto di IE8 per getters e setters, non è possibile emulare la funzionalità nel modo in cui è possibile fare con Opera, Firefox e Safari.
Quando ricaricate l'applicazione, essa si interrompe immediatamente nel punto in cui la variabile globale prop
è stata definita. Di fatto si ferma alla riga del codice di debug che avete aggiunto perché è lì che si trova la dichiarazione debugger. Un click sul pulsante "step out" vi porterà dalla funzione setter al punto in cui la variabile è impostata. Questo codice si trova all'interno della funzione getElements
:
for (prop in attributes) { if (el.getAttribute(prop) != attributes[prop]) includeThisElement» = false;
Ora si ferma proprio sotto la linea che inizia con for (prop
. Qui potete vedere che la variabile prop
è usata senza essere definita come variabile locale, con una keyword var
all'interno della funzione. Basta cambiare tutto in for (var prop
per fissare il bug 3.
Bug 4: l'attributo clone che non dovrebbe essere lì
Il quarto bug è stato riscontrato da un tester che usava il DOM Inspector. Non è infatti visibile partendo dall'interfaccia utente dell'applicazione. Non appena, però, si apre il DOM Inspector (in Firebug c'è la tab "HTML", mentre in Dragonfly la funzionalità è chiamata "DOM") diviene chiaro che molti elementi hanno attributi clone con al loro interno del codice Javascript che non dovrebbe essere lì:
Ogni elemento creato dallo script ha un attributo clone
superfluo. Dato che è qualcosa che l'utente finale non noterà mai potrebbe sembrare un bug non troppo serio. Ma immaginate l'impatto sulle performance se questo script venisse usato per creare un albero DOM con centinaia o migliaia di elementi...
Il modo più rapido per trovare questo problema consiste nel definire un breakpoint (interruzione) che si verifichi solo quando un attributo chiamato clone
è impostato su un elemento HTML. I debugger possono fare questo?
Javascript è un linguaggio molto flessibile e uno dei suoi punti di forza (o di debolezza, dipende dai punti di vista) è che si possono sostituire funzioni core con quelle create da noi. Aggiungete questo snippet di codice di debug alla pagina e sostituirà l'originale metodo setAttribute ()
con uno che interrompe se viene impostato un attributo clone:
var funcSetAttr = Element.prototype.setAttribute; /* keep a pointer to the original function */ Element.prototype.setAttribute = function(name, value) { if (name == 'clone') { debugger; /* break if script sets the 'clone' attribute */ } funcSetAttr.call(this,name,value); /* call original function to ensure those other attributes are set correctly */ };
Per scoprire il punto di creazione dell'attributo clone
:
- Aggiungete il codice di debug all'inizio del primo script del documento
- Ricaricate la pagina
Appena ricaricate la pagina, lo script inizia a generare il DOM, ma si interrompe la prima volta in cui tenta di impostare il cattivo attributo clone
.
Nota: nelle versioni correnti di Firefox setAttribute
ha varie implementazioni specifiche per elemento. Il codice visto qui sopra funziona come ci si attende su Opera; per ottenere lo stesso effetto su Firefox potere sostituire la parola Element
con HTMLFormElement
per rimpiazzare il più specifico metodo HTMLFormElement.prototype.setAttribute
.
Quando l'esecuzione si interrompe al breakpoint, vorrete sapere dove si è verificata la chiamata a setAttribute()
. Ciò significa che dovete tornare indietro alla lista delle funzioni richiamate e scoprire come lo script è arrivato a quel punto. Per questo scopo usiamo la funzionalità stack.
La figura 9 mostra gli stack delle chiamate in Dragonfly e IE8 quando si stoppano in corrispondenza dei breakpoint. Giusto come riferimento, ho creato un breakpoint manuale su IE8 più o meno nella stessa posizione in cui si ferma la tecnica del setAttribute()
. Potete vedere che in Dragonfly la funzione richiamata più di recente è messa all'inizio (è la funzione anonima che è stata mappata su setAttribute()
). È stata richiamata da makeElement
alla riga 95:
La figura 10 mostra invece la vista degli stack su Firebug. La riga setAttribute < makeElement < init
vicina al nome del file è lo stack (potete vedere che la funzione richiamata più di recente è quella all'estrema sinistra):
Cliccando le funzioni precedenti nella lista degli stack potete tornare indietro a ritroso attraverso le chiamate e osservare come siete arrivati a quel punto. È importante che proviate da voi per comprendere pienamente quanto è potente questa funzionalità. Notate che quando passate ad un'altra voce di stack il pannello delle variabili locali è aggiornato per mostrarvi lo stato corrente delle variabili nella funzione che effettua la chiamata.
Come usare lo stack delle chiamate per trovare una funzione problematica:
- Cliccate la funzione che volete vedere nello stack delle chiamate
- Notate che il pannello delle variabili locali viene aggiornato per mostrare le variabili locali per quella funzione
- Ricordate che se usate i pulsanti di step essi vi porteranno avanti dall'ultima chiamata anche se state ispezionando altre parti dello stack
Cliccando il riferimento a makeElement
veniamo portati indietro a questa parte:
for (var prop in attributes) { el.setAttribute(prop, attributes[prop]); }
dove vedete la chiamata a setAttribute
. I pannelli per le variabili locali mostrano che il valore di prop
è in effetti clone
. La variabile prop
è definita nel loop for...in
. Ciò ci dice che una delle proprietà dell'oggetto attributes
deve essere chiamata clone
.
La variabile attributes
è definita come secondo argomento della funzione. Se andate un passo sopra alla parte init
dello stack, potete vedere la precedente chiamata di funzione è stata questa:
var form = makeElement(‘form’, { action:’/login’, method:’post’, name:’loginform’ }, document.body);
Il secondo argomento per il metodo è evidenziato in grassetto: questo oggetto non ha una proprietà clone
. Dunque, da dove viene?
Se cliccate sulla funzione makeElement
nello stack potete ispezionare la variabile attributes
. In Firebug vedrete apparire la proprietà clone
. Potete cliccare la funzione a cui fa riferimento per saltare alla definizione (notate la riga evidenziata in blu nella figura 11):
La versione alpha di Dragonfly non include le proprietà del prototipo di un oggetto nella vista delle variabili, così clone
non appare. Ma ora avete imparato che la funzione clone
esiste su tutti gli oggetti. Se abilitate l'impostazione "stop at a new script" e create un thread che richiama questo metodo, salterete direttamente ad esso.
Per trovare la definizione clone su Dragonfly:
- Spuntate l'opzione "stop at new script" con il pulsante dell'interfaccia mostrato nello screenshot che segue
- Incollate il codice che segue nella barra degli indirizzi della finestra di debug:
javascript:void({}.clone());
Con una bookmarklet potete iniziare nuovi thread nel documento sottoposto a debug quando volete. Per esempio, potete provare il metodo javascript:void({}.clone());
. Quando Dragonfly si blocca al nuovo thread, un click sul pulsante "step into" vi porta alla definizione problematica della proprietà clone
.
Ed ecco la causa del bug 4: un metodo clone
è aggiunto a tutti gli oggetti usando la sintassi Object.prototype
. Questa è considerata una pratica cattiva perché usare for...in
su oggetti farà vedere tutto quello che aggiungete a Object.prototype
. Ciò può creare dei bug molto difficili da scovare.
Per correggere questo bug potete spostare il metodo clone così che sia definito direttamente sull'Object dell'oggetto invece che sul prototipo e poi correggere ogni chiamata a obj.clone
in modo che diventino Object.clone(obj)
. Per esempio, evitate questo:
// BAD, don't do this: Object.prototype.clone = function() { var obj = {}; for (var prop in this) { obj[prop] = this[prop]; } return obj; } // some code that demonstrates using the clone() method: var myObj1 = { 'id': '1' }; var myObj2 = myObj1.clone();
E fate questo:
Object.clone = function(originalObject) { var obj = {}; for (var prop in originalObject) { obj[prop] = originalObject[prop]; } return obj; } // some code that demonstrates using the clone() method: var myObj1 = { 'id': '1' }; var myObj2 = Object.clone(myObj1);
L'ultimo esempio impedisce la modifica di Object.prototype
e così funziona meglio con altri script che usano loop del tipo for...in
.
Bug da 5 a 7
Ora, caro lettore, devi proseguire per conto tuo. Usando le tue nuove conoscenze i bug 5, 6 e 7 puoi correggerli tu. Buona fortuna e buon divertimento.
Conclusioni
Questo articolo dimostra le basi dell'uso dei debugger e alcune tecniche avanzate di debugging Javascript. Avete imparato come impostare breakpoint sia dai debugger sia con gli script, come procedere passo a passo nel codice, come usare l'interfaccia utente dei debugger, come impostare breakpoint avanzati e come integrare bookmarklet nelle tecniche di debugging.
Se avete avuto problemi a seguire la parte più avanzata di questo articolo, non preoccupatevi. Anche padroneggiare inizialmente le basi vi migliorerà come sviluppatori.