Continuiamo la serie su HTML5 con un articolo che si occupa del salvataggio di informazioni off-line in relazione a una determinata applicazione. Con l‘affermarsi di un modello concettuale in cui sempre più si dà quasi per scontato l‘accesso alla rete, e diventa naturale essere costantemente collegati, gli aspetti progettuali e implementativi del local storage finiscono per assumere un ruolo cruciale.
Introduzione
La capacità di salvare dati associati a una applicazione è fondamentale in tutti i contesti, sia web che desktop. La lista di tutti quei dati, in qualche modo legati all’iterazione con l’utente, che possiamo definire “utili” a migliorare l’esperienza d’uso è semplicemente senza fine, a partire da informazioni tecniche per tracciare la navigazioni fra pagine, fino a capire lo stato dell’applicazione in caso di fallimento.
Nulla di trascendentale: anche se tutto è di per se’ utile, esiste un set minimale di queste informazioni che l’utente si aspetta vengano chieste e salvate in un qualsivoglia contesto, come il salvataggio delle opzioni, delle preferenze o delle credenziali di login, tutti dati che fanno ormai parte di pratiche consolidate.
Come salvare le informazioni?
Sebbene su questa tematica i requisiti di applicazioni desktop e web siano pressoche’ i medesimi, le appplicazioni web sono svantaggiate da strumenti strutturalmente non idonei.
Un’applicazione desktop è in grado di poter utilizzare diversi metodi per immagazzinare dati: avendo accesso al file system, può quindi scrivere file di configurazione in testo o strutturati come XML, può accedere a eventuali database, può sfruttare le API del sistema operativo sottostante per scrivere su registri di sistema.
Un’applicazione web, al contrario, vive su un server in un contesto limitato e vede il mondo da un’oblò, il client browser: questo implica che le informazioni da salvare siano immagazzinate lato server e correlate sul client da qualche sistema di associazione o che siano salvate sul client con qualche tecnica e poi inviate al server all’occorrenza. Fino ad adesso l’unico sistema esistente lato client impiega l’utilizzo dei cosiddetti cookie.
I biscottini del salvataggio
Fin dagli esordi del web, l’unico sistema di registrazione di queste informazioni è stato quello dei cookie, piccoli file di testo, gestiti dal browser in maniera trasparente all’utente finale, in grado di contenere piccoli quantitativi di dati, sotto forma di coppie chiave-valore.
Essendo l’unico strumento disponibile, possiamo pomposamente asserire che tutte le applicazioni web salvano dati attraverso i cookie; allo stesso tempo, più realisticamente, possiamo anche dire che tutti hanno almeno una ragione, tecnica o di natura diversa, per odiarli.
I cookie, sebbene basati su un concetto semplicissimo, non solo hanno una creazione e una gestione machiavellica, ma soffrono di un insieme di inconvenienti non indifferenti:
- I cookie sono trasmessi interamente in ogni richiesta HTTP, rallentando la comunicazione con informazioni spesso pleonastiche che vengono ripetute di volta in volta.
- I cookie, di per se’, non forniscono alcuna sicurezza, in quanto vengono trasmessi in chiaro attraverso Internet; certo, è sempre possibile utilizzare una connessione SSL, impedendone l’utilizzo su informazioni sensibili, e usandoli con tutta tranquillità: ma il meccanismo dei cookie in se’ non è intrinsecamente sicuro.
- La dimensione dei cookie è limitata a 4Kb di dati, sufficienti per appesantire un’applicazioni web, ma non grandi abbastanza da avere una reale utilità negli scenari moderni.
Naturalmente quasi nessuno si fa carico della gestione dei cookie, preferendo appoggiarsi su librerie JavaScript che nascondano la complessità ed espongano API molto più umane, come la libreria di Peter Paul Kocks [1] o l’ottima jQuery [2].
Un sistema di storage per lo scenario attuale (e futuro)
Se nel passato, pur con tutti i grattacapi procurati, i cookie hanno rappresentato una soluzione se non ottimale perlomeno accettabile, guardando le cose in prospettiva, i cookie semplicemente non vanno più bene: un’applicazione web moderna ha bisogno di un sistema di salvataggio dati che
- possa fornire maggiore spazio di immagazzinamento (più dei 4 Kb canonici);
- sia residente sul client e sia in grado di persistere anche oltre il ciclo di vita dell’applicazione;
- non richieda necessariamente di essere trasmesso al server;
- sia organizzato in un insieme di API pulito e standardizzato, che ne favorisca l’utilizzo.
Il sistema di storage introdotto da HTML5 è la risposta a questa esigenza.
Una retrospettiva sulle soluzioni
A guardare con attenzione al passato, spesso si riescono a comprendere meglio certi scenari presenti. La specifica HTML5 WebStorage non è stata la prima soluzione a essere introdotta sul mercato, ma è stata preceduta da una nutrita lista di tentativi, ovviamente tutti proprietari, che si sono succeduti nel tempo senza riuscire effettivamente a imporsi come standard de facto.
In principio, quando Internet Explorer dominava totalmente il panorama dei browser e ci si trovava in piena “epoca DHTML”, Microsoft introduceva senza sosta estensioni proprietarie note come DHTML behaviours, e una di queste era la famigerata userData. UserData permettava a ogni pagina web di persistere 64k di dati per dominio, utilizzando una struttura gerarchica XML-based. Se si utilizzava un dominio definito come “trusted”, come una intranet, questo limite veniva innalzato a 640k. Non solo non era possibile oltrepassare questa limitazione in nessuna maniera, ma, cosa che ai giorni nostri sarebbe inacettabile, tutto il meccanismo avveniva trasparentemente all’utente senza alcuna richiesta visibile o dialog di permesso.
Nel 2002 Adobe introdusse in Flash 6 un meccanismo non dissimile, noto come Local Shared Objects (LSO) o familiarmente Flash Cookies, per immagazzinare fino a 100k di informazioni per dominio. La tecnologia LSO è poi effettivamente sopravissuta e si è raffinata fino all’avvento di delle ExtrernalInterface in Flash 8 che ne hanno permesso una crescita spropositata: un’ExternalInterface è un bridge di API JavaScript per interfacciarsi direttamente con il Flash sottostante, fornendo un terreno fertile per il proliferare di librerie JavaScript che potessero sfruttare i meccanismi di storage Flash, senza conoscerne il meccanismo. Questo approccio, denominato AMASS (AJAX massive storage system) è sopravissuto fino a noi grazie a librerie JS come Dojo (Dojox.storage) che, fornendo un layer di astrazione di storage, permette di immagazzinare informazioni anche superiori al MB: in tal modo l’utilizzo di questi hack è stato reso quasi triviale.
Il primo vero e proprio attore capace di cambiare le regole del gioco fu Google, con il lancio nel 2007 di Gears, un plugin disponibile per i più diffusi browser del tempo che forniva un layer di base di servizi aggiuntivi, accedibili da un set di API uniforme, che si occupava di funzionalità accessorie come lo storage, la geolocalizzazione, il funzionamento offline. La rivoluzione di Gears fu quella di gestire un database SQL embedded basato su SQL Lite, che, previa autorizzazione dell’utente, rendeva effettivamente illimitato lo spazio di archiviazione dati.
Dojox.storage [4] ha continuato ad aggiornarsi nel suo lavoro di omogenizzazione e nel 2009 era in grado di determinare e fornire un’interfaccia unica a tecnologie davvero eterogenee quali Flash, Gears, Air, e ai primi prototipi di HTML5 che stavano facendo capolino su Firefox.
Per quanto le librerie JavaScript possano mitigare queste differenze, rimane comunque uno scoglio invalicabile rappresentato dai limiti che le singole tecnologie prevedono, in primis la quantità di spazio allocabile. La necessità di uno standard era diventata quantomeno palese.
HTML5 Storage
Quando ci riferiamo a HTML5 Storage intendiamo una precisa specifica W3C denominata Web Storage, in principio facente parte della specifica HTML5, ma poi separata in un corollario per ragioni più politiche che pratiche [3].
In particolare questo tipo di tecnolgia prende il nome di Web Storage anche per differenziarlo da tecnologie che invece si basano su database embedded, e comprende due declinazioni note come Session Storage e Local Storage.
In cosa consistono?
Pensiamo alla famiglia * Storage come a un cookie sotto steroidi, o meglio un cookie implementato in maniera intelligente. Come in un cookie, stiamo trattando informazioni archiviate come coppia chiave-valore, salvate localmente all’interno del browser; ma a differenza di quanto avviene nei cookie tradizionali, queste informazioni non vengono mai inviate al server, a meno che non sia specificatamente e deliberatamente richiesto.
Così come i cookie, queste informazioni possono avere uno scope di sessione e essere eliminate alla chiusura della finestra o essere persistenti, legate a un dominio, ed essere quindi condivise fra tutte le finestre del browser operanti su quel dominio.
Vi ricordate quando abbiamo accennato alla gestione “machiavellica” dei cookies? Questi due semplici comportamenti erano in realtà ottenuti utilizzando impropriamente hack quali la data di scadenza dei cookie che veniva settata o in data passata o futura. Nella specifica WebStorage, per motivi di ordine e pulizia, sono stati introdotti giustamente due oggetti seperati e distinti, la Session Storage e la Local Storage.
Operativamente non vi è una distinzione fra Session e Local Storage: la differenza risiede unicamente nello scope che hanno. Una Session Storage mantiene il proprio contenuto attivo unicamente durante il ciclo di vita della finestra di browser in cui è stata creata: una volta distrutta la finestra, anche i dati conservati vengono distrutti. In una Local Storage le informazioni vengono invece persistite: sono associate a un dominio e tutte le finestre del browser che accedono a quel dominio possono usufruire del medesimo storage senza limiti di tempo.
Il dominio sul quale si aggancia lo storage è il dominio dal quale viene eseguito lo script e non può essere modificato. Esiste tuttavia, nelle prime implementazioni, un tipo di storage denominato GlobalStorage del tutto simile all’attuale Local in cui era possibile specificare il dominio di appartenenza. Potendo essere una possibile fonte di problemi, sia di sicurezza che di legittimità, nessun browser ha deciso continuare a supportarlo.
Sporchiamoci le manine
Tratteremo in questo articolo il caso più interessante delle Local Storage, con un esempio. Essendo uno standard implementato nei browser moderni, non è necessario appoggiarsi a nessun plugin di terze parti, basta armarsi del browser di riferimento corretto (o del sistema operativo mobile adeguato), in una versione sufficientemente avanzata:
- Internet Explorer: 8+
- Mozilla Firefox: 3.5+
- Apple Safari: 4+
- Google Chrome: 4+
- Opera: 10.5+
- iOS 2.0
- Android: 2.0+
Il browser va bene?
Per creare un oggetto localStorage, innanzitutto dobbiamo capire se il browser che stiamo utilizzando sia effettivamente capace di supportare questa feature:
function isLocalStorageEnabled(){ return ('localStorage' in window) && window['localStorage'] !== null; } if(isLocalStorageEnabled){ // … OK } else { // … BAD }
In questa maniera controlliamo che all’interno dell’oggetto window del mio browser sia presente un oggetto localStorage e che esso non sia nullo. Non sempre questo check è sufficiente a rilevare l’abilitazione o meno di un localStorage: nelle prime implementazioni di Firefox la disabilitazione dei cookie implica anche quella dello storage, e se si cerca di accedere alla proprietà viene generata una eccezione. Consiglio quindi di delegare tutti i grattacapi direttamente a librerie di feature detection come l’ottima Modernizer [6]:
if (Modernizr.localStorage){ // … OK } else{ // … BAD }
Habemus LocalStorage
Diamo un’occhiata all’interfaccia WebStorage descritta nella specifica:
interface Storage { readonly attribute unsigned long length; getter DOMString key(in unsigned long index); getter any getItem(in DOMString key); setter creator void setItem(in DOMString key, in any data); deleter void removeItem(in DomString key); void clear(); }
Abbiamo già accennato che l’oggetto LocalStorage tratta le informazioni come coppie chiave-valore, ne possiede un indice e restituisce una lenght. I due metodi per scrivere e leggere sono setItem(), che scrive il valore alla chiave corrispondete e se esistente la sovrascrive, e getItem(), che restituisce il valore, altrimenti null, piuttosto che lanciare un’eccezione.
localStorage.setItem('nomeChiave', 'valore'); var valore = localStorage.getItem('nomeChiave');
Come la maggior parte degli oggetti JavaScript, LocalStorage ha un comportamento duale e può essere visto e trattatto come un array associativo; invece di utilizzare i due metodi sopracitati, si può usare la bracket notations:
localStorage['nomeChiave'] = 'valore'; var valore = localStorage['nomeChiave'];
Esiste anche un terzo metodo tipico del linguaggio JavaScript, gli expando. Per expando si intende una proprietà che dinamicamente viene creata su un oggetto nel momento stesso in cui viene acceduta:
localStorage.nomeChiave = 'valore'; var valore = localStorage.nomeChiave;
Le API forniscono un metodo denominato key() che prende in ingresso un parametro int index e ritorna la chiave associata, metodo particolarmente utile se vogliamo fare delle enumerazioni:
for (var i = 0; i < localStorage.length; i++){ var nomeChiave = localStorage.key(i); alert( "chiave : " +nomeChiave+ ", valore: " + localStorage.getItem(nomeChiave); }
Che succede se all’interno dello storage inserisco un nomeChiave che collide con una delle proprietà di localStorage? Per la trivialità di come è costruito l’oggetto JavaScript, stiamo evidentemente cercando guai: l’inserimento di un oggetto con nome key, ad esempio, provoca ilari conseguenze.
In Firefox, valore e proprietà sono mantenute separate e quindi non si verifica alcun problema; in Chrome persiste un bug tale percui il valore sovrascrive il metodo key() rendendolo inutilizzabile: sicuramente è un bug che verrà fixato ma, dal momento che non sappiamo se una tale versione del browser sarà o meno ancora in circolazione, pensado sempre al caso peggiore, in un’ottica di retrocompatibilità, bisogna cercare a priori di evitare eventuali collisioni di nomi.
Per rimuovere elementi dallo storage, la specifica prevede due metodi: removeItem() e clear(), rispettivamente per eliminare un singolo elemento data una chiave, e per svuotare l’intero storage di tutti gli elementi.
Come avviene per altri oggetti JavaScript, qualsiasi accesso a indici o elementi non esistenti non solleva alcuna eccezione, e per comodità viene restituito un null.
Quanto e cosa possiamo inserire nello storage?
Lo spazio predefinito dalla specifica è 5 Mb, senza possibilità di innalzare questo limite. Data la specifica, esiste tuttavia già la prima eccezione alla regola: di fatto Opera, su iniziativa personale, permette di innalzare questo limite a discrezione dell’utente, e Internet Explorer ha una quota fissa per dominio di 10 Mb. Se per una qualsiasi ragione si eccede a questa quota, viene lanciata una eccezione.
Qualsiasi tipo primitivo di JavaScript (String, Int, Boolean etc.) può essere inserito come valore all’interno dello storage; tuttavia è da sottolineare che lo storage immagazzina solo e unicamente sotto forma di stringhe. Da questo deriva il fatto che non solo è compito dello sviluppatore fare il cast coercitivo del valore ottenuto in uscita al valore desiderato, ma anche di tenere conto che, se si lavora con tipi complessi come i Float, i 5 Mb di spazio non sono poi così tanti.
Attualmente la best practice consiste nel salvare i dati sottoforma di JSON serializzato in stringhe. Consiglio la libreria JSON.js di Douglas Crockford [7] che possiede due comodi metodi json.stringify() e json.parse().
localStorage.setItem('nomeChiave', JSON.stringify( valoreJSON ) ); var valoreJSON = JSON.parse(localStorage.getItem('nomeChiave') || 'null' ) );
La seconda scrittura ci assicura che se il localStorage.getItem() restituisce null, al metodo JSON.parse() viene passata la stringa “null” in maniera tale che JSON.parse() ritorni una rappresentazione JSON del null.
Tracciare eventi
Ogni volta che vengono effettuate operazioni sullo Storage, sia Session che Local, e viene effettivamente variato qualche elemento, viene lanciato un evento di tipo Storage che è possible intercettare programmaticamente dalla logica della nostra applicazione come un qualsiasi evento. Ovunque sia supportata la specifica Storage, è supportato anche l’evento Storage.
Anche in questo caso, bisogna ricordarsi che il metodo AddEventListener(), standard W3C per la registrazione dei listener non è supportato dalle versioni di Internet Explorer < 9 e che l’oggetto event in IE è figlio di window.
if (window.AddEventListener){ windows.addEventListener('storage', handler, false); } else { window.attachEvent('onstorage', handler); } function handler(e){ if (!e) { e = window.event; } }
L’oggetto Storage Event in dettaglio:
event { key
String, il nomeChiave della proprietà
oldValue
any, il vecchio valore della proprietà o null se è appena creata
newitem
any, il nuovo valore o null se la proprietà è stata rimossa
url
String, la pagina che ha invocato il metodo che ha scatenato l’evento
}
Attenzione: nelle implementazioni iniziali, url portava il nome di uri; pertanto, se si vuole essere completamente retrocompatibili, bisogna tenere conto anche di questo.
Lo Storage Event non è cancellabile, ma la sua gestione non è obbligatoria, in quanto rappresenta una semplice notifica che è avvenuto qualcosa sullo Storage.
Oltre il Web Storage
Questa trattazione non sarebbe completa senza due cenni alla specifica gemella del WebStorage. Come abbiamo introdotto nella breve retrospettiva, la prima grande azienda del web a cercare di cambiare le regole del gioco è stata Google che ha proposto una implementazione basata su un DB vero e proprio. L’implementazione di Gears ha quindi fortemente influenzato una seconda specifica atta allo stoccaggio di dati, Web SQL Database, nota in precedenza come WebDB [8]. Web SQL Database espone un set di API JavaScript per la creazione e il mantenimento di un DB SQL Lite attraverso il linguaggio SQL.
Creiamo un DB chiamato nomeDB di 2 Mb, con il metodo opendatabase(name, version, description, size):
var db; if ('openDatabase' in window){ db = openDatabase( 'nomeDB', '1.0', 'Descrizione del DB', 2 *1014*1024); }
Le operazioni seguenti possono essere fatte direttamente con degli statement SQL utilizzando il metodo executeSQL(statementSQL, argomenti, SuccessCallback, ErrorCallback);
var statementSQL = ' Select from where '; function onSuccess(){ /* processiamo i risultati */ } function onError(){ /* segnaliamo errori */ } db .executeSql( statementSQL, [], onSuccess, onError);
Se masticate regolarmente SQL, il processo è assolutamente lineare e semplice.
Non entriamo in questo articolo eccessivamente nel dettaglio della specifica Web SQL Database perche’ allo stato attuale delle cose la specifica è attualmente in un punto di stallo: dietro alla sigla SQL si celano troppe implementazioni e troppi interessi che tirano e spingono il linguaggio SQL verso dialetti più o meno proprietari.
Il motivo per cui WebDB è stato poi rinominato in Web SQL DB è che una specifica neutra si sta facendo largamente strada sul mercato, e non è basata su SQL: Indexed DB [10]. Indexed DB, nota familiarmente come WebSimpleDB, non supporta statement SQL, ma si basa su una astrazione di un object Store, contenente diversi DB, composti da liste di record. Attraverso metodi esposti direttamente dalle Indexed DB API, una volta selezionato un database, si possono compiere operazione di selezione e di filtro sui record. Il resultSet ottenuto potrà poi essere navigato, posizionando un cursore mobile e consumando via via i risultati.
Conclusioni
In questo articolo abbiamo presentato una panoramica sul tema del Web Storage, di fondamentale importanza nell’attuale evoluzione del mondo web, alla luce delle specifiche collegate a HTML5.
Anche se non è possibile pensionare la tecnologia basata sui cookie per motivi di retrocompatibilità, è ormai definitivamente accertato che essa è inadeguata a soddisfare le esigenze di applicazioni complesse e moderne. L’introduzione della specifica Web Storage oltre ad apportare una ventata di pulizia e freschezza, ha davvero una importanza capitale nel contesto delle applicazioni Web, poichè si tratta di una specifica non imposta dall’alto, ma condivisa, supportata e sviluppata dai vendor di browser stessi. Accompagnato al sistema di caching offline, il gap esistente fra le applicazioni web e desktop è destinato ulteriormente a ridursi.
Non volevamo scendere in dettaglio anche perche’, come visto, il panorama dell’archiviazione client-side su database è tutt’altro che definito e definitivo: torneremo ad aggiornarvi quando il grado di maturità di questi aspetti tecnologici sarà tale da richiedere un approfondimento.
Nel prossimo articolo affronteremo un altro aspetto importante, quello delle web socket. Successivamente, la serie continuerà affrontando, tra l’altro, tutto quello che riguarda le scelte inerenti il multimedia nella nuova specifica HTML5.
Riferimenti
[1] La libreria di Peter Paul Kocks, per la gestione dei cookie
http://www.quirksmode.org/js/cookies.html
[2] Il sistema jquery-jstore per il salvataggio dei cookie
http://code.google.com/p/jquery-jstore/
[3] La specifica Web Storage del W3C
http://dev.w3.org/html5/webstorage/
[4] Dojo Store API
http://dojotoolkit.org/reference-guide/dojo/store.html
[5] La pagina di Wikipedia sul Web Storage
http://en.wikipedia.org/wiki/Web_Storage
[6] Modernizr
[7] La libreria JSON.js di Douglas Crockford
https://github.com/douglascrockford/JSON-js
[8] Web SQL Database, la specifica “parallela”
http://www.w3.org/TR/webdatabase/
[9] Dive Into HTML5
http://diveintohtml5.org/storage.html
[10] Indexed DB
http://www.w3.org/TR/IndexedDB/
[11] Persistence.JS