Datenbindungs-UI-Elemente mit IndexedDB

Raymond Camden
Raymond Camden

Einleitung

IndexedDB ist eine leistungsstarke Möglichkeit, Daten auf der Clientseite zu speichern. Falls du dich das noch nicht angesehen hast, empfehlen wir dir, die hilfreichen MDN-Anleitungen zu diesem Thema zu lesen. In diesem Artikel wird vorausgesetzt, dass Sie mit den APIs und Funktionen vertraut sind. Auch wenn Sie IndexedDB noch nicht gesehen haben, erhalten Sie in der Demo in diesem Artikel hoffentlich einen Eindruck davon, was damit möglich ist.

Unsere Demo ist ein einfaches Proof of Concept für ein Intranet-Anwendungsbeispiel für ein Unternehmen. Mit der Anwendung können Mitarbeiter nach anderen Mitarbeitern suchen. Für eine schnellere und schnellere Nutzung wird die Mitarbeiterdatenbank auf den Computer des Kunden kopiert und mit IndexedDB gespeichert. Die Demo bietet einfach eine automatische Vervollständigung beim Suchen und Anzeigen eines einzelnen Mitarbeiterdatensatzes. Das Tolle ist jedoch, dass wir diese Daten, sobald sie auf dem Client verfügbar sind, auch auf andere Weise verwenden können. Hier ist ein grundlegender Überblick darüber, was unsere Anwendung tun muss.

  1. Wir müssen eine Instanz einer IndexedDB einrichten und initialisieren. Das ist in den meisten Fällen einfach, aber es ist etwas kompliziert, sowohl in Chrome als auch in Firefox funktionieren zu können.
  2. Wir müssen prüfen, ob wir Daten haben, und wenn nicht, laden wir sie herunter. Dies erfolgt normalerweise über AJAX-Aufrufe. Für unsere Demo haben wir eine einfache Dienstprogrammklasse erstellt, um schnell gefälschte Daten zu generieren. Die Anwendung muss erkennen, wann sie diese Daten erstellt, und den Benutzer daran hindern, die Daten bis dahin zu verwenden. Dies ist ein einmaliger Vorgang. Wenn der Nutzer die Anwendung das nächste Mal ausführt, muss er diesen Prozess nicht durchlaufen. In einer komplexeren Demo würden Synchronisierungsvorgänge zwischen dem Client und dem Server durchgeführt. In dieser Demo liegt der Schwerpunkt jedoch mehr auf den Aspekten der Benutzeroberfläche.
  3. Wenn die Anwendung bereit ist, können wir das Autocomplete-Steuerelement der jQuery-Benutzeroberfläche verwenden, um eine Synchronisierung mit der IndexedDB durchzuführen. Das Steuerelement „Autocomplete“ ermöglicht zwar grundlegende Listen und Datenarrays, verfügt aber über eine API, die alle Datenquellen ermöglicht. Wir zeigen Ihnen, wie wir damit eine Verbindung zu unseren IndexedDB-Daten herstellen können.

Erste Schritte

Diese Demo besteht aus mehreren Teilen, sehen wir uns also zunächst 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? Es gibt drei Hauptaspekte dieser Benutzeroberfläche, die uns wichtig sind. Zuerst das Feld "name", das für die automatische Vervollständigung verwendet wird. Das Laden ist deaktiviert und wird später über JavaScript aktiviert. Der nebenstehende Span wird während der anfänglichen Startphase verwendet, um dem Nutzer Aktualisierungen bereitzustellen. Schließlich wird das div-Element mit der ID displayEmployee verwendet, wenn Sie einen Mitarbeiter aus dem automatischen Vorschlag auswählen.

Werfen wir nun einen Blick auf JavaScript. Es gibt hier viel zu verarbeiten. Gehen wir das einmal Schritt für Schritt vor. Der vollständige Code steht am Ende zur Verfügung, sodass Sie ihn vollständig sehen können.

