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. <ph type="x-smartling-placeholder">
  • Edge: 102. <ph type="x-smartling-placeholder">
  • Firefox: nicht unterstützt <ph type="x-smartling-placeholder">
  • Safari: nicht unterstützt. <ph type="x-smartling-placeholder">

Quelle

Single-Page-Anwendungen oder SPAs werden durch eine Hauptfunktion definiert: Sie wird dynamisch umgeschrieben, sobald 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 vollständig umgestaltet, anstatt einfach nur die Ränder der History API zu patchen. 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. Wenn Sie den technischen Vorschlag lesen möchten, sehen Sie sich den Berichtsentwurf im WICG-Repository an.

Verwendungsbeispiel

Wenn Sie die Navigation API verwenden möchten, fügen Sie dem globalen navigation-Objekt einen "navigate"-Listener hinzu. Dieses Ereignis ist grundsätzlich zentralisiert: Es wird bei allen Arten von Aufrufen ausgelöst, unabhängig davon, ob der Nutzer eine Aktion ausgeführt hat (z. B. auf einen Link klickt, ein Formular sendet oder vorwärts und rückwärts navigiert) oder wenn die Navigation programmatisch ausgelöst wird (z. B. über den Code Ihrer Website). In den meisten Fällen kann Ihr Code das Standardverhalten des Browsers für diese Aktion überschreiben. Bei SPAs bedeutet dies wahrscheinlich, dass der Nutzer auf derselben Seite bleibt und der Content der Website geladen oder geändert wird.

Ein NavigateEvent-Objekt wird an den "navigate"-Listener übergeben, der Informationen zur Navigation enthält, z. B. die Ziel-URL. Damit kannst du 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, mit der Navigation umzugehen:

  • Zum Ausführen der Navigation wird intercept({ handler }) wie oben beschrieben aufgerufen.
  • 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 das Übergangsobjekt navigation.transition erstellt, mit dem anderer Code den Fortschritt der Navigation verfolgen kann.

In der Regel sind sowohl intercept() als auch preventDefault() zulässig. Es gibt jedoch Fälle, in denen sie nicht aufgerufen werden können. 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 Zurück- oder Weiter-Taste drückt. sollten Sie die Nutzer auf Ihrer Website nicht einfangen 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. Das ist informativ. Mit Ihrem Code könnte beispielsweise ein Analytics-Ereignis protokolliert werden, das darauf hinweist, dass ein Nutzer Ihre Website verlässt.

Warum auf der Plattform eine weitere Veranstaltung hinzufügen?

Ein "navigate"-Event-Listener zentralisiert die Verarbeitung von URL-Änderungen innerhalb einer SPA. Dies ist bei Verwendung älterer APIs ein schwieriger Vorschlag. Wenn Sie die Routenführung für Ihre eigene SPA mit der History API erstellt haben, haben Sie möglicherweise folgenden Code 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));

Dies ist in Ordnung, aber nicht vollständig. Links können auf Ihrer Seite eingefügt oder verlinkt werden und sind nicht die einzige Möglichkeit für Nutzer, durch die Seiten zu navigieren. Sie können beispielsweise ein Formular einreichen oder sogar eine Bildzuordnung verwenden. Ihre Seite könnte damit umgehen, aber es gibt noch eine ganze Reihe von Möglichkeiten, die einfach vereinfacht werden könnten – etwas, das die neue Navigation API realisiert.

Außerdem wird die Rückwärts-/Vorwärtsnavigation nicht wie oben beschrieben bedient. Dafür gibt es ein anderes Ereignis: "popstate".

Die History API hat oft das Gefühl, dass sie diese Möglichkeiten nutzen könnte. Sie hat jedoch nur zwei Bereiche: eine Antwort, wenn der Nutzer im Browser „Zurück“ oder „Weiter“ drückt, und zusätzlich das Senden und Ersetzen von URLs. Es gibt keine Analogie zu "navigate", es sei denn, Sie richten manuell Listener für Click-Events ein, wie oben gezeigt.

Entscheiden, wie eine Navigation gehandhabt werden soll

navigateEvent enthält viele Informationen zur Navigation, anhand derer du entscheiden kannst, wie mit einer bestimmten Navigation umgegangen wird.

Die wichtigsten Eigenschaften sind:

