Datenbindungs-UI-Elemente mit IndexedDB

Raymond Camden
Raymond Camden

Einführung

IndexedDB ist eine leistungsstarke Methode zum Speichern von Daten auf der Clientseite. Falls Sie es noch nicht getan haben, sollten Sie sich die hilfreichen MDN-Anleitungen zu diesem Thema ansehen. In diesem Artikel werden Grundkenntnisse zu den APIs und Funktionen vorausgesetzt. Auch wenn Sie IndexedDB noch nicht kennen, gibt Ihnen die Demo in diesem Artikel hoffentlich einen Eindruck davon, was damit möglich ist.

Unsere Demo ist eine einfache Intranet-Anwendung für ein Unternehmen. Mit der App können Mitarbeiter nach anderen Mitarbeitern suchen. Für eine schnellere und flüssigere Nutzung wird die Mitarbeiterdatenbank auf den Computer des Kunden kopiert und mit IndexedDB gespeichert. Die Demo bietet einfach eine Suchfunktion im Autocomplete-Stil und die Anzeige eines einzelnen Mitarbeitereintrags. Das Schöne ist jedoch, dass wir diese Daten, sobald sie auf dem Client verfügbar sind, auch auf verschiedene andere Arten verwenden können. Hier ist ein grundlegender Überblick über die Anforderungen an unsere Anwendung.

  1. Wir müssen eine Instanz einer IndexedDB einrichten und initialisieren. Das ist größtenteils unkompliziert, aber es ist etwas schwierig, die Funktion sowohl in Chrome als auch in Firefox zu nutzen.
  2. Wir müssen prüfen, ob wir Daten haben, und sie gegebenenfalls herunterladen. Normalerweise geschieht dies über AJAX-Aufrufe. Für unsere Demo haben wir eine einfache Dienstprogrammklasse erstellt, mit der sich schnell Fake-Daten generieren lassen. Die Anwendung muss erkennen, wann diese Daten erstellt werden, und den Nutzer bis dahin daran hindern, sie zu verwenden. Dieser Vorgang ist nur einmal erforderlich. Wenn der Nutzer die Anwendung das nächste Mal ausführt, muss dieser Vorgang nicht noch einmal durchgeführt werden. Eine erweiterte Demo würde Synchronisierungsvorgänge zwischen dem Client und dem Server verarbeiten, aber diese Demo konzentriert sich mehr auf die UI-Aspekte.
  3. Wenn die Anwendung fertig ist, können wir das Autocomplete-Steuerelement von jQuery UI verwenden, um sie mit IndexedDB zu synchronisieren. Das Autocomplete-Steuerelement unterstützt zwar einfache Listen und Datenarrays, verfügt aber über eine API, die jede Datenquelle zulässt. Wir zeigen, wie wir damit eine Verbindung zu unseren IndexedDB-Daten herstellen können.

Erste Schritte

Diese Demo besteht aus mehreren Teilen. Sehen wir uns zuerst den HTML-Teil an.

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

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

Nicht viel, oder? Bei dieser Benutzeroberfläche geht es um drei Hauptaspekte. Das erste ist das Feld „name“, das für die automatische Vervollständigung verwendet wird. Sie wird deaktiviert geladen und später über JavaScript aktiviert. Die daneben stehende Spanne wird beim ersten Seeding verwendet, um dem Nutzer Updates zur Verfügung zu stellen. Das div-Element mit der ID „displayEmployee“ wird schließlich verwendet, wenn Sie einen Mitarbeiter aus der automatischen Vervollständigung auswählen.

Sehen wir uns nun das JavaScript an. Es gibt hier viel zu verdauen, deshalb gehen wir Schritt für Schritt vor. Der vollständige Code wird am Ende angezeigt, damit Sie ihn in seiner Gesamtheit sehen können.

Zuerst: Bei den Browsern, die IndexedDB unterstützen, gibt es einige Präfixprobleme, die wir berücksichtigen müssen. Hier ist ein Code aus der Mozilla-Dokumentation, der so geändert wurde, dass einfache Aliasse für die wichtigsten IndexedDB-Komponenten unserer Anwendung bereitgestellt werden.

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

Als Nächstes einige globale Variablen, die wir in der Demo verwenden werden:

var db;
var template;

Beginnen wir mit dem jQuery-Block „document ready“:

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

