Modernes clientseitiges Routing: die Navigations-API

Standardisierung des clientseitigen Routings über eine brandneue API, die das Erstellen von Single-Page-Anwendungen komplett überarbeitet

Archibald
Jake Archibald

Unterstützte Browser

  • 102
  • 102
  • x
  • x

Quelle

Single-Page-Anwendungen (SPAs) werden durch eine zentrale Funktion definiert: Die Inhalte werden dynamisch neu geschrieben, wenn der Nutzer mit der Website interagiert, anstelle der Standardmethode, bei der komplett neue Seiten vom Server geladen werden.

SPAs konnten diese Funktion über die History API (oder in bestimmten Fällen durch Anpassen des #hash-Teils) der Website bereitstellen. Es handelt sich jedoch um eine umständliche API, die lange vor der Einführung von SPAs entwickelt wurde – und das Web schreit nach einem völlig neuen Ansatz. Das Navigations-API ist ein vorgeschlagenes API, das diesen Bereich komplett überarbeitet, anstatt zu versuchen, nur die Ränder des History API zu reparieren. Mit Scroll Wiederherstellung wurde die History API beispielsweise repariert, anstatt zu versuchen, sie neu zu erfinden.

In diesem Beitrag wird die Navigation API allgemein beschrieben. Den technischen Vorschlag finden Sie im Berichtentwurf im WICG-Repository.

Verwendungsbeispiel

Fügen Sie dem globalen navigation-Objekt zuerst einen "navigate"-Listener hinzu, um die Navigation API zu verwenden. Dieses Ereignis ist im Wesentlichen zentral: Es wird bei allen Arten von Navigationen ausgelöst, unabhängig davon, ob der Nutzer eine Aktion ausgeführt hat (z. B. auf einen Link klicken, ein Formular senden oder vor- und zurückspringen) oder wenn die Navigation programmatisch ausgelöst wird (d. h. über den Code Ihrer Website). In den meisten Fällen lässt Ihr Code das Standardverhalten des Browsers für diese Aktion überschreiben. Für SPAs bedeutet dies wahrscheinlich, dass der Nutzer auf dem gleichen Stand bleibt und der Content der Website geladen oder geändert wird.

Ein NavigateEvent wird an den "navigate"-Listener übergeben, der Informationen zur Navigation (z. B. die Ziel-URL) enthält, und ermöglicht es Ihnen, an einem zentralen Ort auf die Navigation zu reagieren. Ein einfacher "navigate"-Listener könnte so aussehen:

navigation.addEventListener('navigate', navigateEvent => {
  // Exit early if this navigation shouldn't be intercepted.
  // The properties to look at are discussed later in the article.
  if (shouldNotIntercept(navigateEvent)) return;

  const url = new URL(navigateEvent.destination.url);

  if (url.pathname === '/') {
    navigateEvent.intercept({handler: loadIndexPage});
  } else if (url.pathname === '/cats/') {
    navigateEvent.intercept({handler: loadCatsPage});
  }
});

Sie haben zwei Möglichkeiten, die Navigation zu bearbeiten:

  • Für die Navigation wird wie oben beschrieben intercept({ handler }) aufgerufen.
  • preventDefault() wird aufgerufen, wodurch die Navigation vollständig abgebrochen werden kann.

In diesem Beispiel wird intercept() für das Ereignis aufgerufen. Der Browser ruft deinen handler-Callback auf, der den nächsten Status deiner Website konfigurieren sollte. Dadurch wird das Übergangsobjekt navigation.transition erstellt, mit dem anderer Code den Fortschritt der Navigation verfolgen kann.

Sowohl intercept() als auch preventDefault() sind in der Regel zulässig, können jedoch in bestimmten Fällen nicht aufgerufen werden. Du kannst keine Navigationen über intercept() verarbeiten, wenn es sich um eine ursprungsübergreifende Navigation handelt. Außerdem können Sie eine Navigation über preventDefault() nicht abbrechen, wenn der Nutzer die Schaltflächen „Zurück“ oder „Weiter“ in seinem Browser drückt. Sie sollten Ihre Nutzer auf Ihrer Website nicht eingeschlossen können. (Dies wird auf GitHub besprochen.)

Auch wenn Sie die Navigation nicht beenden oder abfangen können, wird das "navigate"-Ereignis ausgelöst. Er ist informativ, d. h., Ihr Code könnte zum Beispiel ein Analytics-Ereignis protokollieren, das anzeigt, dass ein Nutzer Ihre Website verlässt.

Warum sollten Sie der Plattform ein weiteres Ereignis hinzufügen?

Ein "navigate"-Event-Listener zentralisiert die Verarbeitung von URL-Änderungen in einer SPA. Dies ist bei älteren APIs eine schwierige Aufgabe. Wenn Sie schon einmal das Routing für Ihre eigene SPA mit der History API geschrieben haben, haben Sie möglicherweise Code wie den folgenden hinzugefügt:

function updatePage(event) {
  event.preventDefault(); // we're handling this link
  window.history.pushState(null, '', event.target.href);
  // TODO: set up page based on new URL
}
const links = [...document.querySelectorAll('a[href]')];
links.forEach(link => link.addEventListener('click', updatePage));

Das ist in Ordnung, aber nicht vollständig. Links können auf Ihrer Seite kommen und gehen. Sie sind nicht die einzige Möglichkeit, durch die Nutzer zu navigieren. Sie können beispielsweise ein Formular einreichen oder sogar eine Bilderkarte verwenden. Ihre Seite könnte mit diesen Problemen umgehen, aber es gibt eine ganze Reihe von Möglichkeiten, die einfach vereinfacht werden könnten – etwas, was mit der neuen Navigation API erreicht wird.

Außerdem wird die Vorwärts- und Zurück-Navigation nicht unterstützt. Hier gibt es noch ein Ereignis dafür: "popstate".

Die History API glaubt oft, dass sie bei diesen Möglichkeiten hilfreich sein könnte. In Wirklichkeit gibt es jedoch nur zwei Oberflächen: eine Reaktion, wenn der Nutzer im Browser "Zurück" oder "Weiter" drückt, sowie das Verschieben und Ersetzen von URLs. Es gibt keine Analogie zu "navigate", es sei denn, Sie richten Listener für Klickereignisse wie oben gezeigt manuell ein.

Entscheiden, wie eine Navigation zu behandeln ist

Das navigateEvent enthält viele Informationen zur Navigation, die Sie verwenden können, um zu entscheiden, wie mit einer bestimmten Navigation umzugehen ist.

Die wichtigsten Eigenschaften sind:

canIntercept
Wenn dieser Wert auf „false“ gesetzt ist, kann die Navigation nicht abgefangen werden. Ursprungsübergreifende Navigationen und dokumentübergreifende Durchläufe können nicht abgefangen werden.
destination.url
Das ist wahrscheinlich die wichtigste Information, die Sie bei der Navigation berücksichtigen sollten.
hashChange
„True“, wenn die Navigation zum selben Dokument führt und der Hash der einzige Teil der URL ist, der sich von der aktuellen URL unterscheidet. In modernen SPAs sollte der Hash für die Verknüpfung mit verschiedenen Teilen des aktuellen Dokuments bestimmt sein. Wenn hashChange also „true“ ist, müssen Sie diese Navigation wahrscheinlich nicht abfangen.
downloadRequest
Wenn dies „true“ ist, wurde die Navigation über einen Link mit einem download-Attribut eingeleitet. In den meisten Fällen müssen Sie dies nicht abfangen.
formData
Wenn dieser Wert nicht null ist, ist diese Navigation Teil einer POST-Formularübermittlung. Berücksichtigen Sie dies bei der Navigation. Wenn Sie nur GET-Navigationen verarbeiten möchten, sollten Sie keine Navigationen abfangen, bei denen formData nicht null ist. Das Beispiel zum Einreichen von Formularen finden Sie weiter unten im Artikel.
navigationType
Dies ist entweder "reload", "push", "replace" oder "traverse". Wenn es sich um "traverse" handelt, kann diese Navigation nicht über preventDefault() abgebrochen werden.

Die im ersten Beispiel verwendete shouldNotIntercept-Funktion könnte beispielsweise so aussehen:

function shouldNotIntercept(navigationEvent) {
  return (
    !navigationEvent.canIntercept ||
    // If this is just a hashChange,
    // just let the browser handle scrolling to the content.
    navigationEvent.hashChange ||
    // If this is a download,
    // let the browser perform the download.
    navigationEvent.downloadRequest ||
    // If this is a form submission,
    // let that go to the server.
    navigationEvent.formData
  );
}

Abfangen

Wenn Ihr Code intercept({ handler }) aus seinem "navigate"-Listener aufruft, wird der Browser darüber informiert, dass die Seite jetzt auf den neuen, aktualisierten Status vorbereitet wird und dass die Navigation einige Zeit dauern kann.

Im Browser wird zuerst die Scrollposition für den aktuellen Status erfasst, damit sie später optional wiederhergestellt werden kann. Anschließend ruft er Ihren handler-Callback auf. Wenn dein handler ein Promise zurückgibt (was automatisch mit async functions geschieht), teilt dieses Versprechen dem Browser mit, wie lange die Navigation dauert und ob sie erfolgreich ist.

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
      },
    });
  }
});

