Datenbindungs-UI-Elemente mit IndexedDB

Raymond Camden
Raymond Camden

Einführung

IndexedDB ist eine leistungsstarke Methode, Daten clientseitig zu speichern. Falls du sie dir noch nicht angesehen hast, empfehlen wir dir, die hilfreichen MDN-Anleitungen zu diesem Thema zu lesen. 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 eine Vorstellung davon, was damit möglich ist.

Unsere Demo ist eine einfache Intranet-Anwendung für ein Unternehmen. Die Anwendung ermöglicht Mitarbeitern die Suche nach anderen Mitarbeitern. 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 IndexedDB-Instanz 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. In der Regel 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. Dies ist ein einmaliger Vorgang. 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 der jQuery-UI verwenden, um eine Synchronisierung mit der IndexedDB durchzuführen. 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.

Bei den Browsern, die IndexedDB unterstützen, müssen wir uns Gedanken über einige Präfixprobleme machen. 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 Skriptblock eingerichtet, der einen vom Handlebars erkannten Typ aufweist. 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 Event-Handler-Block onupgradeneeded prüfen wir „objectStoreNames“, ein Array mit Objektspeichern, um festzustellen, ob es „employee“ 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.

Lassen Sie uns kurz zusammenfassen, was hier vor sich geht. Öffnen Sie die Datenbank. Wir prüfen, ob unser Objektspeicher vorhanden ist. Ist dies nicht der Fall, erstellen wir es. Schließlich rufen wir eine Funktion namens handleSeed auf. Wenden wir uns nun dem Daten-Seeding-Teil unserer Demo zu.

Gib mir Daten!

Wie bereits in der Einleitung dieses Artikels erwähnt, handelt es sich bei dieser Demo um die Neuerstellung einer Anwendung im Intranetstil, die eine Kopie aller bekannten Mitarbeiter speichern 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 Startzählung unterstützt und 100 Personen gleichzeitig zurückgibt. Dies könnte asynchron im Hintergrund ausgeführt werden, während der Nutzer andere Vorgänge ausführt.

Für unsere Demo machen wir etwas Einfaches. Wir sehen, wie viele Objekte wir in unserer IndexedDB haben (falls vorhanden). Unter einer bestimmten Anzahl erstellen wir einfach Fake-Nutzer. Andernfalls sind wir mit dem Seed-Teil fertig und können den Teil der automatischen Vervollständigung in der Demo 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 Employee-Objektspeicher ab.

count()

Führen Sie, wie Sie raten können, die Count API aus.

onsuccess = function(e) {

Führen Sie anschließend diesen Callback aus. Innerhalb des Callbacks können wir den Ergebniswert abrufen, der die Anzahl der Objekte darstellt. Wenn die Anzahl null war, beginnen wir mit dem Seed-Prozess.

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, gibt jedoch ein Objekt zurück, das wie folgt 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. Wir haben jedoch eine besondere Anforderung, um unsere Daten durchsuchen zu 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 aufgerufen: setupAutoComplete.

Autocomplete erstellen

Nun zum unterhaltsamen Teil – Sie können das Autocomplete-Plug-in der jQuery-UI verwenden. 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.

Zunächst erstellen wir eine schreibgeschützte Transaktion, ein Array mit dem Namen result und einen oncomplete-Handler, der das Ergebnis einfach an das Steuerelement für die automatische Vervollständigung übergibt.

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 werden Sie gleich sehen. 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. Es soll aber auch eine Detailansicht der Übereinstimmung angezeigt werden, wenn eine ausgewählt wird. Beim Erstellen der automatischen Vervollständigung, die die zuvor verwendete Vorlage für die Handlebars verwendet, haben wir einen Auswahl-Handler angegeben.