In unserer Demo werden die Mitarbeiterdetails mit Handlebars.js angezeigt. Dieser wird erst später verwendet, aber wir können unsere Vorlage jetzt kompilieren und aus dem Weg räumen. Wir haben einen Scriptblock als von Handlebars unterstützten Typ eingerichtet. Es ist nicht besonders schick, aber es erleichtert die Anzeige des dynamischen HTML-Codes.

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

Das wird dann so in unserem JavaScript-Code kompiliert:

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

Jetzt können wir mit IndexedDB arbeiten. Zuerst öffnen wir sie.

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

Wenn wir eine Verbindung zur IndexedDB öffnen, erhalten wir Zugriff auf Lese- und Schreibdaten. Bevor wir dies tun können, müssen wir jedoch einen ObjectStore haben. Ein Objektspeicher ist wie eine Datenbanktabelle. Eine IndexedDB kann viele ObjectStores haben, von denen jeder eine Sammlung verwandter Objekte enthält. Unsere Demo ist einfach und benötigt nur einen ObjectStore namens „employee“. Wenn die IndexedDB zum ersten Mal geöffnet wird oder Sie die Version im Code ändern, wird das Ereignis „onupgradeneeded“ ausgeführt. Damit können wir unseren ObjectStore einrichten.

// 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
();
};

Im onupgradeneeded-Ereignishandler-Block prüfen wir, ob das Array „objectStoreNames“ (Objektspeichernamen) den Wert „employee“ (Mitarbeiter) enthält. Andernfalls sorgen wir einfach dafür, dass dies geschieht. Der createIndex-Aufruf ist wichtig. Wir müssen IndexedDB mitteilen, welche Methoden wir zusätzlich zu Schlüsseln zum Abrufen von Daten verwenden. Wir verwenden den Namen „searchkey“. Das wird gleich erklärt.

Das Ereignis onungradeneeded wird beim ersten Ausführen des Scripts automatisch ausgeführt. Nachdem der Code ausgeführt oder bei zukünftigen Ausführungen übersprungen wurde, wird der onsuccess-Handler ausgeführt. Wir haben einen einfachen (und hässlichen) Fehlerhandler definiert und rufen dann handleSeed auf.

Bevor wir fortfahren, lassen Sie uns kurz noch einmal zusammenfassen, was hier passiert. Öffnen Sie die Datenbank. Wir prüfen, ob unser Objektspeicher vorhanden ist. Andernfalls erstellen wir sie. Schließlich wird eine Funktion namens „handleSeed“ aufgerufen. Sehen wir uns nun den Teil der Demo zum Seeding von Daten an.

Gib mir Daten!

Wie in der Einführung dieses Artikels erwähnt, wird in dieser Demo eine Intranet-Anwendung nachgebildet, in der eine Kopie aller bekannten Mitarbeiter gespeichert werden muss. Normalerweise würde das Erstellen einer serverbasierten API erfordern, die eine Mitarbeiteranzahl zurückgeben und uns die Möglichkeit bieten, Stapel von Datensätzen abzurufen. Stellen Sie sich einen einfachen Dienst vor, der eine Startanzahl unterstützt und jeweils 100 Personen zurückgibt. Dies kann asynchron im Hintergrund ausgeführt werden, während der Nutzer andere Dinge tut.

Für unsere Demo machen wir etwas Einfaches. Wir sehen, ob und wie viele Objekte wir in unserer IndexedDB haben. Wenn diese Zahl unterschritten wird, erstellen wir einfach Fake-Nutzer. Andernfalls sind wir mit dem Seed-Teil fertig und können den Teil der Demo mit der automatischen Vervollständigung aktivieren. Sehen wir uns handleSeed an.

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
();
   
}
 
};
}

Die erste Zeile ist etwas komplex, da mehrere Vorgänge miteinander verkettet sind. Sehen wir uns das genauer an:

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

Dadurch wird eine neue schreibgeschützte Transaktion erstellt. Alle Datenvorgänge mit IndexedDB erfordern eine Art Transaktion.

objectStore("employee");

Rufen Sie den Mitarbeiterobjektspeicher ab.

count()

Führen Sie die „count“-API aus, die wie Sie sich denken können, eine Zählung durchführt.

