Ge­schwin­dig­keit spielt beim Surfen im World Wide Web eine große Rolle. Denn niemand wartet gern Ewig­kei­ten, bis sich eine Seite aufgebaut hat. Soll der Sei­ten­auf­bau möglichst schnell verlaufen, ist es hilfreich, wenn sich ein Teil der In­for­ma­tio­nen bereits beim Nutzer befindet und somit nicht erst über­tra­gen werden muss. Eine Mög­lich­keit dazu bietet IndexedDB: Ein Speicher direkt im Browser des Nutzers, für jede Website zu­gäng­lich. Wie funk­tio­niert das?

Wofür braucht man IndexedDB?

Es ist sinnvoll, dass nicht nur Server die Daten von Clients bei sich speichern, sondern auch Clients aus­ge­wähl­te In­for­ma­tio­nen einer Website bei sich liegen haben. Denn das be­schleu­nigt das Surfen, da nicht länger alles bei jedem Aufruf neu geladen werden muss. Hinzu kommt, dass man in einem solchen Fall Web­ap­pli­ka­tio­nen auch offline verwenden kann. Auch Eingaben der Nutzer lassen sich cli­ent­sei­tig gut un­ter­brin­gen. Gerade für letzteres sind ei­gent­lich Cookies gedacht. Doch diese haben nur einen sehr be­grenz­ten Datei- und Nut­zen­um­fang – für moderne Web­an­wen­dun­gen 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 Su­per­coo­kies genannten Dateien sehr simpel struk­tu­riert. Ei­gen­schaf­ten einer modernen Datenbank sucht man vergebens. Cookies und Su­per­coo­kies sind jedoch nicht nur aufgrund ihrer geringen Größe eine sub­op­ti­ma­le Lösung – beide Formate lassen außerdem keine struk­tu­rier­ten Daten und keine Indexe zu, weshalb eine Suche nicht möglich ist.

Die Ent­wick­lung von Web SQL versprach zunächst eine prin­zi­pi­el­le Neu­ori­en­tie­rung: einen cli­ent­sei­ti­gen Speicher auf der Basis von SQL. Doch das World Wide Web Con­sor­ti­um (W3C) – eine Or­ga­ni­sa­ti­on zur Ent­wick­lung von Web-Standards – stellte die Arbeit daran zu Gunsten von IndexedDB ein. Unter der Fe­der­füh­rung von Mozilla entstand so ein Standard, der in­zwi­schen von den meisten modernen Browsern un­ter­stützt wird.

IndexedDB Browser Support

Chrome Firefox Opera Opera mini Safari IE Edge

Was kann IndexedDB?

Zunächst einmal ist der Standard eine Schnitt­stel­le, die im Browser ein­ge­rich­tet ist. Webseiten können über diese In­for­ma­tio­nen direkt im Browser speichern. Das funk­tio­niert über Ja­va­Script. Jede Website kann so eine eigene Datenbank anlegen. Und nur die ent­spre­chen­de Website kann auf die IndexedDB (kurz für Indexed Database API) zugreifen. So bleiben die Daten privat. In den Da­ten­ban­ken stehen mehrere Object Storages zur Verfügung. Dort lassen sich wiederum ver­schie­de­ne Formate hin­ter­le­gen: Strings, Zahlen, Objekte, Arrays und Da­tums­an­ga­ben.

IndexedDB ist keine re­la­tio­na­le Datenbank, sondern ein in­di­zier­tes Ta­bel­len­sys­tem. Tat­säch­lich 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 Ei­gen­schaft zu diesem. Hinzu kommen Indexe. Diese lassen eine schnelle Suche zu.

Aktionen werden in IndexedDB immer in Form von Trans­ak­tio­nen durch­ge­führt. Jeder Schreib-, Lese- oder Än­de­rungs­vor­gang ist in eine Trans­ak­ti­on in­te­griert. Das ga­ran­tiert, dass Än­de­run­gen an der Datenbank entweder voll­stän­dig oder gar nicht durch­ge­führt werden. Ein Vorteil von IndexedDB ist, dass der Da­ten­trans­fer (in den meisten Fällen) nicht synchron ablaufen muss. Ope­ra­tio­nen werden asynchron durch­ge­führt. Das ga­ran­tiert, dass der Web­brow­ser während der Operation nicht gesperrt wird und weiterhin vom Nutzer bedient werden kann.

