IndexedDB: Tutorial für den Speicher im Browser

Geschwindigkeit spielt beim Surfen im World Wide Web eine große Rolle. Denn niemand wartet gern Ewigkeiten, bis sich eine Seite aufgebaut hat. Soll der Seitenaufbau möglichst schnell verlaufen, ist es hilfreich, wenn sich ein Teil der Informationen bereits beim Nutzer befindet und somit nicht erst übertragen werden muss. Eine Möglichkeit dazu bietet IndexedDB: Ein Speicher direkt im Browser des Nutzers, für jede Website zugänglich. Wie funktioniert das?

Wofür braucht man IndexedDB?

Es ist sinnvoll, dass nicht nur Server die Daten von Clients bei sich speichern, sondern auch Clients ausgewählte Informationen einer Website bei sich liegen haben. Denn das beschleunigt das Surfen, da nicht länger alles bei jedem Aufruf neu geladen werden muss. Hinzu kommt, dass man in einem solchen Fall Webapplikationen auch offline verwenden kann. Auch Eingaben der Nutzer lassen sich clientseitig gut unterbringen. Gerade für letzteres sind eigentlich Cookies gedacht. Doch diese haben nur einen sehr begrenzten Datei- und Nutzenumfang – für moderne Webanwendungen viel zu gering. Außerdem: Cookies müssen bei jedem HTTP-Aufruf durchs Netz geschickt werden.

Eine erste Lösung stellte der Web Storage – oft auch DOM Storage genannt – dar: Diese Technik basiert noch stark auf der Idee eines Cookies, erweitert aber immerhin den Umfang von wenigen Kilobyte auf 10 MB. Doch auch das ist nicht besonders viel. Zudem sind diese oft auch Supercookies genannten Dateien sehr simpel strukturiert. Eigenschaften einer modernen Datenbank sucht man vergebens. Cookies und Supercookies sind jedoch nicht nur aufgrund ihrer geringen Größe eine suboptimale Lösung – beide Formate lassen außerdem keine strukturierten Daten und keine Indexe zu, weshalb eine Suche nicht möglich ist.

Die Entwicklung von Web SQL versprach zunächst eine prinzipielle Neuorientierung: einen clientseitigen Speicher auf der Basis von SQL. Doch das World Wide Web Consortium (W3C) – eine Organisation zur Entwicklung von Web-Standards – stellte die Arbeit daran zu Gunsten von IndexedDB ein. Unter der Federführung von Mozilla entstand so ein Standard, der inzwischen von den meisten modernen Browsern unterstützt wird.

IndexedDB Browser Support

Chrome Firefox Opera Opera mini Safari IE Edge

Was kann IndexedDB?

Zunächst einmal ist der Standard eine Schnittstelle, die im Browser eingerichtet ist. Webseiten können über diese Informationen direkt im Browser speichern. Das funktioniert über JavaScript. Jede Website kann so eine eigene Datenbank anlegen. Und nur die entsprechende Website kann auf die IndexedDB (kurz für Indexed Database API) zugreifen. So bleiben die Daten privat. In den Datenbanken stehen mehrere Object Storages zur Verfügung. Dort lassen sich wiederum verschiedene Formate hinterlegen: Strings, Zahlen, Objekte, Arrays und Datumsangaben.

IndexedDB ist keine relationale Datenbank, sondern ein indiziertes Tabellensystem. Tatsächlich handelt es sich um eine NoSQL-Datenbank, wie es zum Beispiel auch MongoDB ist. Einträge werden immer in Paaren angelegt: Schlüssel und Wert. Dabei ist der Wert ein Objekt und der Schlüssel die Eigenschaft zu diesem. Hinzu kommen Indexe. Diese lassen eine schnelle Suche zu.

Aktionen werden in IndexedDB immer in Form von Transaktionen durchgeführt. Jeder Schreib-, Lese- oder Änderungsvorgang ist in eine Transaktion integriert. Das garantiert, dass Änderungen an der Datenbank entweder vollständig oder gar nicht durchgeführt werden. Ein Vorteil von IndexedDB ist, dass der Datentransfer (in den meisten Fällen) nicht synchron ablaufen muss. Operationen werden asynchron durchgeführt. Das garantiert, dass der Webbrowser während der Operation nicht gesperrt wird und weiterhin vom Nutzer bedient werden kann.

Eine große Rolle spielt bei IndexedDB die Sicherheit. Es muss sichergestellt werden, dass Websites nicht auf die Datenbanken von anderen Websites zugreifen können. Zu diesem Zweck hat IndexedDB eine Same-Origin-Policy etabliert: Domain, Anwendungsschichtenprotokoll und Port müssen gleich sein, sonst stehen die Daten nicht zur Verfügung. Dabei ist es durchaus möglich, dass auch Unterordner einer Domain auf die IndexedDB eines anderen Unterordners zugreifen, da beide die gleiche Herkunft haben. Nicht möglich ist der Zugriff allerdings, wenn ein anderer Port verwendet wird oder das Protokoll von HTTP zu HTTPS oder umgekehrt wechselt.