Daher führt diese API ein semantisches Konzept ein, das der Browser versteht: Derzeit findet im Laufe der Zeit eine SPA-Navigation statt, bei der das Dokument von einer vorherigen URL und einem Status in eine neue geändert wird. Dies hat eine Reihe potenzieller Vorteile, unter anderem die Barrierefreiheit: Browser können den Anfang, das Ende oder einen möglichen Fehler einer Navigation anzeigen. So aktiviert Chrome zum Beispiel die native Ladeanzeige und ermöglicht dem Nutzer, mit der Stopp-Schaltfläche zu interagieren. Dies ist derzeit nicht der Fall, wenn der Nutzer über die Zurück-/Vorwärts-Schaltflächen navigiert. Das Problem wird aber bald behoben.

Wenn Navigationen abgefangen werden, wird die neue URL kurz vor dem handler-Callback wirksam. Wenn Sie das DOM nicht sofort aktualisieren, wird dadurch ein Zeitraum erstellt, in dem der alte Inhalt zusammen mit der neuen URL angezeigt wird. Dies wirkt sich beispielsweise auf die relative URL-Auflösung beim Abrufen von Daten oder Laden neuer Unterressourcen aus.

Eine Möglichkeit, die URL-Änderung zu verzögern, wird auf GitHub erläutert. Generell empfehlen wir jedoch, die Seite sofort mit einem Platzhalter für den eingehenden Inhalt zu aktualisieren:

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        // The URL has already changed, so quickly show a placeholder.
        renderArticlePagePlaceholder();
        // Then fetch the real data.
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
      },
    });
  }
});