canIntercept
Ist dieser Wert „false“, können Sie die Navigation nicht abfangen. Ursprungsübergreifende Navigationen und dokumentübergreifende Durchläufe können nicht abgefangen werden.
destination.url
Das ist wahrscheinlich die wichtigste Information bei der Navigation.
hashChange
„True“, wenn es sich bei der Navigation um dasselbe Dokument handelt und der Hash der einzige Teil der URL ist, der sich von der aktuellen URL unterscheidet. Bei modernen SPAs sollte der Hash-Wert für Verknüpfungen zu verschiedenen Teilen des aktuellen Dokuments verwendet werden. Wenn hashChange also „true“ 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 Informationen nicht abfangen.
formData
Wenn der Wert nicht null ist, ist diese Navigation Teil einer POST-Formulareinreichung. 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 Senden von Formularen finden Sie weiter unten im Artikel.
navigationType
Dies ist entweder "reload", "push", "replace" oder "traverse". Bei "traverse" 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 der Code intercept({ handler }) aus seinem "navigate"-Listener aufruft, wird dem Browser mitgeteilt, dass die Seite jetzt auf den neuen, aktualisierten Status vorbereitet wird und dass die Navigation einige Zeit in Anspruch nehmen kann.

Der Browser beginnt mit der Erfassung der Scrollposition für den aktuellen Status, damit er später optional wiederhergestellt werden kann. Anschließend wird der handler-Callback aufgerufen. Wenn handler ein Promise zurückgibt (was automatisch bei asynchronen Funktionen geschieht), teilt dieses 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 dieses API ein semantisches Konzept ein, das der Browser versteht: Derzeit erfolgt eine SPA-Navigation, bei der das Dokument von einer vorherigen URL und einem neuen Status geändert wird. Dies hat eine Reihe potenzieller Vorteile, einschließlich der Barrierefreiheit: Browser können den Anfang, das Ende oder einen möglichen Fehler einer Navigation anzeigen. Chrome aktiviert beispielsweise die native Ladeanzeige und ermöglicht dem Nutzer, mit der Stopp-Schaltfläche zu interagieren. Das ist derzeit nicht der Fall, wenn der Nutzer über die Zurück- und Vorwärts-Schaltflächen navigiert. Dies wird jedoch bald behoben.

Wenn Navigationen abgefangen werden, wird die neue URL kurz vor dem Aufruf deines handler-Callbacks wirksam. 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 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 erörtert. 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);
      },
    });
  }
});

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

Signale abbrechen

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 mithilfe eines Codes erfolgt eine weitere Navigation. In diesem Fall wird die alte Navigation durch die neue ersetzt.
  • Der Nutzer klickt auf „Stopp“. Schaltfläche im Browser.

Das an den "navigate"-Listener übergebene Ereignis enthält eine signal-Eigenschaft, bei der es sich um eine AbortSignal handelt, um diese Möglichkeiten nutzen zu können. Weitere Informationen finden Sie unter Abgebrochener Abruf.

Die Kurzversion ist, dass sie im Grunde ein Objekt bereitstellt, das ein Ereignis auslöst, wenn Sie Ihre Arbeit beenden sollten. Insbesondere können Sie eine AbortSignal an alle Aufrufe von 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 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 mit Inline-getArticleContent. Es zeigt, wie die 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);
      },
    });
  }
});

Umgang mit Scrollen

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

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

Bei Aktualisierungen und Durchläufen bedeutet dies, dass die Scrollposition an der Stelle wiederhergestellt wird, an der dieser Verlaufseintrag zuletzt angezeigt wurde.

Das passiert standardmäßig, wenn das von „handler“ zurückgegebene Versprechen aufgelöst wird. 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() {
    // …
  },
});

Fokusbearbeitung

Sobald das von deinem handler zurückgegebene Versprechen aufgelöst wurde, fokussiert der Browser das erste Element, bei dem das Attribut autofocus festgelegt ist, 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 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 einem 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;
});

Oder Sie sehen 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 empfängt, ist besonders praktisch, da er garantiert alle Fehler von dem Code erhält, 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 auch dann gleich, wenn sich die URL oder der Status des aktuellen Eintrags ändern. Er befindet sich noch in dieser Anzeigenfläche. 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 weiterleiten kannst. 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“ wieder. Dabei handelt es sich um vom Entwickler bereitgestellte Informationen, die dauerhaft im aktuellen Verlaufseintrag gespeichert werden, für den Nutzer jedoch nicht direkt sichtbar sind. Dies ist dem history.state in der History API, 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());

Der Standardwert ist undefined.

Einstellungsstatus

Obwohl Statusobjekte geändert 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

Der Status wird ordnungsgemäß während der Skriptnavigation festgelegt:

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"-Event-Listener diese Änderung über navigateEvent.destination übernehmen:

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 ist die Statusänderung jedoch bereits vollständig umgesetzt, wenn der Code davon erfährt, z. B. wenn der Nutzer ein <details>-Element umschaltet oder den Status einer Formulareingabe ändert. In diesen Fällen kann es von Vorteil sein, den Status zu aktualisieren, damit diese Änderungen beim erneuten Laden und Durchlauf beibehalten werden. 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 feststellen, dass Sie auf Statusänderungen in "currententrychange" reagieren, teilen oder duplizieren Sie möglicherweise den Zustandscode zwischen dem "navigate"-Ereignis und dem "currententrychange"-Ereignis. Mit navigation.reload({state: newState}) hingegen können Sie den Code an einem Ort verarbeiten.