onsuccess = function(e) {

Führe nach Abschluss diesen Rückruf aus. Im Callback können wir den Ergebniswert abrufen, also die Anzahl der Objekte. Wenn die Anzahl null war, beginnen wir mit dem Seeding.

Wir verwenden das bereits erwähnte Status-Div, um dem Nutzer mitzuteilen, dass wir jetzt Daten abrufen. Aufgrund der asynchronen Natur von IndexedDB haben wir eine einfache Variable namens „done“ eingerichtet, mit der Ergänzungen erfasst werden. Wir wiederholen den Vorgang und fügen die gefälschten Personen ein. Die Quelle dieser Funktion ist im Download verfügbar, sie gibt jedoch ein Objekt zurück, das so aussieht:

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

Das allein reicht aus, um eine Person zu definieren. Es gibt jedoch eine spezielle Anforderung, damit wir in unseren Daten suchen können. IndexedDB bietet keine Möglichkeit, Elemente unabhängig von der Groß- und Kleinschreibung abzurufen. Daher kopieren wir das Feld „Nachname“ in eine neue Property namens „Suchschlüssel“. Das ist der Schlüssel, der als Index für unsere Daten erstellt werden sollte.

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

Da es sich um eine clientspezifische Änderung handelt, wird sie hier und nicht auf dem Back-End-Server (oder in unserem Fall dem imaginären Back-End-Server) vorgenommen.

Um die Datenbankeinträge effizient auszuführen, sollten Sie die Transaktion für alle Batch-Schreibvorgänge wiederverwenden. Wenn Sie für jeden Schreibvorgang eine neue Transaktion erstellen, kann der Browser für jede Transaktion einen Laufwerkschreibvorgang auslösen. Das beeinträchtigt die Leistung beim Hinzufügen vieler Elemente erheblich (z. B. 1 Minute für das Schreiben von 1.000 Objekten).

Sobald der Seed fertig ist, wird der nächste Teil unserer Anwendung ausgeführt: setupAutoComplete.

Autocomplete erstellen

Jetzt kommt der lustige Teil: die Verknüpfung mit dem Autocomplete-Plug-in von jQuery UI. Wie bei den meisten jQuery-UI-Elementen beginnen wir mit einem einfachen HTML-Element und erweitern es durch Aufruf einer Konstruktormethode. Wir haben den gesamten Prozess in einer Funktion namens setupAutoComplete abstrahiert. Sehen wir uns diesen Code an.

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));
   
}
 
});

}

Der komplexeste Teil dieses Codes ist das Erstellen der Quell-Property. Mit dem Autocomplete-Steuerelement von jQuery UI können Sie eine Quelleigenschaft definieren, die an alle Anforderungen angepasst werden kann – auch an unsere IndexedDB-Daten. Die API stellt Ihnen die Anfrage (im Grunde das, was in das Formularfeld eingegeben wurde) und einen Rückruf für die Antwort bereit. Sie sind dafür verantwortlich, ein Ergebnisarray an diesen Callback zurückzugeben.

Zuerst verbergen wir das Div-Element „displayEmployee“. Es wird verwendet, um einen einzelnen Mitarbeiter anzuzeigen und, falls bereits einer geladen wurde, diesen zu löschen. Jetzt können wir mit der Suche beginnen.

Zuerst erstellen wir eine schreibgeschützte Transaktion, ein Array namens „result“ und einen „oncomplete“-Handler, der das Ergebnis einfach an das Autocomplete-Steuerelement weitergibt.

Um Elemente zu finden, die mit unserer Eingabe übereinstimmen, nutzen wir einen Tipp des StackOverflow-Nutzers Fong-Wan Chau: Wir verwenden einen Indexbereich, der auf der Eingabe basiert, als untere Endgrenze und die Eingabe plus den Buchstaben „z“ als obere Endgrenze. Beachten Sie auch, dass wir den Begriff in Kleinbuchstaben schreiben, damit er zu den eingegebenen Daten in Kleinbuchstaben passt.

Danach können wir einen Cursor öffnen (denken Sie dabei an eine Datenbankabfrage) und die Ergebnisse durchgehen. Mit dem Autocomplete-Steuerelement von jQuery UI können Sie beliebige Datentypen zurückgeben, benötigen aber mindestens einen Wertschlüssel. Wir legen den Wert auf eine gut formatierte Version des Namens fest. Wir geben auch die gesamte Person zurück. Das sehen Sie gleich. Hier ist ein Screenshot der automatischen Vervollständigung in Aktion. Wir verwenden das Vader-Design für jQuery UI.

Das reicht aus, um die Ergebnisse unserer IndexedDB-Übereinstimmungen an die automatische Vervollständigung zurückzugeben. Wir möchten aber auch eine Detailansicht des Spiels anzeigen, wenn eines ausgewählt ist. Beim Erstellen des Autocomplete-Elements haben wir einen Select-Handler angegeben, der die Handlebars-Vorlage aus dem vorherigen Beispiel verwendet.