Elementi UI di associazione dati con IndexedDB

Raymond Camden
Raymond Camden

Introduzione

IndexedDB è un modo efficace per archiviare i dati sul lato client. Se non l'hai ancora fatto, ti invitiamo a leggere gli utili tutorial su MDN sullo stesso argomento. Questo articolo presuppone una conoscenza di base delle API e delle funzionalità. Anche se non hai mai visto IndexedDB prima d'ora, speriamo che la demo in questo articolo ti dia un'idea di come utilizzarlo.

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 veloce, il database dei dipendenti viene copiato sul computer del cliente e archiviato utilizzando IndexedDB. La demo fornisce semplicemente la ricerca e la visualizzazione in stile completamento automatico del record di un singolo dipendente, ma il bello è che, una volta che i dati sono disponibili sul cliente, possiamo utilizzarli in molti altri modi. Di seguito è riportata una descrizione di base di ciò che deve fare la nostra applicazione.

  1. Dobbiamo configurare e inizializzare un'istanza di IndexedDB. Per la maggior parte, questa procedura è semplice, ma farla funzionare sia in Chrome sia in Firefox si rivela un po' difficile.
  2. Dobbiamo verificare se ci sono 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 crea questi dati e impedire all'utente di utilizzarli fino ad allora. Questa operazione deve essere eseguita una sola volta. La prossima volta che l'utente esegue l'applicazione, non dovrà eseguire questo processo. Una demo più avanzata gestirebbe le operazioni di sincronizzazione tra client e server, ma questa demo è incentrata maggiormente sugli aspetti dell'interfaccia utente.
  3. Quando l'applicazione è pronta, possiamo utilizzare il controllo Autocomplete dell'interfaccia utente di jQuery per eseguire la sincronizzazione con IndexedDB. Mentre il controllo Autocomplete consente di creare elenchi e array di dati di base, dispone di un'API che consente qualsiasi origine dati. Dimostreremo come possiamo utilizzarlo per eseguire la connessione ai nostri dati IndexedDB.

Per iniziare

La demo contiene diverse parti. Per iniziare, 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 stanno a cuore. Il primo è il campo "name" che verrà utilizzato per il completamento automatico. Viene caricato disabilitato e verrà attivato successivamente tramite JavaScript. L'intervallo accanto viene utilizzato durante il seed iniziale per fornire aggiornamenti all'utente. Infine, quando selezioni un dipendente dal suggerimento automatico, verrà utilizzato il div con ID displayEmployee.

Ora diamo un'occhiata a JavaScript. C'è molto da dire, quindi lo faremo passo dopo passo. Il codice completo sarà disponibile alla fine, così potrai vederlo 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 semplici alias per i componenti principali di IndexedDB necessari per la nostra applicazione.

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

Ecco alcune variabili globali che utilizzeremo nel corso della demo:

var db;
var template;

Ora inizieremo con il blocco Documento jQuery pronto:

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

La nostra demo utilizza Handlebars.js per mostrare i dettagli del dipendente. Questo non verrà usato prima, ma possiamo andare avanti e compilare il nostro modello ora e rimuoverlo. Abbiamo un blocco di script impostato come tipo riconosciuto dal manubrio. Non è molto sofisticato, ma semplifica la visualizzazione del codice HTML dinamico.

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

che 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. Per prima cosa, la apriamo.

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

L'apertura di una connessione a IndexedDB ci permette di accedere in lettura e scrittura di 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 necessita di un solo oggetto objectStore che chiamiamo "employee". Quando l'indexDB viene aperto per la prima volta o quando si modifica 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 restricted. In caso contrario, lo facciamo semplicemente. La chiamata createIndex è importante. Dobbiamo indicare a IndexedDB quali metodi, al di fuori delle chiavi, utilizzeremo per recuperare i dati. Utilizzeremo una chiave di ricerca. che verrà spiegato più avanti.

L'evento onungradeneeded verrà eseguito automaticamente alla prima esecuzione dello script. Dopo essere stato eseguito o ignorato nelle esecuzioni future, viene eseguito il gestore onsuccess. Abbiamo definito un gestore di errori semplice (e brutto) e poi chiamiamo handleSeed.