IndexedDB-Tutorial: Die Technik im Einsatz

Wir erklären IndexedDB an einem Beispiel. Bevor man allerdings eine Datenbank und Object Stores erzeugen kann, sollte man eine Überprüfung einbauen. Auch wenn IndexedDB inzwischen weitestgehend von allen modernen Browsern unterstützt wird, gilt das für veraltete Webbrowser nicht. Deshalb fragen Sie zunächst, ob IndexedDB unterstützt wird. Dabei überprüfen Sie das Window-Objekt.

Hinweis

Sie können die Code-Beispiele über die Konsole der Entwicklerwerkzeuge im Browser nachverfolgen. Über die Tools können Sie auch IndexedDBs von anderen Seiten einsehen.

if (!window.indexedDB) {
	alert("IndexedDB wird nicht unterstützt!");
}

Kann der Browser des Nutzers nicht mit IndexedDB umgehen, erscheint ein Dialogfenster, das darüber informiert. Alternativ können Sie mit console.error auch eine Fehlermeldung in Ihrer Log-Datei erzeugen.

Nun eröffnet man eine Datenbank. Prinzipiell kann eine Website mehrere Datenbanken öffnen, doch in der Praxis hat es sich bewährt, eine IndexedDB pro Domain anzulegen. In dieser hat man die Möglichkeit, mit mehreren Object Stores zu arbeiten. Das Eröffnen einer Datenbank funktioniert über eine Anfrage – ein asynchroner Request.

var request = window.indexedDB.open("MeineDatenbank", 1);

Sie geben beim Öffnen zwei Argumente an: zunächst einen selbstgewählten Namen (als String) und dann die Versionsnummer (als Integer, also ganze Zahl). Man beginnt verständlicherweise mit Version 1. Das daraus resultierende Objekt liefert einen von drei Events:

  • error: Bei der Erstellung hat es einen Fehler gegeben.
  • upgradeneeded: Die Version der Datenbank hat sich geändert. Dies erscheint also auch beim Anlegen, denn auch hierbei ändert sich quasi die Versionsnummer: von nicht existent zu 1.
  • success: Die Datenbank konnte erfolgreich geöffnet werden.

Nun kann die eigentliche Datenbank und ein Object Store erstellt werden.

request.onupgradeneeded = function(event) {
	var db = event.target.result;
	var objectStore = db.createObjectStore("Nutzer", { keyPath: "id", autoIncrement: true });
}

Unser Object Store erhält den Namen Nutzer. Der Key ist id, eine einfache Nummerierung, die wir zudem mit autoIncrement fortlaufend steigen lassen. Nun können Sie die Datenbank bzw. den Object Store mit Daten füttern. Dafür erstellen Sie zuerst einen oder mehrere Indexe. In unserem Beispiel möchten wir einen Index für den Nutzernamen und einen für die verwendeten E-Mail-Adressen anlegen.

objectStore.createIndex("Nickname", "Nickname", { unique: false });
objectStore.createIndex("eMail", "eMail", { unique: true });

So können Sie Datensätze leicht durch das verwendete Pseudonym eines Nutzers oder dessen E-Mail-Adresse finden. Die beiden Indexe unterscheiden sich dadurch, dass der Nickname nicht einmalig vergeben werden muss, jedoch zu jeder E-Mail-Adresse nur ein einziger Eintrag bestehen darf.

Nun können Sie schließlich Einträge vornehmen. Jegliche Operationen mit der Datenbank müssen in eine Transaktion eingegliedert sein. Davon gibt es drei verschiedene:

  • readonly: Liest Daten aus einem Object Store. Mehrere Transaktionen dieses Typs können gleichzeitig laufen, auch wenn diese sich auf den gleichen Bereich beziehen.
  • readwrite: Liest und erstellt Einträge. Diese Transaktionen können nur gleichzeitig laufen, wenn sie sich auf unterschiedliche Bereiche beziehen.
  • versionchange: Nimmt Änderungen am Object Store oder Indexen vor, erzeugt und verändert aber auch Einträge. Dieser Modus kann nicht manuell erstellt werden, sondern wird automatisch bei dem Event upgradeneeded ausgelöst.

Um einen neuen Eintrag zu erstellen, greift man also zu readwrite.

