====== LU12a - Daten laden mit fetch() ======
In modernen Web-Applikationen werden Inhalte selten direkt im Code gespeichert. Stattdessen kommen sie von einem Server — einem CMS, einer Datenbank oder einer öffentlichen API. Die Browser-Funktion ''fetch()'' ist das wichtigste Werkzeug, um solche Daten zu laden.
===== 1. Warum async? Das Single-Thread-Problem =====
JavaScript ist **single-threaded**: Es kann immer nur eine Operation gleichzeitig ausführen. Stellen Sie sich vor, eine Funktion wartet drei Sekunden auf eine Server-Antwort:
console.log('Seite lädt...');
const daten = getDatenVomServer(); // ← blockiert für 3 Sekunden
console.log('Fertig'); // ← erscheint erst nach 3 Sekunden
Während dieser drei Sekunden würde **die gesamte App einfrieren** — kein Klick, kein Scrollen, keine Eingabe würde reagieren.
**Asynchroner Code** löst das Problem: Die lange Operation wird gestartet und in den Hintergrund geschickt. Der restliche Code läuft weiter. Wenn die Antwort eintrifft, wird die Funktion fortgesetzt.
fetchFaqs() startet
│
├── fetch() abgeschickt → läuft im Hintergrund
│
│ Vue rendert das Template (isLoading-Spinner sichtbar)
│ Benutzer kann klicken, scrollen, tippen
│
│ [Server antwortet]
│
└── Daten kommen an → faqItems.value = data → UI updated
===== 2. Promise: Das Versprechen =====
''fetch()'' gibt nicht sofort Daten zurück — das kann es gar nicht. Stattdessen gibt es ein **Promise** zurück: ein Objekt, das ein zukünftiges Ergebnis repräsentiert.
Ein Promise hat drei mögliche Zustände:
^ Zustand ^ Bedeutung ^
| ''pending'' | Anfrage läuft, Antwort noch nicht da |
| ''fulfilled''| Antwort eingetroffen, Daten verfügbar |
| ''rejected'' | Etwas ist schiefgegangen (Netzwerkfehler etc.) |
> **Analogie:** Ein Promise ist wie eine Bestellbestätigung im Online-Shop. Sie bekommen sie sofort — aber das Paket kommt erst später. Die Bestätigung sagt: "Ich liefere dir etwas. Ich weiss noch nicht genau wann und ob alles klappt."
===== 3. async / await =====
Die modernste Art, mit Promises umzugehen. ''async'' und ''await'' machen asynchronen Code lesbar — er sieht aus wie normaler, sequenzieller Code:
const fetchFaqs = async () => { // 'async' markiert die Funktion
const response = await fetch(url); // 'await' pausiert diese Funktion bis die Antwort da ist
const data = await response.json(); // nochmals 'await' — auch das Parsen ist async
faqItems.value = data;
};
**Wichtige Regeln:**
* ''await'' kann nur innerhalb einer ''async''-Funktion verwendet werden
* ''await'' pausiert **nur die aktuelle Funktion** — nicht die gesamte App
* Jedes ''await'' wartet auf ein Promise
===== 4. Das Response-Objekt =====
Nach ''await fetch()'' erhalten wir kein Array mit Daten — sondern ein **''Response''-Objekt**. Das ist eine eingebaute Browser-Klasse mit festen Eigenschaften:
const response = await fetch('https://...mockapi.io/faqs');
console.log(response);
// Ausgabe in der Konsole:
// Response {
// type: "cors",
// url: "https://...mockapi.io/faqs",
// status: 200,
// ok: true,
// statusText: "",
// headers: Headers { ... },
// body: ReadableStream,
// bodyUsed: false
// }
^ Eigenschaft ^ Bedeutung ^
| ''status'' | HTTP-Statuscode (bekannt aus M290): ''200'' OK, ''404'' Not Found, ''500'' Server Error |
| ''ok'' | ''true'' wenn Status zwischen 200–299, sonst ''false'' |
| ''headers'' | Antwort-Headers (Metadaten der HTTP-Antwort) |
| ''body'' | Die eigentlichen Daten — als ''ReadableStream'' |
| ''bodyUsed'' | ''false'' = Stream noch nicht gelesen, ''true'' = bereits gelesen |
==== Warum ist body ein ReadableStream? ====
Wenn ''await fetch()'' zurückkommt, sind die Header vollständig angekommen — aber der Body (die eigentlichen Daten) wird als **Datenstrom** geliefert. Der Browser hat die Bytes empfangen, aber noch nicht interpretiert.
Das ist vergleichbar mit einem Brief im Briefkasten:
* ''await fetch()'' → Der Brief liegt im Kasten. Er ist da.
* ''await response.json()'' → Sie öffnen den Umschlag, lesen und verstehen den Inhalt.
==== Den Body auslesen ====
Um die Daten aus dem Stream zu lesen und in ein JavaScript-Objekt umzuwandeln:
const data = await response.json();
Auch das braucht ''await'': Das Lesen und Parsen des Streams ist bei grossen Antworten messbar zeitaufwändig.
===== 5. Fehlerbehandlung: try / catch / finally =====
Bei einer externen Anfrage kann vieles schiefgehen: Netzwerkausfall, Server überlastet, URL falsch. Ohne Fehlerbehandlung würde die App abstürzen.
const fetchFaqs = async () => {
try {
// Code, der einen Fehler werfen könnte
const response = await fetch(url);
const data = await response.json();
faqItems.value = data;
} catch (err) {
// Wird ausgeführt wenn in 'try' ein Fehler geworfen wird
console.error('Fehler:', err.message);
} finally {
// Wird IMMER ausgeführt — egal ob Erfolg oder Fehler
isLoading.value = false;
}
};
^ Block ^ Wann wird er ausgeführt? ^
| ''try'' | Immer — enthält den "normalen" Code |
| ''catch'' | Nur wenn in ''try'' ein Fehler geworfen wird |
| ''finally'' | Immer — egal ob Erfolg oder Fehler
|
==== Die Besonderheit von fetch(): response.ok ====
''fetch()'' wirft bei HTTP-Fehlercodes **keinen** JavaScript-Fehler. Ein ''404 Not Found'' oder ''500 Server Error'' landet **nicht** automatisch im ''catch''-Block — aus Sicht von ''fetch()'' war die Kommunikation erfolgreich, es kam ja eine Antwort.
Deshalb prüfen wir ''response.ok'' manuell und werfen mit ''throw'' selbst einen Fehler:
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP-Fehler! Status: ${response.status}`);
// throw leitet den Fehler weiter in den catch-Block
}
''throw'' unterbricht die Funktion sofort und springt in den ''catch''-Block — genau wie ein natürlicher JavaScript-Fehler.
===== 6. Das Drei-Zustands-Muster =====
Beim Laden von Daten gibt es immer drei mögliche Zustände. Gutes UI-Design zeigt dem Benutzer jeden davon:
┌──────────────────────────────────────────────────────┐
│ Zustand 1: LADEN isLoading = true │
│ → Spinner / "Daten werden geladen..." │
├──────────────────────────────────────────────────────┤
│ Zustand 2: FEHLER error = "Fehlermeldung" │
│ → Fehlermeldung anzeigen │
├──────────────────────────────────────────────────────┤
│ Zustand 3: DATEN DA faqItems = [...] │
│ → Inhalte rendern │
└──────────────────────────────────────────────────────┘
In Vue werden diese drei Zustände als reaktive Variablen modelliert:
const faqItems = ref([]); // die eigentlichen Daten
const isLoading = ref(false); // lädt gerade?
const error = ref(null); // Fehlermeldung (null = kein Fehler)
Im Template werden die drei Zustände mit ''v-if / v-else-if / v-else'' gesteuert:
Daten werden geladen...
{{ error }}
===== 7. Die vollständige fetchFaqs-Funktion =====
Alle Konzepte zusammen — das ist das vollständige Muster für eine fetch-Funktion in Vue:
import { ref, onMounted } from 'vue';
const faqItems = ref([]);
const isLoading = ref(false);
const error = ref(null);
const fetchFaqs = async () => {
isLoading.value = true; // Laden beginnt
error.value = null; // allfälligen früheren Fehler zurücksetzen
try {
const response = await fetch(
'https://...mockapi.io/faqs' // ← Ihren Endpoint eintragen
);
if (!response.ok) {
throw new Error(`HTTP-Fehler! Status: ${response.status}`);
}
const data = await response.json();
faqItems.value = data; // ← Vue updated das Template automatisch
} catch (err) {
error.value = 'Daten konnten nicht geladen werden: ' + err.message;
} finally {
isLoading.value = false; // Laden beendet — egal ob Erfolg oder Fehler
}
};
onMounted(fetchFaqs); // Funktion aufrufen sobald Komponente bereit ist
===== 8. Warum onMounted? =====
''onMounted'' ist ein Vue-Lifecycle-Hook: Er wird ausgeführt, sobald die Komponente das erste Mal ins DOM eingehängt wurde.
setup() → Komponente wird initialisiert
→ Template wird gerendert (DOM ist bereit)
onMounted() → wird jetzt aufgerufen ← fetch startet hier
''onMounted'' läuft garantiert nur im Browser — nicht auf dem Server (relevant bei Server-Side Rendering mit z. B. Nuxt.js). Es ist ausserdem die konventionelle Stelle für alles, das nach dem ersten Render passieren soll.
===== 9. API-Strukturen vergleichen =====
Verschiedene APIs strukturieren ihre JSON-Antworten unterschiedlich. Immer zuerst ''console.log(data)'' aufrufen und die Struktur in der Konsole inspizieren:
^ API ^ Antwort-Struktur ^ Zugriff auf die Daten ^
| MockAPI.io | Daten direkt als Array | ''faqItems.value = data'' |
| Sanity.io | Daten unter ''result'' | ''faqItems.value = data.result'' |
| Andere | Evtl. ''data.items'', ''data.faqs'' etc. | Konsole prüfen |
// MockAPI — Daten direkt:
// [ { id: "1", question: "...", answer: "..." }, ... ]
faqItems.value = data;
// Sanity.io — Daten verschachtelt:
// { result: [ { _id: "faq-1", question: "...", answer: "..." }, ... ] }
faqItems.value = data.result;
===== 10. Testen im Browser =====
Die Browser DevTools sind das wichtigste Werkzeug beim Entwickeln mit ''fetch()'':
**Network-Reiter (F12 → Network):**
* Zeigt alle HTTP-Requests der Seite
* Status-Code des Requests prüfen (''200''?)
* Unter **Response** das rohe JSON inspizieren
* Unter **Timing** die Ladezeit sehen
**Fehlerfall simulieren:**
* //URL absichtlich falsch schreiben// → Server antwortet mit ''404'', ''response.ok'' ist ''false''
* //Network → Offline stellen// → ''fetch()'' kann Server nicht erreichen, ''catch'' greift
* //Network → Slow 3G// → Ladezustand (Spinner) wird sichtbar
===== Weiterführende Ressourcen =====
* 📺 [[https://www.youtube.com/playlist?list=PL4cUxeGkcC9jx2TTZk3IGWKSbtugYdrlu|Net Ninja – Async JavaScript]] (Playlist, 11 Videos, Englisch) — Empfehlung: Video 1, 7 und 9
* 📖 [[https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch|MDN – Fetch API]]
* 📖 [[https://developer.mozilla.org/en-US/docs/Web/API/Response|MDN – Response Objekt]]
* 📖 [[https://vuejs.org/guide/essentials/lifecycle.html|Vue.js – Lifecycle Hooks]]