Dadurch werden nicht nur Probleme bei der URL-Auflösung vermieden, es fühlt sich auch schnell an, weil Sie dem Nutzer sofort antworten.

Signale abbrechen

Da Sie asynchrone Arbeiten in einem intercept()-Handler ausführen können, kann die Navigation redundant werden. Dies geschieht in folgenden Fällen:

  • Der Nutzer klickt auf einen anderen Link oder über Code wird eine weitere Navigation durchgeführt. In diesem Fall wird die alte Navigation zugunsten der neuen Navigation aufgegeben.
  • Der Nutzer klickt im Browser auf die Stopp-Schaltfläche.

Um all diese Möglichkeiten zu berücksichtigen, enthält das an den "navigate"-Listener übergebene Ereignis eine signal-Eigenschaft, die ein AbortSignal ist. Weitere Informationen finden Sie unter Abortable Fetch.

In der Kurzversion wird im Grunde ein Objekt bereitgestellt, das ein Ereignis auslöst, wenn Sie Ihre Arbeit beenden sollten. Vor allem können Sie ein AbortSignal an alle Aufrufe an fetch() übergeben. Dadurch werden laufende Netzwerkanfragen abgebrochen, wenn die Navigation vorzeitig beendet wird. Dadurch wird die Bandbreite des Nutzers gespart und das von fetch() zurückgegebene Promise wird abgelehnt. Dadurch wird der folgende Code von Aktionen wie das Aktualisieren des DOMs zur Anzeige einer nun ungültigen Seitennavigation verhindert.

Hier das vorherige Beispiel mit eingefügtem getArticleContent, das zeigt, wie AbortSignal mit fetch() verwendet werden kann:

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        // The URL has already changed, so quickly show a placeholder.
        renderArticlePagePlaceholder();
        // Then fetch the real data.
        const articleContentURL = new URL(
          '/get-article-content',
          location.href
        );
        articleContentURL.searchParams.set('path', url.pathname);
        const response = await fetch(articleContentURL, {
          signal: navigateEvent.signal,
        });
        const articleContent = await response.json();
        renderArticlePage(articleContent);
      },
    });
  }
});

