29 Browser Storage APIs

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.

29.1 Cookies: Der traditionelle Ansatz

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)

29.2 Web Storage: sessionStorage und localStorage

Die Web Storage API bietet zwei Mechanismen zur Datenspeicherung mit einer einfacheren Schnittstelle als Cookies.

29.2.1 sessionStorage

// 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

29.2.2 localStorage

// 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)

29.3 IndexedDB: Für komplexe Datenspeicherung

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

29.4 Cache API: Für Ressourcen-Caching

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)

29.5 Web Storage API: Fallstricke und Best Practices

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:

  1. Datentypen: Web Storage speichert ausschließlich Strings. Verwende JSON.stringify() und JSON.parse() für komplexe Daten.

  2. Fehlerbehandlung: Prüfe immer auf null bei getItem(), da nicht vorhandene Keys null zurückgeben.

  3. 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();
  }
}
  1. Versionierung: Implementiere ein Versionierungsschema für gespeicherte Daten.
// 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);
}
  1. Wrapper-Bibliotheken: Für bessere Typsicherheit und Fehlerbehandlung eignen sich Wrapper-Klassen.
// 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');

29.6 FileSystem Access API: Direkter Zugriff auf Dateisystem

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

29.7 Sicherheitsaspekte

Bei der Verwendung von Browser Storage APIs müssen verschiedene Sicherheitsaspekte beachtet werden:

  1. Sensible Daten: Speichere niemals unkodierte sensible Daten (Passwörter, Tokens) in localStorage, da dieser Speicher für JavaScript auf derselben Domain zugänglich ist.
// 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
  1. XSS-Schutz: Bei einem XSS-Angriff hat der Angreifer Zugriff auf den gesamten Web Storage.
// 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');
}
  1. Same-Origin-Policy: Storage-Mechanismen sind auf die gleiche Origin beschränkt.

29.8 Strategien für die Datenverwaltung

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);
    });
  }
}