Modernes clientseitiges Routing: die Navigations-API

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

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.

Bei SPAs war diese Funktion zwar über die History API verfügbar (oder in begrenzten Fällen durch Anpassung des #hash-Teils der Website), aber es handelt sich dabei um eine klobige API, die lange vor der Zeit der SPAs entwickelt wurde. Das Web schreit nach einem völlig neuen Ansatz. Die Navigation API ist eine vorgeschlagene API, die diesen Bereich komplett überarbeitet, anstatt nur die Mängel der History API zu beheben. (Beispielsweise wurde bei Scroll Restoration die History API gepatcht, anstatt 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.
  • Durch Eingabe von preventDefault(), wodurch die Navigation vollständig abgebrochen werden kann.

In diesem Beispiel wird intercept() für das Ereignis aufgerufen. Der Browser ruft Ihren handler-Callback auf, der den nächsten Zustand Ihrer Website konfigurieren soll. Dadurch wird ein Ü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 aber in bestimmten Fällen nicht aufgerufen werden. 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. (Dieses Thema wird auf GitHub diskutiert.)

Auch wenn Sie die Navigation selbst nicht beenden oder abfangen können, wird das Ereignis "navigate" 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. Das ist mit älteren APIs schwierig. 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. 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.

Umgang mit Navigation festlegen

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. Navigationen zwischen verschiedenen Ursprüngen und plattformübergreifende Aufrufe können nicht abgefangen werden.
destination.url
Dies ist 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. Weitere Informationen finden Sie im Beispiel zur Verarbeitung von Formulareinreichungen 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 vom Browser verstanden wird: Es findet derzeit eine SPA-Navigation statt, die das Dokument im Laufe der Zeit von einer vorherigen URL und einem vorherigen Status in einen neuen ändert. Das hat eine Reihe von potenziellen Vorteilen, einschließlich 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, wird die neue URL kurz vor dem Aufrufen des handler-Callbacks wirksam. Wenn Sie das DOM nicht sofort aktualisieren, werden die alten Inhalte zusammen mit der neuen URL angezeigt. Dies wirkt sich unter anderem auf die Auflösung relativer URLs beim Abrufen von Daten oder Laden neuer untergeordneter Ressourcen 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 zugunsten der neuen Navigation aufgegeben.
  • 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 Abbruchbarer 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 nicht nur die Bandbreite des Nutzers gespart, sondern auch die von fetch() zurückgegebene Promise abgelehnt. So wird verhindert, dass nachfolgender Code Aktionen ausführt, z. B. das Aktualisieren des DOM, um eine jetzt ungültige Seitennavigation anzuzeigen.

Hier ist das vorherige Beispiel, aber mit getArticleContent in der Zeile, 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 eine Navigation intercept(), versucht der Browser, das Scrollen automatisch zu steuern.

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 der Scrollvorgang 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);
      },
    });
  }
});

Alternativ können Sie die automatische Scrollfunktion vollständig deaktivieren, indem Sie die Option scroll von intercept() auf "manual" festlegen:

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 Zeitverlauf identifiziert werden kann, und 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 gleich, auch wenn sich die URL oder der Status des aktuellen Eintrags ändert. Es befindet sich immer noch im selben Steckplatz. 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 in den Status anderer Einträge halten, um ganz einfach zwischen den 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 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 jedes objekt sein, das geklont werden kann.

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

Im Allgemeinen ist es besser, den Status asynchron über navigation.reload({state: newState}) zu aktualisieren, damit der "navigate"-Listener diesen Status anwenden kann. 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 kann beispielsweise eine andere Benutzeroberfläche angezeigt werden, je nachdem, wie der Nutzer zu einer bestimmten Seite gelangt ist, oder es können die vorherigen URLs oder ihre Status abgerufen werden. Das ist mit der aktuellen History API nicht möglich.

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

Beispiele

Das Ereignis "navigate" wird wie oben erwähnt für alle 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 des neuen Verlaufseintrags, wie er über die Methode .getState() auf der NavigationHistoryEntry verfügbar ist.
  • 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 anzugeben, die das Aufrufen der nächsten Seite auslöst. 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 bietet auch eine Reihe anderer Navigationsmethoden, die alle ein Objekt mit { committed, finished } zurückgeben. Ich habe bereits traverseTo() erwähnt, bei dem ein key für einen bestimmten Eintrag im Verlauf des Nutzers verwendet wird, und navigate(). Außerdem sind back(), forward() und reload() enthalten. Diese Methoden werden genau wie navigate() vom zentralen "navigate"-Ereignis-Listener verarbeitet.

Eingereichte Formulare

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?

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>. Dies hat eine Reihe interessanter Auswirkungen, die in der Spezifikation weiter dokumentiert sind, in der Praxis aber die Verwirrung der Entwickler verringern werden. Die vorherige History API hatte eine Reihe verwirrender Grenzfälle, z. B. die Unterstützung von Frames. Die neu konzipierte Navigation API kümmert sich von Anfang an um diese Grenzfälle.

Schließlich gibt es noch keine Einigung darüber, ob die Liste der Einträge, die der Nutzer aufgerufen hat, programmatisch geändert oder neu angeordnet werden darf. 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 mag zwar einfach erscheinen, ist aber nicht sehr gut definiert und weist eine große Anzahl von Problemen auf, die sich aus Grenzfällen und der unterschiedlichen Implementierung in verschiedenen Browsern ergeben. 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.