Modernes clientseitiges Routing: die Navigations-API

Standardisierung des clientseitigen Routings durch eine brandneue API, die die Entwicklung von Single-Page-Anwendungen vollständig überholt.

Unterstützte Browser

  • Chrome: 102.
  • Edge: 102.
  • Firefox: Nicht unterstützt.
  • Safari: Nicht unterstützt.

Quelle

Single-Page-Anwendungen (SPAs) zeichnen sich durch eine Hauptfunktion aus: Sie überschreiben ihre Inhalte dynamisch, während der Nutzer mit der Website interagiert, anstatt wie bei der Standardmethode komplett neue Seiten vom Server zu laden.

SPAs konnten diese Funktion zwar über die History API (oder in seltenen Fällen durch Anpassung des #hash-Teils der Website) anbieten. Es handelt sich jedoch um eine umständliche API, die schon lange entwickelt wurde, bevor SPAs die Norm waren – und das Web schreit nach einem komplett neuen Ansatz. Die Navigation API ist eine vorgeschlagene API, die diesen Bereich komplett überarbeitet, anstatt nur die Mängel der History API zu beheben. Bei Scroll Restoration wurde beispielsweise die History API gepatcht, anstatt zu versuchen, sie neu zu erfinden.

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

Nutzungsbeispiel

Wenn Sie die Navigation API verwenden möchten, fügen Sie zuerst einen "navigate"-Listener zum globalen navigation-Objekt hinzu. Dieses Ereignis ist zentralisiert: Es wird für alle Arten von Navigationen ausgelöst, unabhängig davon, ob der Nutzer eine Aktion ausgeführt hat (z. B. auf einen Link geklickt, ein Formular gesendet oder zurück- und vorgegangen ist) oder die Navigation programmatisch ausgelöst wird (d. h. über den Code Ihrer Website). In den meisten Fällen können Sie mit diesem Code das Standardverhalten des Browsers für diese Aktion überschreiben. Bei SPAs bedeutet das wahrscheinlich, dass der Nutzer auf derselben Seite bleibt und die Websiteinhalte geladen oder geändert werden.

Dem "navigate"-Listener wird eine NavigateEvent übergeben, die Informationen zur Navigation enthält, z. B. die Ziel-URL. So können Sie an einem zentralen Ort auf die Navigation 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 steuern:

  • intercept({ handler }) wie oben beschrieben aufrufen, um die Navigation zu steuern.
  • preventDefault() wird aufgerufen. Dadurch kann die Navigation vollständig abgebrochen werden.

In diesem Beispiel wird intercept() für das Ereignis aufgerufen. Der Browser ruft deinen handler-Callback auf. Dadurch wird der nächste Status deiner Website konfiguriert. Dadurch wird ein Übergangsobjekt navigation.transition erstellt, mit dem andere Code den Fortschritt der Navigation verfolgen können.

In der Regel sind sowohl intercept() als auch preventDefault() zulässig. Es gibt jedoch Fälle, in denen sie nicht aufgerufen werden können. Navigationen über intercept() können nicht verarbeitet werden, wenn es sich um eine plattformübergreifende Navigation handelt. Außerdem kann eine Navigation über preventDefault() nicht abgebrochen werden, wenn der Nutzer in seinem Browser die Schaltflächen „Zurück“ oder „Weiter“ drückt. Sie sollten Ihre Nutzer nicht auf Ihrer Website gefangen halten können. (Dies wird auf GitHub besprochen.)

Auch wenn Sie die Navigation selbst nicht stoppen oder abfangen können, wird das "navigate"-Ereignis trotzdem ausgelöst. Er ist informativ. So kann Ihr Code beispielsweise ein Analytics-Ereignis erfassen, um anzugeben, dass ein Nutzer Ihre Website verlässt.

Warum sollte ich der Plattform ein weiteres Ereignis hinzufügen?

Mit einem "navigate"-Ereignis-Listener wird die Verarbeitung von URL-Änderungen in einer SPA zentralisiert. Dies ist bei Verwendung älterer APIs ein schwieriger Vorschlag. Wenn Sie schon einmal das Routing für Ihre eigene SPA mit der History API geschrieben haben, haben Sie möglicherweise Code wie diesen 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 hinzugefügt oder entfernt werden und sind nicht die einzige Möglichkeit, wie Nutzer zwischen Seiten wechseln können. Sie können beispielsweise ein Formular einreichen oder sogar eine Bildkarte verwenden. Auf Ihrer Seite werden diese Anforderungen möglicherweise erfüllt, aber es gibt viele Möglichkeiten, die vereinfacht werden könnten – und genau das ist mit der neuen Navigation API möglich.

Außerdem wird die Navigation zurück/vor nicht unterstützt. Dafür gibt es ein anderes Ereignis, "popstate".

Ich persönlich habe das Gefühl, dass die History API bei diesen Möglichkeiten helfen könnte. Es gibt jedoch nur zwei Oberflächenbereiche: die Reaktion, wenn der Nutzer im Browser auf „Zurück“ oder „Vor“ klickt, sowie das Senden und Ersetzen von URLs. Es gibt keine Analogie zu "navigate", es sei denn, Sie richten beispielsweise wie oben gezeigt manuell Listener für Klickereignisse ein.

Entscheiden, wie eine Navigation gehandhabt werden soll

Der navigateEvent enthält viele Informationen zur Navigation, anhand derer Sie entscheiden können, wie Sie mit einer bestimmten Navigation umgehen.

Die wichtigsten Eigenschaften sind:

canIntercept
Wenn dies nicht der Fall ist, können Sie die Navigation nicht abfangen. Ursprungsübergreifende Navigationen und dokumentübergreifende Durchläufe können nicht abgefangen werden.
destination.url
Wahrscheinlich die wichtigste Information, die Sie beim Umgang mit der Navigation berücksichtigen sollten.
hashChange
Wahr, wenn die Navigation im selben Dokument erfolgt 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 verwendet werden. Wenn also hashChange wahr ist, müssen Sie diese Navigation wahrscheinlich nicht abfangen.
downloadRequest
Wenn dies der Fall ist, wurde die Navigation durch einen Link mit einem download-Attribut initiiert. In den meisten Fällen müssen Sie diese 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 Navigationen, bei denen formData nicht null ist, nicht abfangen. Das Beispiel zum Senden von Formularen finden Sie weiter unten im Artikel.
navigationType
Das ist "reload", "push", "replace" oder "traverse". Wenn es "traverse" ist, 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 }) innerhalb des "navigate"-Listeners aufruft, wird der Browser darüber informiert, dass die Seite für den neuen, aktualisierten Status vorbereitet wird und dass die Navigation einige Zeit dauern kann.