Zunächst: Es gibt einige Präfixprobleme bei den Browsern, die IndexedDB unterstützen. Hier sehen Sie Code aus der Mozilla-Dokumentation, der modifiziert wurde, um einfache Aliase für die IndexedDB-Kernkomponenten bereitzustellen, die unsere Anwendung benötigt.

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

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

var db;
var template;

Beginnen wir jetzt mit dem jQuery-Block „Bereit“ für das Dokument:

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

In unserer Demo werden mit Handlebars.js die Mitarbeiterdetails angezeigt. Das wird erst später verwendet, aber wir können unsere Vorlage jetzt kompilieren und entfernen. Wir haben einen Skriptblock als von Lenken erkannten Typ eingerichtet. Es ist nicht extrem schick, erleichtert aber die Anzeige des dynamischen HTML-Codes.

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

Dies wird dann wie folgt wieder in unserem JavaScript kompiliert:

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

Beginnen wir nun mit unserer IndexedDB. Zuerst öffnen wir sie.

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

Durch Öffnen einer Verbindung zur IndexedDB können wir Daten lesen und schreiben. Vorher müssen Sie jedoch prüfen, ob ein ObjektStore vorhanden ist. Ein „objectStore“ ist wie eine Datenbanktabelle. Eine IndexedDB kann viele Objektspeicher haben, von denen jeder eine Sammlung verwandter Objekte enthält. Unsere Demo ist einfach und benötigt nur einen ObjectStore, den wir „employee“ nennen. Wenn die indexierteDB zum ersten Mal geöffnet wird oder Sie die Version im Code ändern, wird bei einem Upgrade ein Ereignis ausgeführt, das erforderlich ist. Damit können Sie den „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 von Objektspeichern, um festzustellen, ob es „employee“ enthält. Falls nicht, nehmen wir das einfach an. Der Aufruf createIndex ist wichtig. Wir müssen IndexedDB mitteilen, welche Methoden außer Schlüssel zum Abrufen von Daten verwendet werden. Wir verwenden einen namens searchkey. Dies wird später erläutert.

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

Bevor wir weitermachen, schauen wir uns kurz an, was hier passiert. Wir öffnen die Datenbank. Wir prüfen, ob unser Objektspeicher vorhanden ist. Wenn nicht, erstellen wir sie. Schließlich rufen wir eine Funktion namens handleSeed auf. Konzentrieren wir uns nun auf den Teil unserer Demo zur Datenseeding.

Gib mir Daten!

Wie bereits in der Einleitung dieses Artikels erwähnt, wird bei dieser Demo eine Anwendung im Intranet-Stil neu erstellt, die eine Kopie aller bekannten Mitarbeiter speichern muss. Normalerweise würde dies die Erstellung einer serverbasierten API beinhalten, die eine Anzahl von Mitarbeitern zurückgeben und uns die Möglichkeit bietet, Datensätze in Batches abzurufen. Stellen Sie sich einen einfachen Dienst vor, der eine Startzählung unterstützt und 100 Personen auf einmal zurückgibt. Dieser könnte asynchron im Hintergrund ausgeführt werden, während der Nutzer andere Dinge erledigt.

In unserer Demo machen wir etwas Einfaches. Wir sehen, wie viele Objekte gegebenenfalls in unserer IndexedDB vorhanden sind. Liegt dieser Wert unter einer bestimmten Anzahl, erstellen wir einfach gefälschte Nutzer. Andernfalls gilt der Quellteil als erledigt und wir können die automatische 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 aneinander verkettet sind. Schauen wir uns das genauer an:

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

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

objectStore("employee");

Rufen Sie den Employee Object Store 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ühren Sie anschließend diesen Callback aus. Innerhalb des Callbacks können wir den Ergebniswert abrufen, also die Anzahl der Objekte. Wenn die Anzahl Null war, beginnen wir mit dem Seed-Prozess.

