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: wird nicht unterstützt.

Quelle

Single-Page-Anwendungen oder SPAs werden durch eine Kernfunktion definiert: Sie wird dynamisch umgeschrieben, wenn der Nutzer mit der Website interagiert. Dies geschieht im Gegensatz zur Standardmethode zum Laden vollständig neuer Seiten vom Server.

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. (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.
  • preventDefault() wird aufgerufen. Dadurch kann die Navigation vollständig abgebrochen werden.

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 andere Code den Fortschritt der Navigation verfolgen können.

Sowohl intercept() als auch preventDefault() sind in der Regel zulässig, können aber in bestimmten Fällen nicht aufgerufen werden. Sie können Navigationen über intercept() nicht verarbeiten, wenn es sich um eine ursprungsü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?

Ein "navigate"-Event-Listener zentralisiert die Verarbeitung von URL-Änderungen innerhalb einer SPA. 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 – 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.

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 Dokumenten 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
Ist dies der Fall, 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 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. Dies ist derzeit nicht der Fall, wenn Nutzer über die Zurück- und Vorwärts-Schaltflächen navigieren. Das Problem wird jedoch demnächst 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, wird ein Punkt erstellt, in dem der alte Inhalt zusammen mit der neuen URL angezeigt wird. 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 besprochen. Generell wird jedoch empfohlen, die Seite sofort mit Platzhaltern für eingehende 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 passiert in folgenden Fällen:

  • 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 Stopp-Schaltfläche.

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

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() {
    // …
  },
});

Fokusbearbeitung

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.

Diese Ereignisse ermöglichen es Ihrem Code, Erfolg oder Misserfolg zentral zu erfassen. Im Erfolgsfall können Sie beispielsweise eine zuvor angezeigte Fortschrittsanzeige wie folgt 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() in der Gewissheit, dass der Fehler 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, 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. Umgekehrt ändert sich key, wenn ein Nutzer auf „Zurück“ drückt und dann die gleiche Seite noch einmal öffnet, da durch diesen neuen Eintrag ein neuer Slot erstellt wird.

Für Entwickler ist key nützlich, weil du mit der Navigation API den Nutzer direkt zu einem Eintrag mit einem passenden Schlüssel leiten kannst. 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 .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.

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

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 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 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.)

Auf vielen Websites ist es meistens dann der Fall, wenn der Nutzer auf <a href="..."> klickt. Es gibt jedoch zwei wichtige, komplexere Navigationstypen, die es sich lohnt, sie durchzugehen.

Programmatische Navigation

Die 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 jeder Stelle im Code aufrufen, um eine Navigation zu starten. Dies wird vom zentralen Event-Listener verarbeitet, der auf dem "navigate"-Listener registriert ist, und Ihr zentraler 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 aufnehmen. 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(). Dazu gehören auch back(), forward() und reload(). 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 serverseitiges Rendering (Server Side Rendering, SSR) für alle Status verwenden, kann dies in Ordnung sein. Ihr Server könnte dann den richtigen Anfangszustand zurückgeben und so den Nutzern Inhalte am schnellsten bereitstellen. 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 näher dokumentiert sind. In der Praxis wird dies jedoch zu weniger Verwirrung bei den Entwicklern führen. 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 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 könnte ich zum Beispiel:

  • dem Nutzer eine Frage stellen, indem Sie eine neue URL aufrufen oder einen neuen Status festlegen
  • Ermöglichen Sie dem Nutzer, seine Arbeit abzuschließen (oder zurückzugehen).
  • 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 Domenic Denicola ausprobieren.

Die klassische History API erscheint zwar einfach, 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.