Prima di proseguire, rivediamo rapidamente cosa succede qui. Apriamo il database. Verifichiamo l'esistenza del nostro archivio di oggetti. In caso contrario, lo creiamo. Infine, chiamiamo una funzione chiamata handleSeed. Ora passiamo alla parte della nostra demo dedicata al seeding dei dati.

Fornisci qualche dato

Come accennato nell'introduzione di questo articolo, questa demo ricrea un'applicazione di tipo Intranet che deve archiviare una copia di tutti i dipendenti noti. Normalmente, questa operazione comporta la creazione di un'API basata su server in grado di restituire un conteggio dei dipendenti e di fornire 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 eseguendo altre operazioni.

Per la nostra demo, facciamo qualcosa di semplice. Vediamo quanti oggetti sono presenti nel nostro IndexedDB. Se il numero è inferiore a un determinato numero, creeremo utenti falsi. In caso contrario, la parte originale non è più completa e possiamo attivare la parte del completamento automatico della demo. Diamo un'occhiata all'handle 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 poiché abbiamo più operazioni collegate l'una all'altra, quindi analizziamola:

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

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

objectStore("employee");

Recupera l'archivio di oggetti dei dipendenti.

count()

Esegui l'API di conteggio, che come puoi intuire, esegue un conteggio.

onsuccess = function(e) {

Al termine, esegui il callback. All'interno del callback possiamo ottenere il valore risultato, ovvero il numero di oggetti. Se il conteggio era zero, iniziamo il processo di origine.

Utilizziamo il div di stato menzionato in precedenza per comunicare all'utente che inizieremo a raccogliere dati. Data la natura asincrona di IndexedDB, abbiamo impostato una semplice variabile che tiene traccia delle aggiunte. Facciamo un giro e inseriamo le persone false. 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 da solo è sufficiente per definire una persona. Ma abbiamo un requisito speciale per poter cercare i nostri dati. IndexedDB non fornisce un modo per cercare gli elementi senza distinzione tra maiuscole e minuscole. Di conseguenza, creiamo una copia del campo lastname in una nuova proprietà, searchkey. Come ricorderai, questa è la chiave da 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, la modifica viene eseguita qui anziché sul server back-end (o, nel nostro caso, sul server back-end immaginario).

Per eseguire l'aggiunta del 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 e questo renderà le tue prestazioni terribili quando aggiungi molti elementi (pensa "1 minuto per scrivere un 1000 oggetti"-terribile).

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

Creazione del completamento automatico

E ora la parte divertente: collegare il plug-in Autocomplete dell'UI di jQuery. Come per la maggior parte dell'interfaccia utente di jQuery, iniziamo con un elemento HTML di base e lo miglioriamo richiamandovi un metodo di costruzione. Abbiamo estrapolato l'intero processo in una funzione chiamata setupAutoComplete. 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à sorgente. Il controllo Autocomplete dell'interfaccia utente di jQuery ti consente di definire una proprietà sorgente 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. Sei responsabile dell'invio di un array di risultati a quel callback.

La prima cosa da fare è nascondere il div displayEmployee. Viene utilizzato per visualizzare un singolo dipendente e, se ne è già stato caricato uno, per eliminarlo. Ora possiamo iniziare a cercare.

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

Per trovare gli elementi che corrispondono al nostro input, usiamo un suggerimento dell'utente di StackOverflow Fong-Wan Chau: utilizziamo un intervallo di indice basato sull'input come limite inferiore e l'input più la lettera z come limite dell'intervallo superiore. Inoltre, tieni presente che il termine viene utilizzato in minuscolo in modo che corrisponda ai dati in minuscolo che abbiamo inserito.

Al termine, possiamo aprire un cursore (pensiamo ad eseguire una query di database) ed eseguire l'iterazione dei risultati. Il controllo di completamento automatico dell'interfaccia utente di jQuery 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 tra un secondo. Innanzitutto, ecco uno screenshot del completamento automatico in azione. Stiamo usando il tema Vader per l'interfaccia utente di jQuery.

Questo è da solo, questo è sufficiente per restituire i risultati delle nostre corrispondenze IndexedDB al completamento automatico. Tuttavia, vogliamo anche supportare la visualizzazione di una vista 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 di prima.