Mit dem bereits erwähnten „div“-Status teilen wir dem Nutzer mit, dass wir Daten abrufen werden. Da IndexedDB asynchron ist, haben wir eine einfache Variable eingerichtet, die Ergänzungen verfolgt. Wir sehen uns an und stellen die gefälschten Leute 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. Für das Durchsuchen unserer Daten gelten jedoch besondere Anforderungen. IndexedDB bietet keine Möglichkeit, Elemente nachzuschlagen, bei denen die Groß-/Kleinschreibung nicht berücksichtigt wird. Daher erstellen wir eine Kopie des Felds „lastname“ in die neue Eigenschaft „searchkey“. Wie Sie sich erinnern, ist dies der Schlüssel, von dem wir gesagt haben, dass er als Index für unsere Daten erstellt werden sollte.

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

Da dies eine clientspezifische Änderung ist, erfolgt sie hier und nicht auf dem Backend-Server (oder in unserem Fall auf dem imaginären Backend-Server).

Um die Datenbank-Additionen leistungsfähig auszuführen, sollten Sie die Transaktion für alle Batch-Schreibvorgänge wiederverwenden. Wenn Sie für jeden Schreibvorgang eine neue Transaktion erstellen, führt der Browser möglicherweise zu einem Schreibvorgang auf der Festplatte, und das kann sich negativ auf die Leistung auswirken, wenn Sie viele Elemente hinzufügen.

Sobald der Startvorgang abgeschlossen ist, wird der nächste Teil unserer Anwendung ausgelöst: „setupAutoComplete“.

Automatische Vervollständigung erstellen

Nun kommen wir zum spannenden Teil – zur Einrichtung des Plug-ins Autocomplete in der jQuery-UI. Wie bei den meisten jQuery-Benutzeroberflächen beginnen wir mit einem einfachen HTML-Element und verbessern es, indem wir dafür eine Konstruktormethode aufrufen. 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 der jQuery-Benutzeroberfläche können Sie eine Quelleigenschaft definieren, die an alle möglichen Anforderungen angepasst werden kann – auch für unsere IndexedDB-Daten. Die API stellt die Anfrage (also im Grunde, was in das Formularfeld eingegeben wurde) und einen Antwort-Callback bereit. Sie sind dafür verantwortlich, ein Array von Ergebnissen zurück an diesen Callback zu senden.

Zuerst blenden wir das div-Element displayEmployee aus. Damit wird ein einzelner Mitarbeiter angezeigt und, falls er zuvor geladen wurde, ihn gelöscht. Jetzt können wir mit der Suche beginnen.

Zunächst erstellen wir eine schreibgeschützte Transaktion, ein Array namens result und einen oncomplete-Handler, der das Ergebnis einfach an das Autocomplete-Steuerelement übergibt.

Um Elemente zu finden, die mit unserer Eingabe übereinstimmen, verwenden wir einen Tipp des StackOverflow-Nutzers Fong-Wan Chau: Wir verwenden einen Indexbereich, der auf der Eingabe als Untergrenze und der Eingabe plus dem Buchstaben Z als Obergrenze des Bereichs basiert. Beachten Sie auch, dass der Begriff in Kleinbuchstaben geschrieben wurde, damit er mit den eingegebenen Daten übereinstimmt.

Anschließend können wir einen Cursor öffnen (eine Art Datenbankabfrage ausführen) und über die Ergebnisse iterieren. Das Steuerelement zur automatischen Vervollständigung der jQuery-Benutzeroberfläche ermöglicht die Rückgabe beliebiger Datentypen, erfordert jedoch mindestens einen Wertschlüssel. Wir setzen den Wert auf eine gut formatierte Version des Namens. Außerdem wird die gesamte Person zurückgegeben. Den Grund dafür wissen Sie gleich. Hier sehen Sie einen Screenshot der automatischen Vervollständigung in Aktion. Wir verwenden das Vader-Design für die jQuery-Benutzeroberfläche.

Dies allein reicht aus, um die Ergebnisse der IndexedDB-Übereinstimmungen an die automatische Vervollständigung zurückzugeben. Wir möchten aber auch die Anzeige einer Detailansicht der Übereinstimmung unterstützen, wenn eine ausgewählt wird. Beim Erstellen der automatischen Vervollständigung haben wir einen Auswahl-Handler angegeben, der die zuvor erwähnte Vorlage für die Lenkerleiste verwendet.