Der Browser erfasst zuerst die Scrollposition für den aktuellen Status, damit sie optional später wiederhergestellt werden kann. Anschließend ruft er Ihren handler-Callback auf. Wenn handler ein Versprechen zurückgibt (was bei asynchroen Funktionen automatisch geschieht), wird dem Browser mitgeteilt, wie lange die Navigation dauert und ob sie erfolgreich war.

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: Es findet gerade eine SPA-Navigation statt, bei der das Dokument im Laufe der Zeit von einer vorherigen URL und einem vorherigen Status in einen neuen geändert wird. Das hat eine Reihe von potenziellen Vorteilen, einschließlich der Barrierefreiheit: Browser können den Anfang, das Ende oder einen potenziellen Fehler einer Navigation anzeigen. In Chrome wird beispielsweise der native Ladebalken aktiviert und der Nutzer kann mit der Schaltfläche „Beenden“ interagieren. Das passiert derzeit nicht, wenn der Nutzer über die Schaltflächen „Zurück“ und „Weiter“ navigiert. Das wird bald behoben.

Wenn Navigationen abgefangen werden, tritt die neue URL in Kraft, kurz bevor der handler-Callback aufgerufen wird. Wenn Sie das DOM nicht sofort aktualisieren, werden die alten Inhalte zusammen mit der neuen URL angezeigt. Dies wirkt sich auf Dinge wie die relative URL-Auflösung beim Abrufen von Daten oder beim Laden neuer Unterressourcen aus.

Eine Möglichkeit, die URL-Änderung zu verzögern, wird auf GitHub diskutiert. Es wird jedoch allgemein empfohlen, die Seite sofort mit einer Art Platzhalter für die ankommenden Inhalte 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);
      },
    });
  }
});

So lassen sich nicht nur Probleme bei der URL-Auflösung vermeiden, sondern es wirkt auch schneller, da Sie sofort auf den Nutzer reagieren.

