Elementi UI di associazione dati con IndexedDB

Raymond Camden
Raymond Camden

Introduzione

IndexedDB è un modo efficace per memorizzare i dati lato client. Se non li hai ancora esaminati, ti invitiamo a leggere i tutorial MDN utili sull'argomento. Questo articolo presuppone alcune conoscenze di base delle API e delle funzionalità. Anche se non hai mai utilizzato IndexedDB, la demo in questo articolo ti darà un'idea di cosa puoi fare con questa API.

La nostra demo è un semplice proof of concept di un'applicazione intranet per un'azienda. L'applicazione consentirà ai dipendenti di cercare altri dipendenti. Per offrire un'esperienza più rapida e immediata, il database dei dipendenti viene copiato sul computer del cliente e archiviato utilizzando IndexedDB. La demo fornisce semplicemente una ricerca e una visualizzazione in stile di completamento automatico di un singolo record del dipendente, ma la cosa bella è che, una volta che questi dati sono disponibili sul client, possiamo utilizzarli anche in molti altri modi. Ecco una descrizione di base di ciò che deve fare la nostra applicazione.

  1. Dobbiamo configurare e inizializzare un'istanza di IndexedDB. In genere questa operazione è semplice, ma farla funzionare sia in Chrome che in Firefox si rivela un po' complicato.
  2. Dobbiamo vedere se abbiamo dei dati e, in caso contrario, scaricarli. In genere, questa operazione viene eseguita tramite chiamate AJAX. Per la nostra demo abbiamo creato una semplice classe di utilità per generare rapidamente dati falsi. L'applicazione dovrà riconoscere quando sta creando questi dati e impedire all'utente di utilizzarli fino a quel momento. Si tratta di un'operazione una tantum. La volta successiva che l'utente eseguirà l'applicazione, non dovrà seguire questa procedura. Una demo più avanzata gestirebbe le operazioni di sincronizzazione tra il client e il server, ma questa demo si concentra maggiormente sugli aspetti dell'interfaccia utente.
  3. Quando l'applicazione è pronta, possiamo utilizzare il controllo Autocompletamento di jQuery UI per eseguire la sincronizzazione con IndexedDB. Sebbene il controllo di completamento automatico consenta elenchi e array di dati di base, dispone di un'API che consente qualsiasi origine dati. Dimostreremo come possiamo utilizzarlo per connetterci ai nostri dati IndexedDB.

Per iniziare

Questa demo è composta da più parti, quindi per iniziare in modo semplice, diamo un'occhiata alla parte HTML.

<form>
  <p>
    <label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
    </p>
</form>

<div id="displayEmployee"></div>

Non molto, vero? Ci sono tre aspetti principali di questa UI che ci interessano. Il primo è il campo "name" che verrà utilizzato per il completamento automatico. Il caricamento è disabilitato e verrà attivato successivamente tramite JavaScript. L'elemento span accanto viene utilizzato durante il seed iniziale per fornire aggiornamenti all'utente. Infine, il div con l'id displayEmployee verrà utilizzato quando selezioni un dipendente dal completamento automatico.

Ora diamo un'occhiata al codice JavaScript. Ci sono molte informazioni da assimilare, quindi procederemo passo passo. Il codice completo sarà disponibile alla fine, in modo da poterlo vedere nella sua interezza.

Innanzitutto, ci sono alcuni problemi relativi ai prefissi di cui dobbiamo preoccuparci tra i browser che supportano IndexedDB. Ecco un po' di codice della documentazione di Mozilla modificato per fornire alias semplici per i componenti di base di IndexedDB di cui ha bisogno la nostra applicazione.

window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

Di seguito sono riportate alcune variabili globali che utilizzeremo durante la demo:

var db;
var template;

Ora iniziamo con il blocco pronto per il documento jQuery:

$(document).ready(function() {
  console.log("Startup...");
  ...
});

La nostra demo utilizza Handlebars.js per visualizzare i dettagli del dipendente. Non verrà usato fino in seguito, ma possiamo andare avanti con la compilazione del modello e eliminarlo. Abbiamo un blocco di script configurato come tipo riconosciuto da Handlebars. Non è particolarmente elegante, ma semplifica la visualizzazione del codice HTML dinamico.

<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>

che viene poi ricompilata in JavaScript nel seguente modo:

//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);

Ora iniziamo a lavorare con IndexedDB. Innanzitutto, apriamolo.

var openRequest = indexedDB.open("employees", 1);

L'apertura di una connessione a IndexedDB ci permette di accedere in lettura e scrittura ai dati, ma prima di farlo dobbiamo assicurarci di avere un objectStore. Un oggettoObjectStore è come una tabella di database. Un database IndexedDB può avere molti oggettiStore, ognuno dei quali contiene una raccolta di oggetti correlati. La nostra demo è semplice e richiede un solo objectStore chiamato "employee". Quando indexedDB viene aperto per la prima volta o quando modifichi la versione nel codice, viene eseguito un evento onupgradeneeded. Possiamo utilizzarlo per configurare il nostro objectStore.

