Die Fähigkeit, Daten clientseitig zu speichern, ist für moderne Webanwendungen unverzichtbar. Browser Storage APIs ermöglichen es, Nutzerpräferenzen zu speichern, den Anwendungszustand zu erhalten und Offline-Funktionalität zu implementieren. Diese APIs bieten unterschiedliche Mechanismen für die Datenspeicherung, jeweils mit eigenen Eigenschaften bezüglich Persistenz, Kapazität und Zugriffsmustern.
Cookies waren der erste Mechanismus zur clientseitigen Datenspeicherung und werden vor allem für sitzungsübergreifende Informationen verwendet.
// Beispiel: Setzen, Lesen und Löschen von Cookies
// Cookie setzen mit Ablaufdatum und Pfad
document.cookie = "username=JohnDoe; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/; SameSite=Strict";
// Weitere Cookies hinzufügen (überschreibt nicht bestehende Cookies)
document.cookie = "language=de; path=/";
// Cookie mit HttpOnly-Flag kann nur serverseitig gesetzt werden
// Cookie auslesen (gibt alle Cookies als einen String zurück)
const allCookies = document.cookie;
console.log('Alle Cookies:', allCookies); // "username=JohnDoe; language=de"
// Hilfsfunktion zum Extrahieren eines bestimmten Cookies
function getCookie(name) {
const cookieArr = document.cookie.split(';');
for (let i = 0; i < cookieArr.length; i++) {
const cookiePair = cookieArr[i].split('=');
if (name === cookiePair[0].trim()) {
return decodeURIComponent(cookiePair[1]);
}
}
return null;
}
// Cookie löschen (durch Setzen eines abgelaufenen Datums)
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";Eigenschaften von Cookies: - Werden bei jedem HTTP-Request zum Server gesendet - Begrenzte Größe (in der Regel 4 KB pro Cookie) - Können ein Ablaufdatum haben - Können auf bestimmte Domains und Pfade beschränkt werden - Zugänglich für JavaScript (außer HttpOnly-Cookies)
Die Web Storage API bietet zwei Mechanismen zur Datenspeicherung mit einer einfacheren Schnittstelle als Cookies.
// Beispiel: sessionStorage verwenden
// Daten speichern
sessionStorage.setItem('currentUser', 'JohnDoe');
sessionStorage.setItem('lastAction', JSON.stringify({
type: 'EDIT_PROFILE',
timestamp: Date.now()
}));
// Daten abrufen
const currentUser = sessionStorage.getItem('currentUser');
const lastAction = JSON.parse(sessionStorage.getItem('lastAction'));
// Spezifischen Eintrag löschen
sessionStorage.removeItem('lastAction');
// Gesamten Storage leeren
sessionStorage.clear();
// Anzahl der gespeicherten Items
const itemCount = sessionStorage.length;
// Iterieren über alle Einträge
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
const value = sessionStorage.getItem(key);
console.log(`${key}: ${value}`);
}Eigenschaften von sessionStorage: - Daten existieren nur innerhalb der aktuellen Browsersitzung - Daten gehen bei Schließen des Tabs oder Fensters verloren - Kapazität von ca. 5-10 MB (browserabhängig) - Auf die gleiche Origin (Domain, Protokoll, Port) beschränkt
// Beispiel: localStorage verwenden
// Daten speichern
localStorage.setItem('userPreferences', JSON.stringify({
theme: 'dark',
fontSize: 16,
sidebarCollapsed: true
}));
// Daten abrufen und verwenden
const preferences = JSON.parse(localStorage.getItem('userPreferences'));
if (preferences) {
document.body.classList.add(preferences.theme);
document.documentElement.style.fontSize = `${preferences.fontSize}px`;
}
// Storage-Events für domainübergreifende Kommunikation
window.addEventListener('storage', (event) => {
console.log('Storage changed in another tab/window');
console.log('Key modified:', event.key);
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);
console.log('Storage area:', event.storageArea); // localStorage oder sessionStorage
// UI entsprechend aktualisieren
if (event.key === 'userPreferences') {
updateUIFromPreferences(JSON.parse(event.newValue));
}
});Eigenschaften von localStorage: - Daten bleiben über Browser-Neustarts hinweg bestehen - Kapazität von ca. 5-10 MB (browserabhängig) - Auf die gleiche Origin beschränkt - Synchrone API (kann bei großen Datensätzen UI blockieren)
IndexedDB ist eine leistungsstarke, transaktionale Datenbank-API für umfangreiche strukturierte Daten.
// Beispiel: Grundlegende IndexedDB-Operationen
// Datenbank öffnen oder erstellen
const request = indexedDB.open('MyAppDatabase', 1);
// Schema-Definition (wird nur bei Versions-Upgrade ausgeführt)
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Object Store erstellen (ähnlich einer Tabelle)
const productsStore = db.createObjectStore('products', { keyPath: 'id' });
productsStore.createIndex('name', 'name', { unique: false });
productsStore.createIndex('price', 'price', { unique: false });
// Weiterer Object Store mit auto-increment key
const customersStore = db.createObjectStore('customers', { autoIncrement: true });
customersStore.createIndex('email', 'email', { unique: true });
};
request.onsuccess = (event) => {
const db = event.target.result;
// Daten lesen (in einer Transaktion)
const transaction = db.transaction(['products'], 'readonly');
const productsStore = transaction.objectStore('products');
// Einzelnen Eintrag per Key abrufen
const getRequest = productsStore.get(123);
getRequest.onsuccess = (event) => {
const product = event.target.result;
console.log('Produkt:', product);
};
// Über einen Index suchen
const nameIndex = productsStore.index('name');
const nameQuery = nameIndex.getAll('Smartphone');
nameQuery.onsuccess = (event) => {
const smartphones = event.target.result;
console.log('Gefundene Smartphones:', smartphones);
};
// Transaktion abschließen
transaction.oncomplete = () => console.log('Transaktion abgeschlossen');
};
// Daten schreiben
function addProduct(db, product) {
const transaction = db.transaction(['products'], 'readwrite');
const productsStore = transaction.objectStore('products');
// Produkt hinzufügen oder aktualisieren
const request = productsStore.put(product);
request.onsuccess = () => console.log('Produkt gespeichert');
request.onerror = () => console.error('Fehler beim Speichern');
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve(product);
transaction.onerror = () => reject(transaction.error);
});
}
// Beispiel für Verwendung mit async/await und Promises
async function updateProductPrice(productId, newPrice) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyAppDatabase', 1);
request.onerror = () => reject(request.error);
request.onsuccess = async (event) => {
const db = event.target.result;
const tx = db.transaction(['products'], 'readwrite');
const store = tx.objectStore('products');
try {
// Produkt abrufen
const getRequest = store.get(productId);
const product = await new Promise((resolve, reject) => {
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
if (!product) {
throw new Error('Produkt nicht gefunden');
}
// Preis aktualisieren und zurückspeichern
product.price = newPrice;
product.lastUpdated = Date.now();
const updateRequest = store.put(product);
await new Promise((resolve, reject) => {
updateRequest.onsuccess = () => resolve();
updateRequest.onerror = () => reject(updateRequest.error);
});
resolve(product);
} catch (error) {
tx.abort();
reject(error);
}
};
});
}Eigenschaften von IndexedDB: - Asynchrone API, blockiert nicht den UI-Thread - Hohe Speicherkapazität (in der Regel 50% des verfügbaren Festplattenspeichers) - Unterstützt komplexe strukturierte Daten (nicht nur Strings) - Transaktionales Modell für Datenkonsistenz - Schnelle Indizierung und Abfrage großer Datenmengen - Unterstützung für Blob und File-Objekte
Die Cache API ist Teil der Service Worker API und ermöglicht das programmgesteuerte Caching von HTTP-Anfragen und -Antworten.
// Beispiel: Ressourcen mit der Cache API speichern und abrufen
// Cache öffnen oder erstellen
caches.open('app-static-v1').then(cache => {
// Mehrere Ressourcen auf einmal cachen
return cache.addAll([
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
'/offline.html'
]);
});
// Cache-Verwendung in einem Service Worker
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(response => {
// Cache-Treffer zurückgeben, wenn vorhanden
if (response) {
return response;
}
// Andernfalls Netzwerkanfrage stellen und Ergebnis cachen
return fetch(event.request).then(response => {
// Nur valide Antworten cachen (vermeide Fehlerseiten)
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Response klonen, da sie nur einmal gelesen werden kann
const responseToCache = response.clone();
caches.open('app-dynamic').then(cache => {
cache.put(event.request, responseToCache);
});
return response;
}).catch(() => {
// Bei Netzwerkfehler Offline-Fallback liefern
if (event.request.headers.get('accept').includes('text/html')) {
return caches.match('/offline.html');
}
});
})
);
});
// Cache-Verwaltung: Alte Caches bereinigen
self.addEventListener('activate', (event) => {
const currentCaches = ['app-static-v2', 'app-dynamic']; // Aktuelle Cache-Versionen
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (!currentCaches.includes(cacheName)) {
return caches.delete(cacheName); // Alte Cache-Versionen löschen
}
})
);
})
);
});Eigenschaften der Cache API: - Speziell für HTTP-Ressourcen (Request-Response-Paare) konzipiert - Separate Speicherbereiche vom Browser-Cache - Programmatische Kontrolle über Caching-Strategien - Wird häufig mit Service Workern für Offline-Funktionalität verwendet - Persistenter Speicher (bleibt über Browser-Neustarts hinweg erhalten)
Web Storage (localStorage/sessionStorage) bietet eine einfache API, hat aber einige wichtige Einschränkungen:
// Beispiel: Typensicheres Arbeiten mit localStorage
// Problem: localStorage speichert nur Strings
localStorage.setItem('count', 42);
const count = localStorage.getItem('count'); // Ergebnis ist "42" (String)
console.log(typeof count); // "string"
// Lösung: Explizite Typkonvertierung
localStorage.setItem('count', String(42));
const countNumber = Number(localStorage.getItem('count'));
// Komplexe Daten: JSON-Serialisierung
const user = {
id: 123,
name: 'Max Mustermann',
lastLogin: new Date(),
isAdmin: false
};
// Beachte: Date-Objekte werden zu Strings
localStorage.setItem('currentUser', JSON.stringify(user));
// Beim Auslesen müssen Date-Objekte manuell konvertiert werden
const storedUser = JSON.parse(localStorage.getItem('currentUser'));
storedUser.lastLogin = new Date(storedUser.lastLogin);Wichtige Fallstricke und Best Practices:
Datentypen: Web Storage speichert ausschließlich
Strings. Verwende JSON.stringify() und
JSON.parse() für komplexe Daten.
Fehlerbehandlung: Prüfe immer auf
null bei getItem(), da nicht vorhandene Keys
null zurückgeben.
Kapazitätsgrenzen: Implementiere Fehlerbehandlung für QuotaExceededError.
// Beispiel: Fehlerbehandlung bei Kapazitätsüberschreitung
try {
localStorage.setItem('largeData', veryLargeString);
} catch (e) {
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
alert('Speicherplatz erschöpft. Bitte alte Daten bereinigen.');
// Bereinigungsstrategie implementieren
cleanupOldData();
}
}// Beispiel: Datenversionierung
const appData = {
version: '1.2.3',
settings: { /* ... */ },
userPreferences: { /* ... */ }
};
localStorage.setItem('appData', JSON.stringify(appData));
// Beim Laden prüfen
const loadedData = JSON.parse(localStorage.getItem('appData')) || {};
if (loadedData.version !== '1.2.3') {
// Datenformat migrieren oder neu initialisieren
migrateDataFromOldVersion(loadedData);
}// Beispiel: Typsicherer Storage-Wrapper
class StorageManager {
constructor(storage = localStorage) {
this.storage = storage;
}
get(key, defaultValue = null) {
try {
const item = this.storage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (e) {
console.error(`Error retrieving ${key} from storage:`, e);
return defaultValue;
}
}
set(key, value) {
try {
this.storage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
console.error(`Error storing ${key} in storage:`, e);
return false;
}
}
remove(key) {
try {
this.storage.removeItem(key);
return true;
} catch (e) {
return false;
}
}
clear() {
try {
this.storage.clear();
return true;
} catch (e) {
return false;
}
}
}
// Verwendung
const storage = new StorageManager();
storage.set('user', {id: 1, name: 'Test'});
const user = storage.get('user');Die moderne FileSystem Access API ermöglicht Webanwendungen den direkten Zugriff auf lokale Dateien und Verzeichnisse (mit Benutzererlaubnis).
// Beispiel: Datei öffnen mit FileSystem Access API
async function openFile() {
try {
// Dateiauswahldialog öffnen
const [fileHandle] = await window.showOpenFilePicker({
types: [
{
description: 'Text Files',
accept: {
'text/plain': ['.txt', '.md']
}
}
],
multiple: false
});
// Datei-Objekt erhalten
const file = await fileHandle.getFile();
// Dateiinhalt lesen
const contents = await file.text();
console.log('Dateiinhalt:', contents);
// Speichern des FileHandle für späteren Zugriff
return { fileHandle, contents };
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Fehler beim Öffnen der Datei:', err);
}
return null;
}
}
// Beispiel: Datei speichern mit FileSystem Access API
async function saveFile(fileHandle, content) {
try {
// Prüfen, ob wir einen bestehenden FileHandle haben
if (!fileHandle) {
// Neuen Speicherdialog öffnen
fileHandle = await window.showSaveFilePicker({
types: [
{
description: 'Text File',
accept: { 'text/plain': ['.txt'] }
}
]
});
}
// Schreibbaren Stream erhalten
const writable = await fileHandle.createWritable();
// In die Datei schreiben
await writable.write(content);
// Stream schließen, um Datei zu speichern
await writable.close();
return fileHandle;
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Fehler beim Speichern der Datei:', err);
}
return null;
}
}
// Beispiel: Verzeichniszugriff
async function openDirectory() {
try {
const dirHandle = await window.showDirectoryPicker();
// Über alle Dateien im Verzeichnis iterieren
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file') {
console.log(`Datei: ${name}`);
// Datei verarbeiten...
} else if (handle.kind === 'directory') {
console.log(`Verzeichnis: ${name}`);
// Unterverzeichnis verarbeiten...
}
}
return dirHandle;
} catch (err) {
console.error('Fehler beim Öffnen des Verzeichnisses:', err);
return null;
}
}Eigenschaften der FileSystem Access API: - Erfordert explizite Benutzererlaubnis per Picker-Dialog - Bietet schreibenden und lesenden Zugriff auf lokale Dateien - Erlaubt Zugriff auf Verzeichnisstrukturen - Unterstützt persistenten Zugriff auf Dateien über mehrere Sitzungen - Unterstützt binäre Daten, Text und andere Dateiformate
Bei der Verwendung von Browser Storage APIs müssen verschiedene Sicherheitsaspekte beachtet werden:
// Beispiel: Besserer Umgang mit Auth-Tokens
// Schlecht (direkt in localStorage)
localStorage.setItem('authToken', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
// Besser (HttpOnly-Cookie vom Server)
// Der Server setzt: Set-Cookie: authToken=xyz; HttpOnly; Secure; SameSite=Strict// Beispiel: Daten validieren beim Auslesen
const userData = JSON.parse(localStorage.getItem('userData'));
// Daten vor der Verwendung validieren/bereinigen
if (userData && isValidUserData(userData)) {
// Sichere Verwendung
} else {
// Daten zurücksetzen
localStorage.removeItem('userData');
}Für komplexe Anwendungen empfiehlt sich ein abgestufter Ansatz zur Datenverwaltung:
// Beispiel: Mehrschichtige Datenverwaltungsstrategie
class DataManager {
constructor() {
this.memoryCache = new Map(); // In-Memory-Cache für schnellen Zugriff
this.storage = new StorageManager(); // Wrapper für localStorage
this.dbPromise = this.initIndexedDB(); // IndexedDB für große Datasets
}
async initIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('AppDatabase', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('entities', { keyPath: 'id' });
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Mehrschichtige Datenabruf-Strategie
async getEntity(id) {
// 1. Zuerst im Memory-Cache suchen (schnellste Option)
if (this.memoryCache.has(id)) {
return this.memoryCache.get(id);
}
// 2. Dann im localStorage suchen
const fromStorage = this.storage.get(`entity:${id}`);
if (fromStorage) {
// In Memory-Cache für zukünftige Abfragen speichern
this.memoryCache.set(id, fromStorage);
return fromStorage;
}
// 3. Schließlich in IndexedDB suchen
const db = await this.dbPromise;
return new Promise((resolve) => {
const tx = db.transaction(['entities'], 'readonly');
const store = tx.objectStore('entities');
const request = store.get(id);
request.onsuccess = () => {
const entity = request.result;
if (entity) {
// In beiden Cache-Ebenen speichern
this.memoryCache.set(id, entity);
this.storage.set(`entity:${id}`, entity);
}
resolve(entity || null);
};
request.onerror = () => resolve(null);
});
}
// Speichern über alle Schichten
async saveEntity(entity) {
// In Memory-Cache speichern
this.memoryCache.set(entity.id, entity);
// In localStorage speichern
this.storage.set(`entity:${entity.id}`, entity);
// In IndexedDB speichern
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const tx = db.transaction(['entities'], 'readwrite');
const store = tx.objectStore('entities');
const request = store.put(entity);
tx.oncomplete = () => resolve(true);
tx.onerror = () => reject(tx.error);
});
}
}