Abbruchsignale

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

  • Der Nutzer klickt auf einen anderen Link oder ein Code führt zu einer anderen Navigation. In diesem Fall wird die alte Navigation durch die neue ersetzt.
  • Der Nutzer klickt im Browser auf die Schaltfläche „Anzeigen ausblenden“.

Für alle diese Möglichkeiten enthält das an den "navigate"-Listener übergebene Ereignis die Eigenschaft signal, eine AbortSignal. Weitere Informationen finden Sie unter Abgebrochener Abruf.

Kurz gesagt: Es wird ein Objekt bereitgestellt, das ein Ereignis auslöst, wenn Sie Ihre Arbeit beenden sollten. Sie können allen Aufrufen von fetch() ein AbortSignal übergeben. Dadurch werden laufende Netzwerkanfragen abgebrochen, wenn die Navigation unterbrochen wird. Dadurch wird die Bandbreite des Nutzers gespart und das von fetch() zurückgegebene Promise wird abgelehnt. Dadurch wird verhindert, dass folgende Codeaktionen wie das Aktualisieren des DOMs für die Anzeige einer jetzt ungültigen Seitennavigation verhindert werden.

Hier ist das vorherige Beispiel, aber mit getArticleContent in der Zeichenfolge, 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);
      },
    });
  }
});

Scrollen

Wenn Sie intercept() für eine Navigation verwenden, versucht der Browser, das Scrollen automatisch durchzuführen.

Wenn Sie zu einem neuen Verlaufseintrag wechseln (navigationEvent.navigationType ist "push" oder "replace"), wird versucht, zum Teil zu scrollen, der durch das URL-Fragment angegeben ist (der Teil nach dem #), oder die Scrollposition wird auf den Anfang der Seite zurückgesetzt.

Bei Aktualisierungen und Durchlaufvorgängen bedeutet das, dass die Scrollposition wiederhergestellt wird, an der dieser Verlaufseintrag zuletzt angezeigt wurde.

Standardmäßig geschieht dies, sobald das von handler zurückgegebene Versprechen erfüllt ist. Wenn es 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);
      },
    });
  }
});

Sie können das automatische Scrollen auch vollständig deaktivieren, indem Sie die scroll-Option von intercept() auf "manual" setzen:

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

Fokusverwaltung

Sobald das von handler zurückgegebene Versprechen erfüllt ist, legt der Browser den Fokus auf das erste Element mit dem festgelegten autofocus-Attribut oder auf das <body>-Element, wenn kein Element dieses Attribut hat.

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

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

Erfolgs- und Fehlerereignisse

Wenn der intercept()-Handler aufgerufen wird, geschieht eines von zwei Dingen:

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

Mit diesen Ereignissen kann Ihr Code den Erfolg oder Misserfolg zentral verarbeiten. So können Sie beispielsweise einen zuvor angezeigten Fortschrittsbalken ausblenden:

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

Sie können auch eine Fehlermeldung bei einem Fehler anzeigen:

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

Der "navigateerror"-Ereignis-Listener, der ein ErrorEvent empfängt, ist besonders praktisch, da er garantiert alle Fehler aus Ihrem Code empfängt, mit dem eine neue Seite eingerichtet wird. Sie können einfach await fetch(), da der Fehler bei nicht verfügbarem Netzwerk an "navigateerror" weitergeleitet wird.

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, eine eindeutige Stringeigenschaft jedes Eintrags, die den aktuellen Eintrag und seinen Slot darstellt. Dieser Schlüssel bleibt auch dann gleich, wenn sich die URL oder der Status des aktuellen Eintrags ändern. Er befindet sich noch in dieser Anzeigenfläche. Wenn ein Nutzer dagegen die Schaltfläche „Zurück“ drückt und dieselbe Seite wieder öffnet, ändert sich key, da durch diesen neuen Eintrag ein neuer Slot erstellt wird.

Für Entwickler ist key nützlich, da Sie mit der Navigation API Nutzer direkt zu einem Eintrag mit einem übereinstimmenden Schlüssel weiterleiten können. Sie können sie auch im Status anderer Einträge beibehalten und so leicht zwischen Seiten 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 gibt den Begriff „Status“ an. Dabei handelt es sich um vom Entwickler bereitgestellte Informationen, die dauerhaft im aktuellen Verlaufseintrag gespeichert, aber für den Nutzer nicht direkt sichtbar sind. Diese Funktion ähnelt stark der history.state in der History API, wurde aber verbessert.