Scroll-Handhabung

Wenn Sie eine Navigation intercept(), versucht der Browser, das Scrollen automatisch zu handhaben.

Beim Aufrufen eines neuen Verlaufseintrags (wenn navigationEvent.navigationType den Wert "push" oder "replace" hat), bedeutet dies, dass versucht wird, zu dem Teil zu scrollen, der durch das URL-Fragment (das Bit nach dem #) angegeben ist, oder den Scrollvorgang zum Anfang der Seite zurückzusetzen.

Beim Aktualisieren und Durchsuchen bedeutet dies, dass die Scrollposition an der Stelle wiederhergestellt wird, an der dieser Verlaufseintrag zuletzt angezeigt wurde.

Das passiert standardmäßig, sobald das von handler zurückgegebene Versprechen aufgelöst ist. Wenn es jedoch sinnvoll ist, früher zu scrollen, kannst du navigateEvent.scroll() aufrufen:

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
        navigateEvent.scroll();

        const secondaryContent = await getSecondaryContent(url.pathname);
        addSecondaryContent(secondaryContent);
      },
    });
  }
});

Alternativ kannst du die automatische Scroll-Verarbeitung auch komplett deaktivieren, indem du die scroll-Option von intercept() auf "manual" setzt:

navigateEvent.intercept({
  scroll: 'manual',
  async handler() {
    // …
  },
});

Fokus-Handhabung

Sobald das von deinem handler zurückgegebene Versprechen aufgelöst ist, fokussiert der Browser das erste Element mit festgelegtem autofocus-Attribut oder das <body>-Element, wenn kein Element dieses Attribut hat.

Sie können dieses Verhalten deaktivieren, indem Sie die focusReset-Option von intercept() auf "manual" setzen:

navigateEvent.intercept({
  focusReset: 'manual',
  async handler() {
    // …
  },
});

Erfolgs- und Fehlerereignisse

Wenn der intercept()-Handler aufgerufen wird, geschieht Folgendes:

  • Wenn die zurückgegebene Promise erfüllt (oder Sie nicht intercept() aufgerufen haben), löst die Navigation API "navigatesuccess" mit einer Event aus.
  • Wenn das zurückgegebene Promise abgelehnt wird, löst die API "navigateerror" mit einer ErrorEvent aus.

Mithilfe dieser Ereignisse kann Ihr Code zentral mit Erfolg oder Misserfolg umgehen. Beispielsweise könnten Sie eine zuvor angezeigte Fortschrittsanzeige wie folgt ausblenden, um Erfolg zu haben:

navigation.addEventListener('navigatesuccess', event => {
  loadingIndicator.hidden = true;
});

Oder Sie erhalten bei einem Fehler eine Fehlermeldung:

navigation.addEventListener('navigateerror', event => {
  loadingIndicator.hidden = true; // also hide indicator
  showMessage(`Failed to load page: ${event.message}`);
});

Der "navigateerror"-Event-Listener, der ein ErrorEvent-Objekt empfängt, ist besonders praktisch, da er garantiert alle Fehler von Ihrem Code empfängt, der eine neue Seite einrichtet. Sie können einfach await fetch() in dem Wissen verwenden, dass der Fehler schließlich an "navigateerror" weitergeleitet wird, wenn das Netzwerk nicht verfügbar ist.

navigation.currentEntry bietet Zugriff auf den aktuellen Eintrag. Dies ist ein Objekt, das beschreibt, wo sich der Nutzer gerade befindet. Dieser Eintrag enthält die aktuelle URL, Metadaten, mit denen dieser Eintrag im Laufe der Zeit identifiziert werden kann, und den vom Entwickler bereitgestellten Status.

Die Metadaten enthalten key, ein eindeutiges Stringattribut jedes Eintrags, das den aktuellen Eintrag und seinen Bereich darstellt. Dieser Schlüssel bleibt auch dann erhalten, wenn sich die URL oder der Status des aktuellen Eintrags ändert. Sie befindet sich immer noch an derselben Anzeigenfläche. Wenn ein Nutzer dagegen auf „Zurück“ drückt und dann dieselbe Seite noch einmal öffnet, ändert sich key, wenn durch diesen neuen Eintrag eine neue Anzeigenfläche erstellt wird.

