Einführung
IndexedDB ist eine leistungsstarke Möglichkeit, Daten auf der Clientseite zu speichern. Falls Sie es noch nicht getan haben, empfehlen wir Ihnen, die hilfreichen MDN-Anleitungen zu diesem Thema zu lesen. In diesem Artikel werden grundlegende Kenntnisse der APIs und Funktionen vorausgesetzt. Auch wenn Sie IndexedDB noch nicht kennen, sollte Ihnen die Demo in diesem Artikel eine Vorstellung davon geben, was damit möglich ist.
Unsere Demo ist eine einfache Proof of Concept-Intranetanwendung für ein Unternehmen. Mit der Anwendung können Mitarbeiter nach anderen Mitarbeitern suchen. Damit die Anwendung schneller und reaktionsschneller ist, wird die Mitarbeiterdatenbank auf den Computer des Clients kopiert und mit IndexedDB gespeichert. Die Demo bietet lediglich eine Suche im Stil der automatischen Vervollständigung und die Anzeige eines einzelnen Mitarbeiterdatensatzes. 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 darüber, was unsere Anwendung tun muss.
- Wir müssen eine Instanz einer IndexedDB einrichten und initialisieren. Das ist größtenteils unkompliziert, aber es ist etwas knifflig, es sowohl in Chrome als auch in Firefox zum Laufen zu bringen.
- Wir müssen prüfen, ob wir Daten haben, und wenn nicht, sie herunterladen. Normalerweise würde dies über AJAX-Aufrufe erfolgen. Für unsere Demo haben wir eine einfache Hilfsklasse erstellt, um schnell fiktive Daten zu generieren. Die Anwendung muss erkennen, wann diese Daten erstellt werden, und den Nutzer daran hindern, die Daten bis dahin zu verwenden. Das ist jedoch nur einmal erforderlich. Wenn der Nutzer die Anwendung das nächste Mal ausführt, muss er diesen Vorgang nicht noch einmal durchlaufen. In einer komplexeren Demo würden Synchronisierungsvorgänge zwischen Client und Server behandelt, aber in dieser Demo geht es mehr um die UI-Aspekte.
- Wenn die Anwendung bereit ist, können wir das Autocomplete-Steuerelement von jQuery UI verwenden, um die Synchronisierung mit der IndexedDB durchzuführen. Das Autocomplete-Steuerelement ermöglicht zwar einfache Listen und Arrays von Daten, verfügt aber über eine API, die jede Datenquelle zulässt. Wir zeigen Ihnen, wie Sie damit eine Verbindung zu Ihren IndexedDB-Daten herstellen können.
Erste Schritte
Diese Demo besteht aus mehreren Teilen. Beginnen wir mit dem HTML-Teil.
<form>
<p>
<label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
</p>
</form>
<div id="displayEmployee"></div>
Nicht viel, oder? Diese Benutzeroberfläche hat drei Hauptaspekte, die für uns wichtig sind. Zuerst das Feld „name“, das für die automatische Vervollständigung verwendet wird. Sie wird deaktiviert geladen und später über JavaScript aktiviert. Der Bereich daneben wird während des ersten Seedings verwendet, um dem Nutzer Updates bereitzustellen. Das div-Element mit der ID „displayEmployee“ wird verwendet, wenn Sie einen Mitarbeiter aus den automatischen Vorschlägen auswählen.
Sehen wir uns nun den JavaScript-Code an. Das sind viele Informationen, die wir Schritt für Schritt durchgehen. Der vollständige Code wird am Ende angezeigt, damit Sie ihn in seiner Gesamtheit sehen können.
Zunächst gibt es einige Präfixprobleme, die wir bei den Browsern, die IndexedDB unterstützen, berücksichtigen müssen. Hier ist ein Code aus der Mozilla-Dokumentation, der so geändert wurde, dass er einfache Aliasse für die wichtigsten IndexedDB-Komponenten bereitstellt, 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 einige globale Variablen, die wir in der Demo verwenden werden:
var db;
var template;
Wir beginnen mit dem jQuery-Block „document ready“:
$(document).ready(function() {
console.log("Startup...");
...
});
In unserer Demo wird Handlebars.js verwendet, um die Mitarbeiterdetails anzuzeigen. Diese wird erst später verwendet, aber wir können die Vorlage jetzt kompilieren und uns dann anderen Dingen widmen. Wir haben einen Skriptblock als Handlebars-erkannten Typ eingerichtet. Es ist nicht besonders ausgefallen, erleichtert aber die Anzeige des dynamischen HTML-Codes.
<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>
Dies wird dann so in unser JavaScript zurückkompiliert:
//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);
Jetzt können wir mit der IndexedDB arbeiten. Zuerst öffnen wir sie.
var openRequest = indexedDB.open("employees", 1);
Wenn wir eine Verbindung zur IndexedDB öffnen, können wir Daten lesen und schreiben. Vorher müssen wir aber sicherstellen, dass wir einen objectStore haben. Ein objectStore ist wie eine Datenbanktabelle. Eine IndexedDB kann viele objectStores enthalten, die jeweils eine Sammlung verwandter Objekte enthalten. Unsere Demo ist einfach und benötigt nur einen objectStore, den wir „employee“ nennen. Wenn die indexedDB zum ersten Mal geöffnet wird oder wenn Sie die Version im Code ändern, wird ein 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 Ereignis-Handler-Block onupgradeneeded prüfen wir, ob objectStoreNames, ein Array von Objektspeichern, employee enthält. Falls nicht, sorgen wir einfach dafür. Der Aufruf „createIndex“ ist wichtig. Wir müssen IndexedDB mitteilen, welche Methoden wir außer Schlüsseln zum Abrufen von Daten verwenden. Wir verwenden einen mit dem Namen „searchkey“. Dies wird gleich noch genauer erläutert.
Das Ereignis onungradeneeded wird automatisch ausgeführt, wenn wir das Script zum ersten Mal ausführen. Nachdem er ausgeführt oder bei zukünftigen Ausführungen übersprungen wurde, wird der onsuccess-Handler ausgeführt. Wir haben einen einfachen (und unschönen) Fehler-Handler definiert und rufen dann handleSeed auf.
Bevor wir fortfahren, sehen wir uns kurz an, was hier passiert. Wir öffnen die Datenbank. Wir prüfen, ob der Objektspeicher vorhanden ist. Andernfalls erstellen wir sie. Schließlich rufen wir eine Funktion namens handleSeed auf. Kommen wir nun zum Teil der Demo, in dem es um das Seeding von Daten geht.
Ich brauche Daten!
Wie in der Einleitung dieses Artikels erwähnt, wird in dieser Demo eine Intranet-Anwendung nachgebildet, in der eine Kopie aller bekannten Mitarbeiter gespeichert werden muss. Normalerweise müssten wir dazu eine serverbasierte API erstellen, die die Anzahl der Mitarbeiter zurückgibt und es uns ermöglicht, Datensätze in Batches abzurufen. Stellen Sie sich einen einfachen Dienst vor, der eine Startanzahl unterstützt und jeweils 100 Personen zurückgibt. Dies könnte asynchron im Hintergrund ausgeführt werden, während der Nutzer andere Dinge tut.
Für unsere Demo machen wir etwas Einfaches. Wir sehen, wie viele Objekte sich in unserer IndexedDB befinden. Wenn die Anzahl unter einem bestimmten Wert liegt, erstellen wir einfach Fake-Nutzer. Andernfalls gilt der Seed-Teil als abgeschlossen und wir 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 die Zeile genauer an:
db.transaction(["employee"], "readonly");
Dadurch wird eine neue Transaktion mit Lesezugriff erstellt. Für alle Datenvorgänge mit IndexedDB ist eine Transaktion erforderlich.
objectStore("employee");
Rufen Sie den Objektspeicher für Mitarbeiter ab.
count()
Führen Sie die Count API aus, die – wie Sie sich denken können – eine Zählung durchführt.
onsuccess = function(e) {
Und wenn Sie fertig sind, führen Sie diesen Callback aus. Im Callback können wir den Ergebniswert abrufen, d. h. die Anzahl der Objekte. Wenn die Anzahl null ist, beginnen wir mit dem Seed-Prozess.
Wir verwenden das zuvor erwähnte Status-div, um dem Nutzer mitzuteilen, dass wir mit dem Abrufen von Daten beginnen. Aufgrund der asynchronen Natur von IndexedDB haben wir eine einfache Variable „done“ eingerichtet, die Ergänzungen verfolgt. Wir durchlaufen die Schleife und fügen die gefälschten Personen ein. Die Quelle dieser Funktion ist im Download verfügbar. Sie gibt 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. Wir haben jedoch eine besondere Anforderung, um unsere Daten durchsuchen zu können. IndexedDB bietet keine Möglichkeit, Elemente ohne Berücksichtigung der Groß-/Kleinschreibung zu suchen. Daher erstellen wir eine Kopie des Felds „lastname“ in einer neuen Property „searchkey“. 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.
Damit die Datenbankergänzungen leistungsstark sind, 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 Schreibvorgang auf dem Laufwerk ausführen. Das führt zu einer sehr schlechten Leistung, wenn Sie viele Elemente hinzufügen (z. B. 1 Minute, um 1.000 Objekte zu schreiben).
Sobald der Seed abgeschlossen ist, wird der nächste Teil unserer Anwendung ausgelöst: setupAutoComplete.
Automatische Vervollständigung erstellen
Jetzt kommt der Teil, der Spaß macht: die Verknüpfung mit dem jQuery UI-Plug-in Autocomplete. Wie bei den meisten jQuery-UI-Elementen beginnen wir mit einem einfachen HTML-Element und erweitern es, indem wir eine Konstruktormethode dafür aufrufen. Wir haben den gesamten Prozess in eine Funktion namens „setupAutoComplete“ abstrahiert. Sehen wir uns den 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 ein Quellattribut definieren, das an alle möglichen Anforderungen angepasst werden kann – sogar an unsere IndexedDB-Daten. Die API stellt Ihnen die Anfrage (im Grunde das, was in das Formularfeld eingegeben wurde) und einen Antwort-Callback zur Verfügung. Sie sind dafür verantwortlich, ein Array von Ergebnissen an diesen Callback zurückzusenden.
Zuerst blenden wir das div-Element „displayEmployee“ aus. Es wird verwendet, um einen einzelnen Mitarbeiter anzuzeigen und, falls zuvor 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 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 als untere Grenze und der Eingabe plus dem Buchstaben „z“ als obere Grenze basiert. Beachten Sie auch, dass wir den Begriff in Kleinbuchstaben schreiben, damit er mit den eingegebenen Daten in Kleinbuchstaben übereinstimmt.
Anschließend können wir einen Cursor öffnen (ähnlich wie beim Ausführen einer Datenbankabfrage) und die Ergebnisse durchlaufen. Mit dem Autocomplete-Steuerelement von jQuery UI können Sie jeden gewünschten Datentyp zurückgeben, es ist jedoch mindestens ein Wertschlüssel erforderlich. Wir legen den Wert auf eine gut formatierte Version des Namens fest. Wir geben auch die gesamte Person zurück. Sie werden gleich sehen, warum. Zuerst sehen Sie hier einen Screenshot der automatischen Vervollständigung in Aktion. Wir verwenden das Vader-Design für jQuery UI.
Das allein 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 wird. Beim Erstellen der automatischen Vervollständigung haben wir einen Auswahl-Handler angegeben, der die Handlebars-Vorlage von vorhin verwendet.