In der Navigation API können Sie die Methode .getState() 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.

Status der Einstellung

Statusobjekte können zwar mutiert werden, diese Änderungen werden jedoch nicht mit dem Verlaufseintrag 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

Der Status sollte während der Scriptnavigation festgelegt werden:

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, die den aktuellen Eintrag ersetzt:

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

Anschließend kann der "navigate"-Ereignis-Listener diese Änderung über navigateEvent.destination erkennen:

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

Status synchron aktualisieren

Generell ist es besser, den Status asynchron über navigation.reload({state: newState}) zu aktualisieren. Dann kann Ihr "navigate"-Listener diesen Status anwenden. Manchmal wird die Statusänderung jedoch bereits vollständig angewendet, wenn Ihr Code davon erfährt. Das ist beispielsweise der Fall, wenn der Nutzer ein <details>-Element ein- oder ausschaltet oder den Status einer Formulareingabe ändert. In diesen Fällen sollten Sie den Status aktualisieren, damit diese Änderungen bei Aktualisierungen und Durchläufen erhalten bleiben. Das ist mit updateCurrentEntry() möglich:

navigation.updateCurrentEntry({state: newState});

Außerdem gibt es eine Veranstaltung zu dieser Änderung:

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

Wenn Sie jedoch auf Statusänderungen in "currententrychange" reagieren, teilen oder duplizieren Sie möglicherweise den Code zur Statusverwaltung zwischen dem "navigate"-Ereignis und dem "currententrychange"-Ereignis. Mit navigation.reload({state: newState}) können Sie ihn an einem Ort verwalten.

Status- und URL-Parameter

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

Wenn Sie möchten, dass der Status beibehalten wird, wenn der Nutzer die URL für einen anderen Nutzer freigibt, speichern Sie ihn in der URL. Andernfalls ist das Statusobjekt die bessere Option.

Auf alle Einträge zugreifen

Der „aktuelle Eintrag“ ist jedoch nicht alles. Über die API können Sie auch über den navigation.entries()-Aufruf auf die gesamte Liste der Einträge zugreifen, die ein Nutzer bei der Nutzung Ihrer Website aufgerufen hat. Dabei wird ein Snapshot-Array von Einträgen zurückgegeben. So lässt sich beispielsweise eine andere Benutzeroberfläche anzeigen, je nachdem, wie der Nutzer zu einer bestimmten Seite gelangt ist, oder einfach nur die vorherigen URLs oder deren Status ansehen. Das ist mit der aktuellen History API nicht möglich.

Sie können auch auf ein "dispose"-Ereignis für einzelne NavigationHistoryEntrys warten. Dieses wird ausgelöst, wenn der Eintrag nicht mehr Teil des Browserverlaufs ist. Das kann im Rahmen der allgemeinen Bereinigung, aber auch bei der Navigation passieren. Wenn Sie beispielsweise 10 Positionen zurückgehen und dann wieder vorwärts, werden diese 10 Verlaufseinträge gelöscht.

Beispiele

Das "navigate"-Ereignis wird wie oben erwähnt bei allen Navigationstypen ausgelöst. (In der Spezifikation gibt es einen langen Anhang mit allen möglichen Typen.)

Bei vielen Websites klicken Nutzer am häufigsten auf ein <a href="...">. Es gibt jedoch zwei bemerkenswerte, komplexere Navigationstypen, die es wert sind, erwähnt zu werden.

Programmatische Navigation

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

Du kannst navigation.navigate('/another_page') an einer beliebigen Stelle in deinem Code aufrufen, um eine Navigation auszulösen. Dies wird vom zentralen Ereignis-Listener verwaltet, der beim "navigate"-Listener registriert ist. Der zentrale Listener wird synchron aufgerufen.

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

Die navigation.navigate()-Methode gibt ein Objekt zurück, das zwei Promise-Instanzen in { committed, finished } enthält. So kann der Aufrufer warten, bis der Übergang entweder „committet“ (die sichtbare URL hat sich geändert und eine neue NavigationHistoryEntry ist verfügbar) oder „fertig“ (alle von intercept({ handler }) zurückgegebenen Versprechen sind abgeschlossen oder aufgrund eines Fehlers oder einer anderen Navigation abgelehnt wurden) ist.

Die navigate-Methode hat auch ein Optionsobjekt, in dem Sie Folgendes festlegen können:

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

