Abschnitt 10 · Lektion 6
Synthese: die Seite auslesen
Dieser Abschnitt endet dort, wo Abschnitt 7 aufgehört hat: bei einem Array aus Objekten. Der Unterschied ist nur, woher die Daten kommen. Diesmal stehen sie schon auf der Seite.
Zwei Schritte statt einem
<article class="kurs" data-id="html" data-preis-cent="0"><h2>HTML</h2></article>
<article class="kurs" data-id="js" data-preis-cent="4900"><h2>JavaScript</h2></article> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
h2 {
font-size: 1.2rem;
margin: 1.25rem 0 0.5rem;
} // So nicht: Suchen, Umwandeln und Rechnen in einer Zeile.
let summe = 0;
document.querySelectorAll(".kurs").forEach((el) => {
summe = summe + Number(el.dataset.preisCent);
});
console.log("vermischt:", summe);
// So: erst eine Datenstruktur, dann die Rechnung.
const kurse = Array.from(document.querySelectorAll(".kurs")).map((el) => ({
id: el.dataset.id,
preis: Number(el.dataset.preisCent),
}));
console.log("Daten:", kurse);
const gesamt = kurse.reduce((s, kurs) => s + kurs.preis, 0);
console.log("gerechnet:", gesamt);
// Der zweite Weg ist länger und hat zwei Vorteile: man sieht
// die Daten in der Console, und die Rechnung kommt ohne DOM aus. Die Versuchung ist, alles in einem Rutsch zu machen: suchen, umwandeln, rechnen. Das geht, ist kürzer und hat zwei Nachteile, die man erst später merkt.
Der erste: du siehst die Daten nie. Wenn das Ergebnis nicht stimmt, hast du eine Zahl und keine Ahnung, an welcher Stelle sie schiefging. Mit einem Zwischenschritt steht die ganze Struktur in der Console, aufklappbar Eintrag für Eintrag, und ein falsches data--Attribut fällt sofort auf.
Der zweite: die Rechnung hängt am DOM. Solange document mitten in der Summe steht, lässt sie sich nicht ohne Seite ausprobieren, nicht wiederverwenden und nicht einzeln prüfen.
Mach also erst Daten, dann rechne. Das ist derselbe Gedanke wie in Lektion 8.5: eine klare Grenze zwischen „schmutzig” und „belastbar”, nur dass die Unordnung hier aus dem Markup kommt statt aus einer Eingabe.
Eine Funktion je Element
<article class="kurs" data-id="html" data-preis-cent="0" data-frei="true"><h2>HTML</h2></article>
<article class="kurs" data-id="js" data-preis-cent="4900" data-frei="false"><h2>JavaScript</h2></article> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
h2 {
font-size: 1.2rem;
margin: 1.25rem 0 0.5rem;
} // Eine Funktion, die aus einem Element einen Datensatz macht.
// Alle Umwandlungen stehen hier und sonst nirgends.
function alsKurs(el) {
return {
id: el.dataset.id,
titel: el.querySelector("h2").textContent,
preis: Number(el.dataset.preisCent),
frei: el.dataset.frei === "true",
};
}
const kurse = Array.from(document.querySelectorAll(".kurs")).map(alsKurs);
console.log(kurse);
// Ab hier ist alles Abschnitt 6 und 7, ohne ein einziges
// document.
console.log("Titel:", kurse.map((k) => k.titel));
console.log("frei:", kurse.filter((k) => k.frei).map((k) => k.id));
console.log("Summe:", kurse.reduce((s, k) => s + k.preis, 0)); Die saubere Form ist eine kleine Funktion, die aus einem Element einen Datensatz macht, dazu ein Array.from(...).map(...) darüber.
In dieser Funktion stehen dann alle Umwandlungen an einer Stelle: das Number(...) für den Preis, das === "true" für den Wahrheitswert, das querySelector("h2").textContent für den Titel. Wer später wissen will, wie ein Feld entsteht, schaut an genau eine Stelle.
Und weil map eine Funktion entgegennimmt, kannst du sie direkt übergeben: map(alsKurs) statt map((el) => alsKurs(el)). Das ist Lektion 5.7, und hier passt es zum ersten Mal wirklich.
Ab dieser Zeile kommt kein document mehr vor. Alles Weitere ist Abschnitt 6 und 7, und genau das ist das Ziel.
Und weil die Funktion ein Element nimmt und einen Datensatz zurückgibt, kannst du sie einzeln fragen: alsKurs(document.querySelector(".kurs")) in der Eingabezeile der Console antwortet {id: "html", titel: "HTML", preis: 0, frei: true}. Ein Element hinein, ein Datensatz heraus, ohne den Rest des Programms. kurse allein getippt zeigt die ganze Liste, aufklappbar.
Der Gewinn zeigt sich beim Umbau
<ul class="liste">
<li data-id="html" data-preis-cent="0">HTML</li>
<li data-id="js" data-preis-cent="4900">JavaScript</li>
</ul> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
} // Dasselbe Markup wie vorher, nur ganz anders gebaut:
// eine Liste statt Artikeln, der Titel direkt im Element.
function alsKurs(el) {
return {
id: el.dataset.id,
titel: el.textContent,
preis: Number(el.dataset.preisCent),
};
}
const kurse = Array.from(document.querySelectorAll(".liste li")).map(alsKurs);
console.log(kurse);
// Und die Auswertung darunter ist Wort für Wort dieselbe wie
// im Beispiel davor. Nur die eine Funktion oben hat sich
// geändert, und das ist der ganze Gewinn.
console.log("Titel:", kurse.map((k) => k.titel));
console.log("Summe:", kurse.reduce((s, k) => s + k.preis, 0)); Das dritte Beispiel zeigt dieselben Daten in völlig anderem Markup: eine Liste statt Artikeln, der Titel direkt im Element statt in einer Überschrift.
Die Auswertung darunter ist Wort für Wort dieselbe. Geändert hat sich nur die eine Funktion oben. Wäre das document über die ganze Rechnung verteilt, müsstest du jede Zeile anfassen.
Das ist kein theoretischer Vorteil. Markup ändert sich häufiger als Logik, meistens weil jemand das Aussehen anpasst, und dabei soll die Auswertung nicht kaputtgehen.
Nebenbei: diese Richtung kommt überhaupt nur vor, weil Daten manchmal schon auf der Seite stehen. Bei serverseitig gerendertem Markup ist das der Normalfall, und dann ist es meistens besser, sie von dort zu lesen, als sie ein zweites Mal aus dem Netz zu holen.
Und wenn nichts da ist
<h1>Eine Seite ganz ohne Kurse</h1> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
h1 {
font-size: 1.5rem;
margin: 0 0 0.75rem;
} function alsKurs(el) {
return { id: el.dataset.id, preis: Number(el.dataset.preisCent) };
}
const kurse = Array.from(document.querySelectorAll(".kurs")).map(alsKurs);
console.log("Anzahl:", kurse.length);
// Alles Weitere läuft durch, weil eine leere Liste harmlos ist.
console.log("Titel:", kurse.map((k) => k.id));
console.log("Summe mit Startwert:", kurse.reduce((s, k) => s + k.preis, 0));
// Nur reduce ohne Startwert bricht ab. Diese Zeile steht
// deshalb ganz unten.
console.log("ohne Startwert:", kurse.reduce((s, k) => s + k.preis));
console.log("Diese Zeile wird nie erreicht"); Weil querySelectorAll eine leere Liste liefert und nicht null, läuft die ganze Kette auch dann durch, wenn kein einziges Element passt. Array.from liefert ein leeres Array, map ebenfalls, filter ebenfalls.
Die einzige Stelle, die dann abbricht, ist ein reduce ohne Startwert. Das ist die Warnung aus Lektion 6.7, und hier trifft sie öfter, als man denkt: eine Seite ohne Einträge ist ein völlig normaler Zustand, etwa direkt nach dem Laden oder nach dem Filtern.
Ein Startwert kostet nichts und erspart dir diesen Fall. Schreib ihn hin.
Zum Mitnehmen
Mach aus dem DOM zuerst Daten und rechne dann. Danach ist die Rechnung prüfbar und das Markup austauschbar, und beides war es vorher nicht.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe im Editor, direkt im Browser geprüft
Öffnet sich mit dem Basis Konto.