const dbconnect = window.indexedDB.open('MeineDatenbank', 1);
dbconnect.onupgradeneeded = ev => {
  console.log('Upgrade DB');
  const db = ev.target.result;
  const store = db.createObjectStore('Nutzer', { keyPath: 'id', autoIncrement: true });
  store.createIndex('Nickname', 'Nickname', { unique: false });
  store.createIndex('eMail', 'eMail', { unique: true });
}
dbconnect.onsuccess = ev => {
  console.log('DB-Upgrade erfolgreich');
  const db = ev.target.result;
  const transaction = db.transaction('Nutzer', 'readwrite');
  const store = transaction.objectStore('Nutzer');
  const data = [
    {Nickname: 'Raptor123', eMail: 'raptor@example.com'},
    {Nickname: 'Dino2', eMail: 'dino@example.com'}
  ];
  data.forEach(el => store.add(el));
  transaction.onerror = ev => {
    console.error('Ein Fehler ist aufgetreten!', ev.target.error.message);
  };
  transaction.oncomplete = ev => {
    console.log('Daten wurden erfolgreich hinzugefügt!');
    const store = db.transaction('Nutzer', 'readonly').objectStore('Nutzer');
    //const query = store.get(1); // Einzel-Query
    const query = store.openCursor()
    query.onerror = ev => {
      console.error('Anfrage fehlgeschlagen!', ev.target.error.message);
    };
    /*
    // Verarbeitung der Einzel-Query
    query.onsuccess = ev => {
      if (query.result) {
        console.log('Datensatz 1', query.result.Nickname, query.result.eMail);
      } else {
        console.warn('Kein Eintrag vorhanden!');
      }
    };
    */
    query.onsuccess = ev => {
      const cursor = ev.target.result;
      if (cursor) {
        console.log(cursor.key, cursor.value.Nickname, cursor.value.eMail);
        cursor.continue();
      } else {
        console.log('Keine Einträge mehr vorhanden!');
      }
    };
  };
}

Hiermit fügen Sie Informationen in Ihren Object Store ein. Außerdem lassen Sie sich so Meldungen über die Konsole anzeigen, abhängig vom Gelingen der Transaktion. Daten, die Sie in eine IndexedDB gelegt haben, möchten Sie in der Regel auch auslesen. Hierfür verwendet man get.

var transaction = db.transaction(["Nutzer"]);
var objectStore = transaction.objectStore("Nutzer");
var request = objectStore.get(1);

request.onerror = function(event) {
  console.log("Anfrage fehlgeschlagen!");
}

request.onsuccess = function(event) {
  if (request.result) {
    console.log(request.result.Nickname);
    console.log(request.result.eMail);
  } else {
    console.log("Kein Eintrag vorhanden!");
  }
};

Mit diesem Code suchen Sie nach dem Eintrag unter dem Key 1 – also mit dem id-Wert 1. Sollte die Transaktion fehlschlagen, wird eine Fehlermeldung erzeugt. Wenn die Transaktion allerdings erfolgreich ist, erfahren Sie den Inhalt der beiden Einträge Nickname und eMail. Sollte kein Eintrag unter der Nummer zu finden sein, erhalten Sie auch darüber Auskunft.

Wenn Sie nicht nur einen Eintrag suchen, sondern gleich mehrere anzeigen wollen, hilft ein Cursor. Diese Funktion erfragt einen Eintrag nach dem anderen. Dabei können Sie entweder alle Einträge der Datenbank in Betracht ziehen, oder nur einen bestimmten Key-Bereich auswählen.

var objectStore = db.transaction("Nutzer").objectStore("Nutzer");
objectStore.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    console.log(cursor.key);
    console.log(cursor.value.Nickname);
    console.log(cursor.value.eMail);
    cursor.continue();
  } else {
    console.log("Keine Einträge mehr vorhanden!");
  }
};

Wir haben im Vorfeld zwei Indexe erzeugt, um auch über diese Informationen abrufen zu können. Auch dies läuft über get.

var index = objectStore.index("Nickname");

index.get("Raptor123").onsuccess = function(event) {
  console.log(event.target.result.eMail);
};

Möchten Sie schließlich einen Eintrag aus der Datenbank löschen, funktioniert dies ganz ähnlich zum Hinzufügen des Datensatzes – mit einer readwrite-Transaktion.

var request = db.transaction(["Nutzer"], "readwrite")
  .objectStore("Nutzer")
  .delete(1);

request.onsuccess = function(event) {
  console.log("Eintrag erfolgreich gelöscht!");
};
Fazit

Dieser Artikel hat Sie in die ersten Schritte mit IndexedDB eingeführt. Weitere Informationen finden Sie zum Beispiel bei Mozilla oder Google. Google verwendet im Beispiel-Code allerdings eine spezielle Bibliothek, weshalb sich der Code teilweise von dem von Mozilla unterscheidet.