Elementi UI di associazione dati con IndexedDB

Raymond Camden
Raymond Camden

Introduzione

IndexedDB è un modo efficace per archiviare i dati lato client. Se non l'hai ancora fatto, ti consiglio di leggere gli utili tutorial di MDN sull'argomento. Questo articolo presuppone una conoscenza di base delle API e delle funzionalità. Anche se non hai mai visto IndexedDB, spero che la demo di questo articolo ti dia un'idea di cosa puoi fare con questo strumento.

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

  1. Dobbiamo configurare e inizializzare un'istanza di un IndexedDB. Per la maggior parte, si tratta di un'operazione semplice, ma farla funzionare sia su Chrome sia su Firefox si rivela un po' complicato.
  2. Dobbiamo verificare 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 fittizi. L'applicazione dovrà riconoscere quando sta creando questi dati e impedire all'utente di utilizzarli fino a quel momento. Questa operazione deve essere eseguita una sola volta. La volta successiva che l'utente eseguirà l'applicazione, non dovrà ripetere 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 Autocomplete di jQuery UI per la sincronizzazione con IndexedDB. Sebbene il controllo di completamento automatico consenta elenchi e array di dati di base, dispone di un'API per consentire qualsiasi origine dati. Ti mostreremo come utilizzarlo per connetterti 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. Viene caricato disattivato e verrà attivato in un secondo momento tramite JavaScript. Lo span accanto viene utilizzato durante il seeding 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 dopo passo. Il codice completo sarà disponibile alla fine, così potrai vederlo nella sua interezza.

Innanzitutto, ci sono alcuni problemi di prefisso di cui dobbiamo preoccuparci tra i browser che supportano IndexedDB. Ecco un codice della documentazione di Mozilla modificato per fornire semplici alias per i componenti principali di IndexedDB necessari alla nostra applicazione.

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

Poi, alcune variabili globali che utilizzeremo nel corso della demo:

var db;
var template;

Ora inizieremo con il blocco pronto per il documento jQuery:

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

La nostra demo utilizza Handlebars.js per visualizzare i dettagli dei dipendenti. Non verrà utilizzato fino a un secondo momento, ma possiamo procedere e compilare il nostro template ora per non doverlo fare in seguito. Abbiamo configurato un blocco di script come tipo riconosciuto da Handlebars. Non è particolarmente sofisticato, ma semplifica la visualizzazione dell'HTML dinamico.

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

Questo viene poi compilato nel nostro JavaScript in questo modo:

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

Ora iniziamo a lavorare con IndexedDB. Innanzitutto, apriamola.

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 IndexedDB può avere molti objectStore, ognuno dei quali contiene una raccolta di oggetti correlati. La nostra demo è semplice e richiede un solo objectStore che chiamiamo "employee". Quando indexedDB viene aperto per la prima volta o quando cambi 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 employee. In caso contrario, lo facciamo noi. La chiamata createIndex è importante. Dobbiamo indicare a IndexedDB quali metodi, oltre alle chiavi, utilizzeremo per recuperare i dati. Ne utilizzeremo una chiamata searchkey. Questo aspetto verrà spiegato tra poco.

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

Prima di proseguire, rivediamo rapidamente cosa sta succedendo qui. Apriamo il database. Controlliamo se il nostro archivio di oggetti esiste. In caso contrario, la creiamo. Infine, chiamiamo una funzione denominata handleSeed. Ora concentriamoci sulla parte della demo relativa al seeding dei dati.

Dammi dei dati!

Come accennato nell'introduzione di questo articolo, questa demo ricrea un'applicazione in stile Intranet che deve archiviare una copia di tutti i dipendenti conosciuti. Normalmente, ciò comporterebbe la creazione di un'API basata su server che potrebbe restituire un conteggio dei dipendenti e fornirci un modo per recuperare batch di record. Puoi immaginare un servizio semplice che supporti un conteggio iniziale e restituisca 100 persone alla volta. Questa operazione potrebbe essere eseguita in modo asincrono in background mentre l'utente sta facendo altro.

Per la nostra demo, facciamo qualcosa di semplice. Vediamo quanti oggetti, se presenti, abbiamo nel nostro IndexedDB. Se il numero è inferiore a una certa soglia, creeremo semplicemente utenti fittizi. In caso contrario, consideriamo completata la parte di seed 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, in quanto abbiamo più operazioni concatenate 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 l'archivio di 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 era pari a zero, avviamo il processo di seeding.

Utilizziamo il div di stato menzionato in precedenza per inviare all'utente un messaggio che indica che inizieremo a recuperare i dati. A causa della natura asincrona di IndexedDB, abbiamo configurato una semplice variabile, done, che monitorerà le aggiunte. Eseguiamo un loop e inseriamo le persone fittizie. L'origine di questa 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"
}

Questo è sufficiente per definire una persona. Tuttavia, abbiamo un requisito speciale per poter eseguire ricerche nei nostri dati. IndexedDB non fornisce un modo per cercare elementi senza distinzione tra maiuscole e minuscole. Pertanto, creiamo una copia del campo lastname in una nuova proprietà, searchkey. Se ricordi, questa è la chiave che abbiamo detto che deve essere creata 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 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 in batch. 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 (pensa a "1 minuto per scrivere 1000 oggetti").

Una volta completato il seed, viene attivata la parte successiva della nostra applicazione: setupAutoComplete.

Creazione del completamento automatico

Ora la parte divertente: il collegamento con il plug-in Autocomplete di jQuery UI. Come per la maggior parte della UI di jQuery, iniziamo con un elemento HTML di base e lo miglioriamo chiamando un metodo costruttore su di esso. Abbiamo astratto l'intero processo 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 Autocomplete 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 a quel 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 la ricerca.

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 gli elementi che corrispondono 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. Tieni presente che il termine viene convertito in minuscolo per corrispondere ai dati in minuscolo inseriti.

Una volta fatto, possiamo aprire un cursore (pensa a questo come all'esecuzione di una query di database) ed eseguire l'iterazione dei 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 del nome formattata correttamente. Restituiamo anche l'intera persona. Scoprirai il motivo a breve. Innanzitutto, ecco uno screenshot del completamento automatico in azione. Utilizziamo il tema Vader per l'interfaccia utente di jQuery.

Questo è sufficiente per restituire i risultati delle corrispondenze di IndexedDB al completamento automatico. Tuttavia, vogliamo anche supportare la visualizzazione dei dettagli della partita quando ne viene selezionata una. Abbiamo specificato un gestore di selezione durante la creazione del completamento automatico che utilizza il modello Handlebars precedente.