mitmario.dev

Ladezustände anzeigen

JavaScript Im Browser 3 Min Lesezeit 3 BeispieleLektion 4 von 7

Zwischen dem Klick und den Daten liegt Zeit. Was in dieser Zeit auf dem Bildschirm steht, ist keine Nebensache: Es entscheidet darüber, ob jemand denkt, die Seite sei kaputt.

Vier Zustände, nicht zwei

Die vier Zustände nacheinander
const statusFeld = document.getElementById("status");
const liste = document.getElementById("liste");

function verzoegern(ms) {
  return new Promise((fertig) => setTimeout(fertig, ms));
}

function zeichnen(titel) {
  liste.innerHTML = "";
  for (const eintrag of titel) {
    const li = document.createElement("li");
    li.textContent = eintrag;
    liste.append(li);
  }
}

async function vorfuehren() {
  statusFeld.textContent = "Lädt ...";
  await verzoegern(700);

  zeichnen(["HTML", "CSS", "JavaScript"]);
  statusFeld.textContent = "";
  await verzoegern(900);

  zeichnen([]);
  statusFeld.textContent = "Keine Einträge";
  await verzoegern(900);

  zeichnen([]);
  statusFeld.textContent = "Die Liste konnte nicht geladen werden.";
}

document.getElementById("los").addEventListener("click", vorfuehren);

Die meisten bauen zwei: Es lädt, dann ist es da. Tatsächlich sind es vier.

Es lädt. Etwas ist unterwegs. Der Nutzer sieht, dass gearbeitet wird.

Es ist da. Die Daten stehen auf der Seite.

Es ist da und leer. Der Server hat geantwortet, alles hat funktioniert, es gibt nur nichts anzuzeigen. Ein Filter, der nichts übriglässt. Eine Suche ohne Treffer.

Es ist schiefgegangen. Verbindung weg, Server kaputt, Adresse falsch.

Der dritte ist der, den man vergisst, und er ist der ärgerlichste: Wenn die leere Liste aussieht wie der Ladezustand, der nie endet, hat der Nutzer keine Chance zu erkennen, was los ist.

Im ersten Beispiel siehst du alle vier hintereinander. Achte darauf, wie unterschiedlich „leer” und „fehlgeschlagen” wirken, obwohl in beiden Fällen nichts in der Liste steht.

Die Wartezeiten darin sind eingebaut, dort steht gar kein fetch. Das musste so sein: Die Übungsdaten liegen gleich nebenan. Geh in Lektion 16.2 zurück und sieh dir im Reiter Netzwerk die Dauer der echten Anfrage an, dann weißt du warum. Dort stehen fünf oder sechs Millisekunden für 637 Byte. In dieser Zeit ist ein Ladehinweis nicht zu sehen, er wäre da und wieder weg, bevor ein Auge ihn erfasst.

Auf einem Handy in einem vollen Zug sind es zwei Sekunden, und dafür baust du ihn.

Bau alle vier von Anfang an. Nachträglich einen Zustand einzuziehen bedeutet, jede Stelle wiederzufinden, an der die Ansicht etwas schreibt.

finally ist hier zu Hause

finally räumt in jedem Fall auf
const statusFeld = document.getElementById("status");

async function laden(knopf, adresse) {
  statusFeld.textContent = "Lädt ...";
  knopf.disabled = true;
  console.log("Knopf gesperrt:", knopf.id);

  try {
    const antwort = await fetch(adresse);
    if (!antwort.ok) {
      throw new Error(`Server meldet ${antwort.status}`);
    }
    const daten = await antwort.json();
    statusFeld.textContent = `${daten.length} Einträge geladen.`;
  } catch (fehler) {
    statusFeld.textContent = "Konnte nicht geladen werden.";
    console.warn("Fehlgeschlagen:", fehler.message);
  } finally {
    // Läuft in beiden Fällen. Ohne das bliebe der Knopf nach
    // einem Fehler für immer gesperrt.
    knopf.disabled = false;
    console.log("Knopf wieder frei:", knopf.id);
  }
}

const gut = document.getElementById("gut");
const kaputt = document.getElementById("kaputt");