Für Entwickler ist key nützlich, da sie mit der Navigation API den Nutzer direkt zu einem Eintrag mit einem übereinstimmenden Schlüssel leiten können. Sie können es selbst im Status anderer Einträge beibehalten, um leicht zwischen Seiten zu wechseln.

// On JS startup, get the key of the first loaded page
// so the user can always go back there.
const {key} = navigation.currentEntry;
backToHomeButton.onclick = () => navigation.traverseTo(key);

// Navigate away, but the button will always work.
await navigation.navigate('/another_url').finished;

Status

Die Navigation API stellt den Begriff „Status“ dar. Dabei handelt es sich um vom Entwickler bereitgestellte Informationen, die dauerhaft im aktuellen Verlaufseintrag gespeichert werden, aber nicht direkt für den Nutzer sichtbar sind. Dies ist history.state in der History API sehr ähnlich, wurde aber verbessert.

In der Navigation API können Sie die .getState()-Methode des aktuellen Eintrags (oder eines beliebigen Eintrags) aufrufen, um eine Kopie seines Status zurückzugeben:

console.log(navigation.currentEntry.getState());

Standardmäßig ist dies undefined.

Einstellungsstatus

Obwohl Statusobjekte mutiert werden können, werden diese Änderungen nicht mit dem Verlaufseintrag wieder gespeichert. Daher gilt:

const state = navigation.currentEntry.getState();
console.log(state.count); // 1
state.count++;
console.log(state.count); // 2
// But:
console.info(navigation.currentEntry.getState().count); // will still be 1

Die richtige Methode zum Festlegen des Status ist während der Skriptnavigation:

navigation.navigate(url, {state: newState});
// Or:
navigation.reload({state: newState});

Dabei kann newState ein beliebiges klonbares Objekt sein.

Wenn Sie den Status des aktuellen Eintrags aktualisieren möchten, sollten Sie eine Navigation ausführen, bei der der aktuelle Eintrag ersetzt wird:

navigation.navigate(location.href, {state: newState, history: 'replace'});

Dann kann der "navigate"-Event-Listener diese Änderung über navigateEvent.destination aufnehmen:

navigation.addEventListener('navigate', navigateEvent => {
  console.log(navigateEvent.destination.getState());
});

Status synchron aktualisieren

Im Allgemeinen ist es besser, den Status asynchron über navigation.reload({state: newState}) zu aktualisieren. Dann kann Ihr "navigate"-Listener diesen Zustand anwenden. Manchmal ist die Statusänderung jedoch bereits vollständig übernommen, als der Code vom Code hört, z. B. wenn der Nutzer ein <details>-Element umschaltet oder den Status einer Formulareingabe ändert. In diesen Fällen kann es sinnvoll sein, den Status zu aktualisieren, damit diese Änderungen auch bei Neuladen und Durchläufen erhalten bleiben. Dies ist mit updateCurrentEntry() möglich:

navigation.updateCurrentEntry({state: newState});

Es gibt auch eine Veranstaltung, bei der Sie über diese Änderung informiert werden:

navigation.addEventListener('currententrychange', () => {
  console.log(navigation.currentEntry.getState());
});

Wenn Sie jedoch auf Statusänderungen in "currententrychange" reagieren, können Sie den Code zur Zustandsbehandlung zwischen dem Ereignis "navigate" und dem Ereignis "currententrychange" aufteilen oder sogar duplizieren, während Sie mit navigation.reload({state: newState}) alles an einem Ort verarbeiten können.

Status und URL-Parameter im Vergleich

Da Zustand ein strukturiertes Objekt sein kann, ist es verlockend, es für den gesamten Anwendungsstatus zu verwenden. In vielen Fällen ist es jedoch besser, diesen Status in der URL zu speichern.

Wenn Sie erwarten würden, dass der Status beibehalten wird, wenn der Nutzer die URL mit einem anderen Nutzer teilt, speichern Sie ihn in der URL. Andernfalls ist das Zustandsobjekt die bessere Option.

Auf alle Einträge zugreifen