// Handle setup.
openRequest.onupgradeneeded = function(e) {

  console.log("running onupgradeneeded");
  var thisDb = e.target.result;

  // Create Employee
  if(!thisDb.objectStoreNames.contains("employee")) {
    console.log("I need to make the employee objectstore");
    var objectStore = thisDb.createObjectStore("employee", {keyPath: "id", autoIncrement: true});
    objectStore.createIndex("searchkey", "searchkey", {unique: false});
  }

};

openRequest.onsuccess = function(e) {
  db = e.target.result;

  db.onerror = function(e) {
    alert("Sorry, an unforseen error was thrown.");
    console.log("***ERROR***");
    console.dir(e.target);
  };

  handleSeed();
};

Nel blocco del gestore di eventi onupgradeneeded, controlliamo objectStoreNames, un array di archivi di oggetti, per vedere se contiene un dipendente. Se non è così, basta farlo. La chiamata createIndex è importante. Dobbiamo dire a IndexedDB quali metodi, oltre alle chiavi, utilizzeremo per recuperare i dati. Ne utilizzeremo una chiamata searchkey. Lo spiego tra un attimo.

L'evento onungradeneeded verrà eseguito automaticamente la prima volta che eseguiamo lo script. Dopo l'esecuzione o il salto nelle esecuzioni future, viene eseguito il gestore onsuccess. Abbiamo definito un gestore degli errori semplice (e brutto) e poi chiamiamo handleSeed.

Prima di continuare, vediamo rapidamente cosa succede. Apriamo il database. Verifichiamo se il nostro archivio di oggetti esiste. In caso contrario, lo creiamo noi. Infine, chiamiamo una funzione denominata handleSeed. Ora concentriamoci sulla parte di seeding dei dati della nostra demo.

Dammi qualche dato.

Come accennato nell'introduzione di questo articolo, questa demo ricrea un'applicazione in stile intranet che deve memorizzare una copia di tutti i dipendenti noti. In genere, questo comporta la creazione di un'API basata su server che possa restituire un conteggio dei dipendenti e fornire un modo per recuperare batch di record. Puoi immaginare un servizio semplice che supporta un conteggio iniziale e restituisce 100 persone alla volta. L'operazione potrebbe essere eseguita in modo asincrono in background mentre l'utente è impegnato in altre attività.

Per la nostra demo, facciamo qualcosa di semplice. Vediamo quanti oggetti abbiamo nel nostro IndexedDB. Se al di sotto di un determinato numero creeremo semplicemente utenti falsi. In caso contrario, la parte seed ha finito e possiamo attivare la parte con completamento automatico della demo. Diamo un'occhiata a handleSeed.

function handleSeed() {
  // This is how we handle the initial data seed. Normally this would be via AJAX.

  db.transaction(["employee"], "readonly").objectStore("employee").count().onsuccess = function(e) {
    var count = e.target.result;
    if (count == 0) {
      console.log("Need to generate fake data - stand by please...");
      $("#status").text("Please stand by, loading in our initial data.");
      var done = 0;
      var employees = db.transaction(["employee"], "readwrite").objectStore("employee");
      // Generate 1k people
      for (var i = 0; i < 1000; i++) {
         var person = generateFakePerson();
         // Modify our data to add a searchable field
         person.searchkey = person.lastname.toLowerCase();
         resp = employees.add(person);
         resp.onsuccess = function(e) {
           done++;
           if (done == 1000) {
             $("#name").removeAttr("disabled");
             $("#status").text("");
             setupAutoComplete();
           } else if (done % 100 == 0) {
             $("#status").text("Approximately "+Math.floor(done/10) +"% done.");
           }
         }
      }
    } else {
      $("#name").removeAttr("disabled");
      setupAutoComplete();
    }
  };
}

La prima riga è un po' complessa perché abbiamo più operazioni incatenate tra loro, quindi analizziamola:

db.transaction(["employee"], "readonly");

Viene creata una nuova transazione di sola lettura. Tutte le operazioni sui dati con IndexedDB richiedono una transazione di qualche tipo.

objectStore("employee");

Recupera lo spazio di archiviazione oggetti dei dipendenti.

count()

L'API count, che come puoi intuire, esegue un conteggio.

