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 uno schema di base di ciò che deve fare la nostra applicazione.

  1. Dobbiamo configurare e inizializzare un'istanza di IndexedDB. Per la maggior parte è semplice, ma farlo funzionare sia in Chrome che in Firefox si rivela un po' complicato.
  2. Dobbiamo vedere se abbiamo 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, giusto? Ci sono tre aspetti principali di questa UI che ci interessano. Il primo è il campo "name" che verrà utilizzato per il completamento automatico. Viene caricato disattivato e verrà attivato in un secondo momento 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 molti aspetti da considerare, quindi procederemo passo passo. Il codice completo sarà disponibile alla fine, quindi potrai visualizzarlo nella sua interezza.

Innanzitutto, ci sono alcuni problemi relativi ai prefissi che dobbiamo considerare tra i browser che supportano IndexedDB. Ecco un po' di codice della documentazione di Mozilla modificato per fornire semplici alias 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 jQuery document ready:

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

La nostra demo utilizza Handlebars.js per visualizzare i dettagli del dipendente. Non verrà utilizzato fino a un secondo momento, ma possiamo procedere e compilare il modello ora per toglierlo di mezzo. Abbiamo un blocco di script configurato come tipo riconosciuto da Handlebars. Non è molto sofisticato, ma semplifica la visualizzazione dell'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 consente di leggere e scrivere dati, ma prima di farlo dobbiamo assicurarci di avere un objectStore. Un objectStore è simile a 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 degli eventi onupgradeneeded, controlliamo objectStoreNames, un array di oggetti store, per vedere se contiene employee. In caso contrario, lo facciamo. 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 viene eseguito 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 qui. Apriamo il database. Controlliamo se il nostro object store esiste. In caso contrario, la creiamo. 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 fornirci 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, se presenti, abbiamo in IndexedDB. Se il numero è inferiore a un determinato valore, creeremo semplicemente utenti falsi. In caso contrario, consideriamo completata la parte del seme e possiamo attivare la parte di 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()

Esegui l'API count, che, come puoi immaginare, 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 è 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. Eseguiamo un ciclo 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"
}

Da solo, 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 per il client, viene eseguita qui anziché sul server di backend (o, nel nostro caso, sul server di backend immaginario).

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 sul 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 denominato result e un gestore oncomplete che passa semplicemente il risultato al controllo di completamento automatico.

Per trovare gli elementi corrispondenti al nostro input, utilizziamo un suggerimento dell'utente di StackOverflow Fong-Wan Chau: utilizziamo un intervallo di indici basato sull'input come limite inferiore e l'input più la lettera z come limite superiore dell'intervallo. Tieni presente che il termine viene scritto in minuscolo per corrispondere ai dati in minuscolo che abbiamo inserito.

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 ben formattata 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.