Der „aktuelle Eintrag“ ist jedoch nicht alle. Die API bietet auch eine Möglichkeit, über den navigation.entries()-Aufruf, der ein Snapshot-Array der Einträge zurückgibt, auf die gesamte Liste der Einträge zuzugreifen, die ein Nutzer bei der Verwendung Ihrer Website durchgegangen ist. So können Sie beispielsweise eine andere Benutzeroberfläche basierend auf der Art und Weise anzeigen, wie der Nutzer zu einer bestimmten Seite navigiert ist, oder um sich die vorherigen URLs oder ihren Status anzusehen. Mit der aktuellen History API ist das nicht möglich.

Sie können auch auf ein "dispose"-Ereignis für einzelne NavigationHistoryEntrys warten, das ausgelöst wird, wenn der Eintrag nicht mehr Teil des Browserverlaufs ist. Dies kann im Rahmen der allgemeinen Bereinigung, aber auch während der Navigation passieren. Wenn Sie beispielsweise zehn Orte zurückspulen und dann vorwärts navigieren, werden diese zehn Verlaufseinträge verworfen.

Beispiele

Das Ereignis "navigate" wird wie oben erwähnt für alle Navigationsarten ausgelöst. Die Spezifikation enthält für alle möglichen Typen einen langen Anhang.

Bei vielen Websites ist es am häufigsten der Fall, wenn der Nutzer auf <a href="..."> klickt. Es gibt jedoch zwei wichtige, komplexere Navigationsarten, die es sich zu lohnen lohnen.

Programmatische Navigation

Der erste ist die programmatische Navigation, bei der die Navigation durch einen Methodenaufruf in Ihrem clientseitigen Code verursacht wird.

Sie können navigation.navigate('/another_page') an einer beliebigen Stelle im Code aufrufen, um eine Navigation zu starten. Dies erfolgt über den zentralisierten Event-Listener, der auf dem "navigate"-Listener registriert ist, und der zentralisierte Listener wird synchron aufgerufen.

Dies ist eine verbesserte Aggregation älterer Methoden wie location.assign() und Freunde sowie der Methoden pushState() und replaceState() der History API.

Die Methode navigation.navigate() gibt ein Objekt zurück, das zwei Promise-Instanzen in { committed, finished } enthält. Dadurch kann der Aufrufer warten, bis der Übergang entweder „Commit“ (mit Commit) durchgeführt wurde (die sichtbare URL hat sich geändert und eine neue NavigationHistoryEntry ist verfügbar) oder „Abgeschlossen“ (alle von intercept({ handler }) zurückgegebenen Versprechen sind abgeschlossen) oder abgelehnt, da ein Fehler aufgetreten ist oder von einer anderen Navigation vorzeitig beendet wird.

Die Methode navigate verfügt auch über ein Optionsobjekt, in dem Sie Folgendes festlegen können:

  • state: der Status für den neuen Verlaufseintrag, der über die Methode .getState() in NavigationHistoryEntry verfügbar ist.
  • history: Kann auf "replace" gesetzt werden, um den aktuellen Verlaufseintrag zu ersetzen.
  • info: ein Objekt, das über navigateEvent.info an das Navigationsereignis übergeben wird.

Mit info kann beispielsweise eine bestimmte Animation gekennzeichnet werden, durch die die nächste Seite angezeigt wird. Die Alternative kann sein, eine globale Variable festzulegen oder sie als Teil des #hash. Beide Optionen sind etwas umständlich.) Vor allem wird dieses info nicht noch einmal abgespielt, wenn ein Nutzer später die Navigation auslöst, z.B. über die Schaltflächen „Zurück“ und „Weiter“. In diesen Fällen ist der Wert immer undefined.

Demo: Öffnen von links oder rechts

navigation verfügt auch über eine Reihe anderer Navigationsmethoden, die ein Objekt zurückgeben, das { committed, finished } enthält. Ich habe bereits traverseTo() (wobei ein key für einen bestimmten Eintrag im Nutzerverlauf akzeptiert) und navigate() erwähnt. Er umfasst auch back(), forward() und reload(). Diese Methoden werden alle – genau wie navigate() – vom zentralen "navigate"-Event-Listener verarbeitet.

Formulareinreichungen