Eine große Rolle spielt bei IndexedDB die Si­cher­heit. Es muss si­cher­ge­stellt werden, dass Websites nicht auf die Da­ten­ban­ken von anderen Websites zugreifen können. Zu diesem Zweck hat IndexedDB eine Same-Origin-Policy etabliert: Domain, An­wen­dungs­schich­ten­pro­to­koll und Port müssen gleich sein, sonst stehen die Daten nicht zur Verfügung. Dabei ist es durchaus möglich, dass auch Un­ter­ord­ner einer Domain auf die IndexedDB eines anderen Un­ter­ord­ners zugreifen, da beide die gleiche Herkunft haben. Nicht möglich ist der Zugriff al­ler­dings, 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 al­ler­dings eine Datenbank und Object Stores erzeugen kann, sollte man eine Über­prü­fung einbauen. Auch wenn IndexedDB in­zwi­schen wei­test­ge­hend von allen modernen Browsern un­ter­stützt wird, gilt das für veraltete Web­brow­ser nicht. Deshalb fragen Sie zunächst, ob IndexedDB un­ter­stützt wird. Dabei über­prü­fen Sie das Window-Objekt.

Hinweis

Sie können die Code-Beispiele über die Konsole der Ent­wick­ler­werk­zeu­ge im Browser nach­ver­fol­gen. Über die Tools können Sie auch In­de­xedDBs von anderen Seiten einsehen.

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

Kann der Browser des Nutzers nicht mit IndexedDB umgehen, erscheint ein Dia­log­fens­ter, das darüber in­for­miert. Al­ter­na­tiv können Sie mit console.error auch eine Feh­ler­mel­dung in Ihrer Log-Datei erzeugen.

Nun eröffnet man eine Datenbank. Prin­zi­pi­ell kann eine Website mehrere Da­ten­ban­ken öffnen, doch in der Praxis hat es sich bewährt, eine IndexedDB pro Domain anzulegen. In dieser hat man die Mög­lich­keit, mit mehreren Object Stores zu arbeiten. Das Eröffnen einer Datenbank funk­tio­niert über eine Anfrage – ein asyn­chro­ner Request.

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

Sie geben beim Öffnen zwei Argumente an: zunächst einen selbst­ge­wähl­ten Namen (als String) und dann die Ver­si­ons­num­mer (als Integer, also ganze Zahl). Man beginnt ver­ständ­li­cher­wei­se mit Version 1. Das daraus re­sul­tie­ren­de Objekt liefert einen von drei Events:

  • error: Bei der Er­stel­lung hat es einen Fehler gegeben.
  • up­grade­nee­ded: Die Version der Datenbank hat sich geändert. Dies erscheint also auch beim Anlegen, denn auch hierbei ändert sich quasi die Ver­si­ons­num­mer: von nicht existent zu 1.
  • success: Die Datenbank konnte er­folg­reich geöffnet werden.

Nun kann die ei­gent­li­che 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 Num­me­rie­rung, die wir zudem mit au­to­In­cre­ment fort­lau­fend 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 Nut­zer­na­men und einen für die ver­wen­de­ten E-Mail-Adressen anlegen.

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

So können Sie Da­ten­sät­ze leicht durch das ver­wen­de­te Pseudonym eines Nutzers oder dessen E-Mail-Adresse finden. Die beiden Indexe un­ter­schei­den 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 Ope­ra­tio­nen mit der Datenbank müssen in eine Trans­ak­ti­on ein­ge­glie­dert sein. Davon gibt es drei ver­schie­de­ne:

  • readonly: Liest Daten aus einem Object Store. Mehrere Trans­ak­tio­nen dieses Typs können gleich­zei­tig laufen, auch wenn diese sich auf den gleichen Bereich beziehen.
  • readwrite: Liest und erstellt Einträge. Diese Trans­ak­tio­nen können nur gleich­zei­tig laufen, wenn sie sich auf un­ter­schied­li­che Bereiche beziehen.
  • ver­sion­ch­an­ge: Nimmt Än­de­run­gen am Object Store oder Indexen vor, erzeugt und verändert aber auch Einträge. Dieser Modus kann nicht manuell erstellt werden, sondern wird au­to­ma­tisch bei dem Event up­grade­nee­ded 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 In­for­ma­tio­nen in Ihren Object Store ein. Außerdem lassen Sie sich so Meldungen über die Konsole anzeigen, abhängig vom Gelingen der Trans­ak­ti­on. 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 Trans­ak­ti­on fehl­schla­gen, wird eine Feh­ler­mel­dung erzeugt. Wenn die Trans­ak­ti­on al­ler­dings er­folg­reich 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 be­stimm­ten 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 In­for­ma­tio­nen 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, funk­tio­niert dies ganz ähnlich zum Hin­zu­fü­gen des Da­ten­sat­zes – mit einer readwrite-Trans­ak­ti­on.

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 ein­ge­führt. Weitere In­for­ma­tio­nen finden Sie zum Beispiel bei Mozilla oder Google. Google verwendet im Beispiel-Code al­ler­dings eine spezielle Bi­blio­thek, weshalb sich der Code teilweise von dem von Mozilla un­ter­schei­det.

Zum Hauptmenü