gut.addEventListener("click", () => laden(gut, "/lernen/api/kurse.json"));
kaputt.addEventListener("click", () => laden(kaputt, "/lernen/api/gibtesnicht.json"));

In Lektion 14.2 stand finally etwas verloren da. Hier bekommt es seine Aufgabe.

Was beim Laden angeschaltet wird, muss danach wieder ausgeschaltet werden, und zwar unabhängig davon, wie es ausgegangen ist. Der Ladehinweis, der gesperrte Knopf, ein Drehsymbol: Alles davon gehört ins finally.

Ohne finally schreibst du dieselbe Zeile zweimal, einmal am Ende des try und einmal im catch. Und beim dritten Mal vergisst du eine davon, und dann bleibt der Knopf nach einem Fehler für immer gesperrt.

Warum der Knopf gesperrt wird

Die langsamere Antwort gewinnt
const ergebnis = document.getElementById("ergebnis");

function verzoegern(ms) {
  return new Promise((fertig) => setTimeout(fertig, ms));
}

async function laden(adresse, dauer, name) {
  console.log("Start:", name);
  await verzoegern(dauer);

  const antwort = await fetch(adresse);
  const daten = await antwort.json();

  console.log("Fertig:", name);
  // Wer zuletzt schreibt, gewinnt. Und das ist nicht der,
  // der zuletzt geklickt hat.
  ergebnis.textContent = `${name}: ${daten.length} Einträge`;
}

document.getElementById("langsam").addEventListener("click", () => {
  laden("/lernen/api/katalog.json", 1200, "langsam");
});

document.getElementById("schnell").addEventListener("click", () => {
  laden("/lernen/api/kurse.json", 100, "schnell");
});

Ein Knopf, der während des Ladens klickbar bleibt, wird auch geklickt. Dann sind zwei Anfragen unterwegs, und beide schreiben am Ende in dasselbe Element.

Das Problem daran ist nicht, dass es zwei sind. Das Problem ist die Reihenfolge: Wer zuletzt antwortet, gewinnt, und das muss nicht der sein, der zuletzt gestartet ist. Im dritten Beispiel klickst du erst auf „langsam” und dann auf „schnell”. Angezeigt wird trotzdem das Ergebnis von „langsam”, weil es später eintrifft. Der Nutzer sieht ein Ergebnis, das er nicht zuletzt angefordert hat.

Der einfachste Ausweg ist das Sperren des Knopfes. Es gibt zwei weitere:

Du merkst dir eine laufende Nummer und verwirfst jede Antwort, deren Nummer nicht die aktuelle ist.

Oder du brichst die alte Anfrage wirklich ab. Dafür gibt es den AbortController: Du erzeugst einen, gibst controller.signal an fetch mit und rufst beim nächsten Start controller.abort() auf. Die alte Anfrage wird dann abgelehnt, und zwar mit einem Fehler namens AbortError, den du in deinem catch gesondert behandelst, damit ein absichtlicher Abbruch keine Fehlermeldung auf die Seite schreibt.

Der Ladehinweis, der zu früh kommt

Zum Schluss eine Feinheit, die den Unterschied zwischen ordentlich und unruhig macht.

Wenn die Antwort in 40 Millisekunden da ist, blitzt ein sofort eingeblendeter Ladehinweis kurz auf und verschwindet wieder. Das wirkt hektischer als gar kein Hinweis.

Deshalb wartet man mit dem Einblenden ungefähr 200 Millisekunden: setTimeout beim Start, clearTimeout beim Eintreffen der Antwort. Ist sie schnell da, war der Hinweis nie sichtbar. Dauert es länger, erscheint er.

In der Aufgabe zu dieser Lektion lassen wir das weg und setzen den Hinweis sofort, damit die Prüfung ihn sehen kann. In einer echten Anwendung ist es die Zeile, die den Unterschied macht.

Zum Mitnehmen

Jede ladende Ansicht hat vier Zustände, nicht zwei. Der vergessene ist: fertig, aber leer.

Jetzt du

Basis Konto, kostenlos

Zu 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 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.