onsuccess = function(e) {

Al termine, esegui questo callback. All'interno del callback possiamo ottenere il valore del risultato, ovvero il numero di oggetti. Se il conteggio è pari a zero, iniziamo la procedura di seeding.

Utilizziamo il div di stato menzionato in precedenza per mostrare all'utente un messaggio che lo informa che stiamo per iniziare a ricevere dati. A causa della natura asincrona di IndexedDB, abbiamo configurato una semplice variabile, done, che monitora le aggiunte. Spieghiamo e inseriamo le persone false. Il codice sorgente della funzione è disponibile nel download, ma restituisce un oggetto simile al seguente:

{
  firstname: "Random Name",
  lastname: "Some Random Last Name",
  department: "One of 8 random departments",
  email: "first letter of firstname+lastname@fakecorp.com"
}

Di per sé, questo è sufficiente per definire una persona. Tuttavia, abbiamo un requisito speciale per poter cercare i nostri dati. IndexedDB non fornisce un modo per cercare elementi senza distinzione tra maiuscole e minuscole. Pertanto, creiamo una copia del campo cognome in una nuova proprietà, searchkey. Se ricordi, questa è la chiave che abbiamo detto di creare come indice per i nostri dati.

// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();

Poiché si tratta di una modifica specifica del client, viene eseguita qui e non sul server di backend (o nel nostro caso, l'immaginario server back-end).

Per eseguire le aggiunte al database in modo efficiente, devi riutilizzare la transazione per tutte le scritture collettive. Se crei una nuova transazione per ogni scrittura, il browser potrebbe causare una scrittura su disco per ogni transazione, il che peggiorerà notevolmente le prestazioni quando aggiungi molti elementi (ad esempio "1 minuto per scrivere 1000 oggetti").

Al termine del seed, viene attivata la parte successiva della nostra applicazione: setupAutoComplete.

Creazione del completamento automatico

Ora la parte divertente: il collegamento al plug-in Autocomplete di jQuery UI. Come per la maggior parte di jQuery UI, iniziamo con un elemento HTML di base e lo miglioriamo chiamando un metodo del costruttore. Abbiamo estratto l'intera procedura in una funzione chiamata setupAutoComplete. Ora diamo un'occhiata al codice.

function setupAutoComplete() {

  //Create the autocomplete
  $("#name").autocomplete({
    source: function(request, response) {

      console.log("Going to look for "+request.term);

      $("#displayEmployee").hide();

      var transaction = db.transaction(["employee"], "readonly");
      var result = [];

      transaction.oncomplete = function(event) {
        response(result);
      };

      // TODO: Handle the error and return to it jQuery UI
      var objectStore = transaction.objectStore("employee");

      // Credit: http://stackoverflow.com/a/8961462/52160
      var range = IDBKeyRange.bound(request.term.toLowerCase(), request.term.toLowerCase() + "z");
      var index = objectStore.index("searchkey");

      index.openCursor(range).onsuccess = function(event) {
        var cursor = event.target.result;
        if(cursor) {
          result.push({
            value: cursor.value.lastname + ", " + cursor.value.firstname,
            person: cursor.value
          });
          cursor.continue();
        }
      };
    },
    minLength: 2,
    select: function(event, ui) {
      $("#displayEmployee").show().html(template(ui.item.person));
    }
  });

}

La parte più complessa di questo codice è la creazione della proprietà di origine. Il controllo di completamento automatico di jQuery UI consente di definire una proprietà di origine che può essere personalizzata per soddisfare qualsiasi esigenza, anche i nostri dati IndexedDB. L'API fornisce la richiesta (in pratica ciò che è stato digitato nel campo del modulo) e un callback di risposta. È tua responsabilità inviare un array di risultati al callback.

La prima cosa che facciamo è nascondere il div displayEmployee. Questo viene utilizzato per visualizzare un singolo dipendente e, se ne è stato caricato uno in precedenza, per cancellarlo. Ora possiamo iniziare a cercare.

Iniziamo creando una transazione di sola lettura, un array chiamato result e un gestore oncomplete che passa semplicemente il risultato al controllo di completamento automatico.

Per trovare elementi che corrispondono al nostro input, utilizziamo un suggerimento dell'utente di StackOverflow Fong-Wan Chau: utilizziamo un intervallo di indice basato sull'input come limite di estremità inferiore e l'input più la lettera z come limite di intervallo superiore. Tieni anche presente che abbiamo scritto in minuscolo il termine affinché corrisponda alle lettere minuscole inserite.

Al termine, possiamo aprire un cursore (consideralo come l'esecuzione di una query sul database) ed eseguire l'iterazione sui risultati. Il controllo di completamento automatico di jQuery UI ti consente di restituire qualsiasi tipo di dati, ma richiede almeno una chiave di valore. Impostiamo il valore su una versione formattata correttamente del nome. Reintegriamo anche l'intera persona. A breve capirai il motivo. Innanzitutto, ecco uno screenshot dell'autocompletamento in azione. Utilizziamo il tema Vader per jQuery UI.

Di per sé, questo è sufficiente per restituire i risultati delle corrispondenze IndexedDB al completamento automatico. Vogliamo però anche supportare la visualizzazione dettagliata della corrispondenza quando ne viene selezionata una. Abbiamo specificato un gestore di selezione durante la creazione del completamento automatico che utilizza il modello Handlebars precedente.