info kann beispielsweise nützlich sein, um eine bestimmte Animation zu kennzeichnen, durch die die nächste Seite angezeigt wird. Alternativ können Sie eine globale Variable festlegen oder sie in den #Hash einfügen. Beide Optionen sind etwas umständlich.) Diese info wird nicht wiedergegeben, wenn ein Nutzer später die Navigation auslöst, z. B. über die Schaltflächen „Zurück“ und „Weiter“. In diesen Fällen ist es immer undefined.

Demo des Öffnens von links oder rechts

navigation verfügt auch über eine Reihe anderer Navigationsmethoden, die alle ein Objekt zurückgeben, das { committed, finished } enthält. Ich habe bereits traverseTo() und navigate() erwähnt, die ein key akzeptieren, das einen bestimmten Eintrag im Nutzerverlauf angibt. Außerdem sind back(), forward() und reload() enthalten. Diese Methoden werden genau wie navigate() vom zentralen "navigate"-Ereignis-Listener verarbeitet.

Formulareinreichungen

Zweitens: Das Einreichen von HTML-<form>-Elementen per POST ist eine spezielle Art der Navigation, die von der Navigation API abgefangen werden kann. Auch wenn er eine zusätzliche Nutzlast enthält, wird die Navigation weiterhin zentral vom "navigate"-Listener verwaltet.

Sie können die Formulareinreichung anhand der Property formData in der NavigateEvent erkennen. Hier ein Beispiel, bei dem jede Formulareinreichung über fetch() so geändert wird, dass der Nutzer 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 zentralen Natur des "navigate"-Ereignis-Listeners wird "navigate" gemäß der aktuellen Navigation API-Spezifikation nicht beim ersten Laden einer Seite ausgelöst. Bei Websites, die für alle Status das serverseitige Rendering (SSR) verwenden, ist das möglicherweise in Ordnung. Ihr Server kann den korrekten Anfangsstatus zurückgeben, was die schnellste Methode ist, Inhalte an Ihre Nutzer zu senden. Bei Websites, die clientseitigen Code zum Erstellen ihrer Seiten verwenden, muss möglicherweise eine zusätzliche Funktion zum Initialisieren der Seite erstellt werden.

Eine weitere beabsichtigte Designentscheidung bei der Navigation API ist, dass sie nur innerhalb eines einzelnen Frames funktioniert, also der Seite der obersten Ebene oder einer einzelnen <iframe>. Das hat eine Reihe interessanter Auswirkungen, die in der Spezifikation weiter dokumentiert sind, in der Praxis aber für weniger Verwirrung bei Entwicklern sorgen werden. Die bisherige History API hat eine Reihe von verwirrenden Grenzfällen, z. B. die Unterstützung von Frames, und die neu konzipierte Navigation API bewältigt diese Grenzfälle von Anfang an.

Schließlich besteht noch kein Konsens über die programmatische Änderung oder Neuanordnung der Liste der Einträge, durch die der Nutzer navigiert ist. Diese Frage wird derzeit diskutiert. Eine Option könnte darin bestehen, nur das Löschen zuzulassen: entweder bisherige Einträge oder „alle zukünftigen Einträge“. Letzteres würde einen vorübergehenden Status zulassen. Als Entwickler kann ich z. B.:

  • dem Nutzer eine Frage stellen, indem Sie eine neue URL aufrufen oder einen neuen Status festlegen
  • dem Nutzer ermöglichen, seine Arbeit abzuschließen (oder zurückzugehen)
  • einen Verlaufseintrag nach Abschluss einer Aufgabe entfernen

Das ist ideal für temporäre Modal- oder Interstitial-Fenster: Nutzer können die neue URL über die Zurück-Geste verlassen, aber nicht versehentlich mit der Vorwärts-Geste wieder öffnen, da der Eintrag entfernt wurde. Das ist mit der aktuellen History API nicht möglich.

Navigation API testen

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

Die klassische History API scheint einfach zu sein, ist aber nicht sehr klar definiert und weist zahlreiche Probleme auf, was zu Grenzfällen führen kann und wie sie in den verschiedenen Browsern unterschiedlich implementiert wurde. Wir würden uns sehr über Feedback zur neuen Navigation API freuen.

Verweise

Danksagungen

Vielen Dank an Thomas Steiner, Domenic Denicola und Nate Chapin für die Überprüfung dieses Beitrags.