Status- und URL-Parameter

Da der Zustand 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 Bundesstaat 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 state-Objekt die bessere Wahl.

Auf alle Einträge zugreifen

Der „aktuelle Eintrag“ ist jedoch nicht alles. Die API bietet auch eine Möglichkeit, über den navigation.entries()-Aufruf, der ein Snapshot-Array von Einträgen zurückgibt, auf die gesamte Liste der Einträge zuzugreifen, die ein Nutzer bei der Verwendung Ihrer Website aufgerufen hat. So lässt sich beispielsweise eine andere Benutzeroberfläche anzeigen, je nachdem, wie der Nutzer zu einer bestimmten Seite navigiert ist, oder um sich nur die vorherigen URLs oder ihren Status anzusehen. Mit der aktuellen History API ist dies 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 Orte zurück- und dann vorwärts gehen, werden diese 10 Verlaufseinträge entfernt.

Beispiele

Das "navigate"-Ereignis wird wie oben erwähnt bei allen Navigationstypen ausgelöst. Es gibt tatsächlich einen langen Anhang in der Spezifikation aller 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

Das erste ist die programmatische Navigation, bei der die Navigation durch einen Methodenaufruf in Ihrem clientseitigen Code ausgelöst 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 ist als verbesserte Aggregation älterer Methoden wie location.assign() und Freunde sowie der Methoden pushState() und replaceState() der History API gedacht.

Die Methode navigation.navigate() gibt ein Objekt zurück, das zwei Promise-Instanzen in { committed, finished } enthält. Dadurch kann der Aufrufer warten, bis entweder der Übergang einen Commit-Vorgang durchgeführt hat. (die angezeigte URL hat sich geändert und ein neues NavigationHistoryEntry ist verfügbar) oder „Fertig“. (Alle von intercept({ handler }) zurückgegebenen Promise sind vollständig – oder abgelehnt, weil sie fehlgeschlagen sind oder von einer anderen Navigation überschrieben wurden).

Die Methode navigate 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: 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 als Teil des #hash hinzufügen. Beide Optionen sind etwas umständlich.) info wird nicht noch einmal wiedergegeben, wenn ein Nutzer später die Navigation startet, z.B. über die Schaltflächen „Zurück“ und „Weiter“. In diesen Fällen lautet der Wert immer undefined.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph> Demo zum Öffnen 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. Dazu gehören auch back(), forward() und reload(). Diese Methoden werden alle genau wie navigate() vom zentralen Event-Listener "navigate" verarbeitet.

Formulareinreichungen

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

Du kannst ein eingereichtes Formular erkennen, indem du auf NavigateEvent nach der Property formData suchst. Hier ist ein Beispiel, bei dem jede Formulareinreichung über fetch() einfach in ein Formular umgewandelt wird, 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?

Obwohl der "navigate"-Event-Listener zentralisiert ist, löst die aktuelle Navigation API-Spezifikation "navigate" nicht beim ersten Laden einer Seite aus. 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. Für Websites, die clientseitigen Code zum Erstellen ihrer Seiten verwenden, muss jedoch möglicherweise eine zusätzliche Funktion zur Initialisierung ihrer Seite erstellt werden.

Eine weitere bewusste Wahl des Designs der Navigation API besteht darin, dass sie nur in einem einzelnen Frame arbeitet, d. h. auf der Seite auf oberster Ebene oder in einer einzelnen spezifischen <iframe>. Dies hat eine Reihe interessanter Auswirkungen, die in der Spezifikation näher dokumentiert sind. In der Praxis wird es jedoch zu weniger Verwirrung bei den Entwicklern kommen. Die bisherige History API hat eine Reihe verwirrender Grenzfälle, 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. Dies wird derzeit noch diskutiert. Sie können aber auch festlegen, dass nur das Löschen von historischen Einträgen oder „Alle zukünftigen Einträge“ möglich ist. Letzteres würde einen temporären Zustand zulassen. Als Entwickler könnte ich zum Beispiel:

  • Dem Nutzer eine Frage stellen, indem er zur neuen URL oder zum neuen Status navigiert
  • Ermöglichen Sie dem Nutzer, seine Arbeit abzuschließen (oder zurückzugehen).
  • Verlaufseintrag nach Abschluss einer Aufgabe entfernen

Dies kann perfekt für temporäre modale Anzeigen oder Interstitials geeignet sein: Der Nutzer kann die neue URL mit der Zurück-Geste verlassen, kann dann aber nicht versehentlich weitergehen, um die URL wieder zu öffnen (weil der Eintrag entfernt wurde). Mit der aktuellen History API ist dies jedoch 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 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 freuen, wenn Sie uns Feedback zur neuen Navigation API geben.

Verweise

Danksagungen

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