Zweitens ist die HTML-<form>-Übermittlung über POST eine spezielle Art der Navigation, die von der Navigation API abgefangen werden kann. Sie enthält zwar eine zusätzliche Nutzlast, die Navigation wird aber weiterhin zentral vom Listener "navigate" abgewickelt.

Wenn ein Formular gesendet wird, suchen Sie nach der Eigenschaft formData in NavigateEvent. In diesem Beispiel wird jede Formularübermittlung per fetch() in ein Formular umgewandelt, das auf der aktuellen Seite bleibt:

navigation.addEventListener('navigate', navigateEvent => {
  if (navigateEvent.formData && navigateEvent.canIntercept) {
    // User submitted a POST form to a same-domain URL
    // (If canIntercept is false, the event is just informative:
    // you can't intercept this request, although you could
    // likely still call .preventDefault() to stop it completely).

    navigateEvent.intercept({
      // Since we don't update the DOM in this navigation,
      // don't allow focus or scrolling to reset:
      focusReset: 'manual',
      scroll: 'manual',
      handler() {
        await fetch(navigateEvent.destination.url, {
          method: 'POST',
          body: navigateEvent.formData,
        });
        // You could navigate again with {history: 'replace'} to change the URL here,
        // which might indicate "done"
      },
    });
  }
});

Was fehlt noch?

Trotz der zentralisierten Natur des "navigate"-Event-Listeners löst die aktuelle Navigation API-Spezifikation "navigate" beim ersten Laden einer Seite nicht aus. Bei Websites, die für alle Bundesstaaten serverseitiges Rendering (SSR) verwenden, ist dies möglicherweise kein Problem. Ihr Server könnte den korrekten Ausgangszustand zurückgeben. Dies ist die schnellste Möglichkeit, Inhalte für Ihre Nutzer bereitzustellen. Bei Websites, die clientseitigen Code zum Erstellen ihrer Seiten nutzen, ist jedoch möglicherweise eine zusätzliche Funktion zum Initialisieren der Seite erforderlich.

Eine weitere bewusste Wahl für die Navigation API besteht darin, dass sie nur in einem einzelnen Frame betrieben wird, d. h. auf der Seite auf oberster Ebene oder in einem einzelnen spezifischen <iframe>. Dies hat eine Reihe interessanter Auswirkungen, die in der Spezifikation näher dokumentiert, in der Praxis jedoch die Verwirrung bei den Entwicklern verringern. Das bisherige History API hat eine Reihe von verwirrenden Grenzfällen, wie etwa die Unterstützung von Frames, und das neu konzipierte Navigations-API verarbeitet diese Grenzfälle von Anfang an.

Schließlich gibt es noch keinen Konsens darüber, wie die Liste der Einträge, durch die der Nutzer navigiert ist, programmatisch geändert oder neu angeordnet werden kann. Dies wird derzeit diskutiert, es könnte jedoch sein, dass nur Löschvorgänge zugelassen werden: entweder bisherige Einträge oder alle zukünftigen Einträge. Bei letzterem ist ein vorübergehender Zustand zulässig. Als Entwickler könnte ich beispielsweise

  • Stell dem Nutzer eine Frage, indem du eine neue URL oder einen neuen Status aufrufst
  • den Nutzenden die Möglichkeit geben, ihre Arbeit abzuschließen (oder zurückzugehen)
  • Einen Verlaufseintrag nach Beendigung einer Aufgabe entfernen

Dies könnte ideal für temporäre modale Anzeigen oder Interstitials sein: Mit der neuen URL kann der Nutzer mit der Touch-Geste „Zurück“ die Seite verlassen, aber er kann dann nicht aus Versehen zur nächsten Seite gehen (weil der Eintrag entfernt wurde). Dies ist mit der aktuellen History API einfach nicht möglich.

Navigation API testen

Die Navigation API ist in Chrome 102 ohne Flags verfügbar. Sie können auch eine Demo von Domenic Denicola ausprobieren.

Die klassische History API erscheint zwar einfach, ist aber nicht sehr gut definiert. Es gibt eine große Anzahl von Problemen in Bezug auf Sonderfälle und ihre Browser-Implementierung. Wir würden uns über Ihr Feedback zum neuen Navigations-API freuen.

Verweise

Danksagungen

Vielen Dank an Thomas Steiner, Domenic Denicola und Nate Chapin für die Rezension. Hero-Image aus Unsplash von Jeremy Zero