Nowoczesny routing po stronie klienta: interfejs API nawigacji

Ujednolicenie routingu po stronie klienta za pomocą zupełnie nowego interfejsu API, który całkowicie zmienia sposób tworzenia aplikacji jednostronicowych.

Browser Support

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 147.
  • Safari: 26.2.

Source

Aplikacje jednostronicowe (SPA) charakteryzują się jedną podstawową cechą: dynamicznym przepisywaniem treści w miarę interakcji użytkownika z witryną, zamiast domyślnej metody wczytywania całkowicie nowych stron z serwera.

Chociaż aplikacje SPA mogły udostępniać tę funkcję za pomocą interfejsu History API (lub w ograniczonych przypadkach przez dostosowanie części witryny z symbolem #), jest to niezgrabny interfejs API opracowany na długo przed tym, jak aplikacje SPA stały się normą. Sieć domaga się zupełnie nowego podejścia. Interfejs Navigation API to proponowany interfejs API, który całkowicie zmienia ten obszar, zamiast po prostu poprawiać niedoskonałości interfejsu History API. (Na przykład przywracanie pozycji przewijania poprawiło interfejs History API, zamiast próbować go od nowa wymyślić).

Ten post zawiera ogólny opis interfejsu Navigation API. Aby zapoznać się z propozycją techniczną, zobacz wersję roboczą raportu w repozytorium WICG.

Przykład użycia

Aby korzystać z Navigation API, zacznij od dodania "navigate" do globalnego obiektu navigation. To zdarzenie jest zasadniczo scentralizowane: będzie uruchamiane w przypadku wszystkich typów nawigacji, niezależnie od tego, czy użytkownik wykonał działanie (np. kliknął link, przesłał formularz lub cofnął się bądź przeszedł dalej), czy nawigacja została wywołana programowo (czyli przez kod witryny). W większości przypadków pozwala to kodowi zastąpić domyślne działanie przeglądarki w przypadku danego działania. W przypadku aplikacji jednostronicowych oznacza to prawdopodobnie pozostawienie użytkownika na tej samej stronie i wczytanie lub zmianę treści witryny.

Do odbiornika "navigate" przekazywany jest obiekt NavigateEvent, który zawiera informacje o nawigacji, takie jak docelowy adres URL, i umożliwia reagowanie na nawigację w jednym centralnym miejscu. Podstawowy "navigate" może wyglądać tak:

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

Nawigację możesz obsługiwać na 2 sposoby:

  • Wywołaj funkcję intercept({ handler }) (zgodnie z opisem powyżej), aby obsłużyć nawigację.
  • dzwonić pod numer preventDefault(), co może całkowicie anulować nawigację;

W tym przykładzie wywoływana jest funkcja intercept() w zdarzeniu. Przeglądarka wywołuje handler funkcję zwrotną, która powinna skonfigurować następny stan witryny. Spowoduje to utworzenie obiektu przejścia navigation.transition, którego inny kod może używać do śledzenia postępu nawigacji.

Zwykle można dzwonić pod numery intercept()preventDefault(), ale w niektórych przypadkach nie jest to możliwe. Nie możesz obsługiwać nawigacji za pomocą interfejsu intercept(), jeśli jest to nawigacja między domenami. Nie możesz też anulować nawigacji za pomocą preventDefault(), jeśli użytkownik naciska przyciski Wstecz lub Dalej w przeglądarce. Nie możesz zatrzymywać użytkowników w swojej witrynie. (Trwa dyskusja na ten temat na GitHubie).

Nawet jeśli nie możesz zatrzymać ani przechwycić nawigacji, zdarzenie "navigate" zostanie uruchomione. Jest to informacja, więc Twój kod może na przykład rejestrować zdarzenie Analytics, aby wskazać, że użytkownik opuszcza Twoją witrynę.

Dlaczego warto dodać kolejne wydarzenie na platformie?

Detektor zdarzeń typu "navigate" centralizuje obsługę zmian adresu URL w aplikacji jednostronicowej. W przypadku starszych interfejsów API jest to trudne. Jeśli kiedykolwiek tworzyłeś(-aś) routing dla własnej aplikacji SPA za pomocą interfejsu History API, być może dodawałeś(-aś) kod podobny do tego:

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

To dobrze, ale nie wyczerpuje tematu. Linki mogą pojawiać się i znikać ze strony, a nie są jedynym sposobem poruszania się użytkowników po stronach. Mogą na przykład przesłać formularz lub nawet użyć mapy obrazu. Twoja strona może sobie z tym radzić, ale istnieje wiele możliwości, które można uprościć – to właśnie umożliwia nowy interfejs Navigation API.

Ponadto powyższe rozwiązanie nie obsługuje nawigacji wstecz/dalej. Jest już inne wydarzenie, "popstate".

Osobiście uważam, że interfejs History API może w pewnym stopniu pomóc w wykorzystaniu tych możliwości. Ma jednak tylko 2 obszary: reagowanie na naciśnięcie przez użytkownika przycisku Wstecz lub Dalej w przeglądarce oraz przesyłanie i zastępowanie adresów URL. Nie ma odpowiednika "navigate", chyba że ręcznie skonfigurujesz odbiorniki zdarzeń kliknięcia, jak pokazano powyżej.

Decyduj, jak obsługiwać nawigację

Sekcja navigateEvent zawiera wiele informacji o nawigacji, które mogą Ci pomóc w podjęciu decyzji, jak postępować w konkretnym przypadku.

Kluczowe właściwości to:

canIntercept
Jeśli ta wartość jest fałszywa, nie możesz przechwycić nawigacji. Nie można przechwytywać nawigacji między domenami ani przechodzenia między dokumentami.
destination.url
Prawdopodobnie najważniejsza informacja, którą należy wziąć pod uwagę podczas obsługi nawigacji.
hashChange
Wartość „true”, jeśli nawigacja odbywa się w ramach tego samego dokumentu, a jedyną różnicą między adresem URL a bieżącym adresem URL jest hash. W nowoczesnych aplikacjach SPA hash powinien służyć do linkowania do różnych części bieżącego dokumentu. Jeśli więc hashChange ma wartość „true”, prawdopodobnie nie musisz przechwytywać tej nawigacji.
downloadRequest
Jeśli ta wartość jest prawdziwa, nawigacja została zainicjowana przez link z atrybutem download. W większości przypadków nie musisz tego przechwytywać.
formData
Jeśli ta wartość nie jest pusta, oznacza to, że nawigacja jest częścią przesyłania formularza POST. Pamiętaj o tym podczas obsługi nawigacji. Jeśli chcesz obsługiwać tylko nawigacje GET, unikaj przechwytywania nawigacji, w których formData nie ma wartości null. Przykład obsługi przesłanych formularzy znajdziesz w dalszej części tego artykułu.
navigationType
Jest to jedna z tych wartości: "reload", "push", "replace" lub "traverse". Jeśli jest to "traverse", nie można anulować nawigacji za pomocą preventDefault().

Na przykład funkcja shouldNotIntercept użyta w pierwszym przykładzie może wyglądać tak:

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

Przechwytywanie

Gdy kod wywołuje intercept({ handler }) w ramach funkcji nasłuchującej "navigate", informuje przeglądarkę, że przygotowuje stronę do nowego, zaktualizowanego stanu i że nawigacja może zająć trochę czasu.

Przeglądarka zaczyna od zarejestrowania pozycji przewijania w bieżącym stanie, aby można ją było później przywrócić. Następnie wywołuje funkcję zwrotną handler. Jeśli funkcja handler zwraca obietnicę (co dzieje się automatycznie w przypadku funkcji asynchronicznych), obietnica ta informuje przeglądarkę, jak długo trwa nawigacja i czy zakończyła się powodzeniem.

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

W związku z tym ten interfejs API wprowadza koncepcję semantyczną, którą rozumie przeglądarka: nawigacja w ramach aplikacji SPA jest w toku i zmienia dokument z poprzedniego adresu URL i stanu na nowy. Ma to wiele potencjalnych zalet, w tym ułatwienia dostępu: przeglądarki mogą wyświetlać początek, koniec lub potencjalne niepowodzenie nawigacji. Chrome na przykład aktywuje swój natywny wskaźnik wczytywania i umożliwia użytkownikowi interakcję z przyciskiem zatrzymania. (Obecnie nie ma to miejsca, gdy użytkownik porusza się po stronie za pomocą przycisków Wstecz i Dalej, ale wkrótce to naprawimy).

Podczas przechwytywania nawigacji nowy adres URL zacznie obowiązywać tuż przed wywołaniem funkcji zwrotnej handler. Jeśli nie zaktualizujesz DOM-u od razu, przez pewien czas będą wyświetlane stare treści wraz z nowym adresem URL. Ma to wpływ na takie kwestie jak rozwiązywanie względnych adresów URL podczas pobierania danych lub wczytywania nowych zasobów podrzędnych.

Sposób opóźnienia zmiany adresu URL jest omawiany w GitHub, ale ogólnie zaleca się natychmiastową aktualizację strony z użyciem jakiegoś rodzaju symbolu zastępczego dla przychodzących treści:

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

Pozwala to nie tylko uniknąć problemów z rozwiązywaniem adresów URL, ale też sprawia, że strona działa szybko, ponieważ natychmiast reagujesz na działania użytkownika.

Sygnały przerwania

Ponieważ w intercept() możesz wykonywać pracę asynchroniczną, nawigacja może stać się zbędna. Dzieje się tak, gdy:

  • Użytkownik klika inny link lub kod wykonuje inną nawigację. W takim przypadku stara nawigacja jest porzucana na rzecz nowej.
  • Użytkownik klika przycisk „stop” w przeglądarce.

Aby poradzić sobie z każdą z tych możliwości, zdarzenie przekazywane do "navigate" zawiera właściwość signal, która jest AbortSignal. Więcej informacji znajdziesz w sekcji Przerwanie pobierania.

W skrócie: udostępnia on obiekt, który wywołuje zdarzenie, gdy należy przerwać pracę. Możesz przekazać AbortSignal do wszystkich wywołań fetch(), co spowoduje anulowanie trwających żądań sieciowych, jeśli nawigacja zostanie wyprzedzona. Pozwoli to zaoszczędzić przepustowość użytkownika i odrzucić Promise zwrócone przez fetch(), co uniemożliwi wykonanie kolejnych działań kodu, takich jak aktualizacja DOM w celu wyświetlenia nieprawidłowej nawigacji po stronie.

Oto poprzedni przykład, ale z właściwością getArticleContent wstawioną w treść, pokazujący, jak można używać właściwości AbortSignal z właściwością fetch():

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

Obsługa przewijania

Gdy intercept() nawigację, przeglądarka spróbuje automatycznie przewinąć stronę.

W przypadku przejść do nowego wpisu w historii (gdy navigationEvent.navigationType ma wartość "push" lub "replace") oznacza to próbę przewinięcia do części wskazanej przez fragment adresu URL (część po znaku #) lub zresetowanie przewijania do góry strony.

W przypadku ponownego wczytywania i przechodzenia oznacza to przywrócenie pozycji przewijania do miejsca, w którym znajdowała się ona ostatnim razem, gdy wyświetlany był ten wpis w historii.

Domyślnie dzieje się to po rozwiązaniu obietnicy zwróconej przez funkcję handler, ale jeśli przewijanie wcześniej ma sens, możesz wywołać funkcję navigateEvent.scroll():

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

Możesz też całkowicie zrezygnować z automatycznego przewijania, ustawiając opcję scroll parametru intercept() na "manual":

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

Obsługa zaznaczania

Gdy obietnica zwrócona przez funkcję handler zostanie spełniona, przeglądarka ustawi fokus na pierwszym elemencie z ustawionym atrybutem autofocus lub na elemencie <body>, jeśli żaden element nie ma tego atrybutu.

Możesz zrezygnować z tego sposobu działania, ustawiając opcję focusReset parametru intercept() na "manual":

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

Zdarzenia związane z sukcesem i niepowodzeniem

Gdy zostanie wywołany moduł obsługi intercept(), nastąpi jedna z tych sytuacji:

  • Jeśli zwrócony element Promise spełnia warunki (lub nie wywołasz funkcji intercept()), interfejs Navigation API wywoła zdarzenie "navigatesuccess" z elementem Event.
  • Jeśli zwrócony obiekt Promise zostanie odrzucony, interfejs API wywoła zdarzenie "navigateerror" z obiektem ErrorEvent.

Te zdarzenia umożliwiają centralne obsługiwanie przez kod sukcesów i porażek. Możesz na przykład ukryć wcześniej wyświetlany wskaźnik postępu, tak jak w tym przykładzie:

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

Możesz też wyświetlić komunikat o błędzie w przypadku niepowodzenia:

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

Detektor zdarzeń "navigateerror", który otrzymuje ErrorEvent, jest szczególnie przydatny, ponieważ gwarantuje otrzymywanie wszelkich błędów z kodu, który konfiguruje nową stronę. Możesz po prostu await fetch(), wiedząc, że jeśli sieć będzie niedostępna, błąd zostanie ostatecznie przekierowany do "navigateerror".

navigation.currentEntry zapewnia dostęp do bieżącego wpisu. Jest to obiekt, który opisuje, gdzie obecnie znajduje się użytkownik. Ten wpis zawiera bieżący adres URL, metadane, których można używać do identyfikowania tego wpisu w czasie, oraz stan podany przez dewelopera.

Metadane zawierają key, czyli unikalną właściwość ciągu znaków każdego wpisu, która reprezentuje bieżący wpis i jego miejsce. Ten klucz pozostaje taki sam, nawet jeśli zmieni się adres URL lub stan bieżącego wpisu. Nadal znajduje się w tym samym slocie. Jeśli użytkownik naciśnie Wstecz, a następnie ponownie otworzy tę samą stronę, wartość key się zmieni, ponieważ nowy wpis utworzy nowe miejsce.

Dla dewelopera symbol key jest przydatny, ponieważ interfejs Navigation API umożliwia bezpośrednie przekierowanie użytkownika do wpisu z pasującym kluczem. Możesz go zachować nawet w przypadku innych wpisów, aby łatwo przechodzić między stronami.

// 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;

Stan

Interfejs Navigation API udostępnia pojęcie „stanu”, czyli informacji dostarczonych przez dewelopera, które są trwale przechowywane w bieżącym wpisie historii, ale nie są bezpośrednio widoczne dla użytkownika. Jest to bardzo podobne do history.state w interfejsie History API, ale ulepszone.

W interfejsie Navigation API możesz wywołać metodę .getState() bieżącego wpisu (lub dowolnego wpisu), aby zwrócić kopię jego stanu:

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

Domyślnie będzie to undefined.

Stan ustawienia

Obiekty stanu można zmieniać, ale te zmiany nie są zapisywane w historii, więc:

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

Prawidłowy sposób ustawiania stanu to nawigacja po skrypcie:

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

gdzie newState może być dowolnym obiektem, który można sklonować.

Jeśli chcesz zaktualizować stan bieżącego wpisu, najlepiej wykonać nawigację, która zastąpi bieżący wpis:

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

Następnie Twój detektor zdarzeń "navigate" może wykryć tę zmianę za pomocą navigateEvent.destination:

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

Synchroniczne aktualizowanie stanu

Zazwyczaj lepiej jest aktualizować stan asynchronicznie za pomocą navigation.reload({state: newState}), a następnie odbiornik "navigate" może zastosować ten stan. Czasami jednak zmiana stanu jest już w pełni zastosowana, zanim kod się o niej dowie, np. gdy użytkownik przełącza element <details> lub zmienia stan elementu wejściowego formularza. W takich przypadkach warto zaktualizować stan, aby zmiany zostały zachowane podczas ponownego wczytywania i przechodzenia między stronami. Możesz to zrobić za pomocą updateCurrentEntry():

navigation.updateCurrentEntry({state: newState});

O tej zmianie możesz też dowiedzieć się z wydarzenia:

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

Jeśli jednak reagujesz na zmiany stanu w "currententrychange", możesz dzielić lub nawet duplikować kod obsługi stanu między zdarzeniami "navigate""currententrychange", podczas gdy navigation.reload({state: newState}) pozwoli Ci obsługiwać go w jednym miejscu.

Stan a parametry adresu URL

Stan może być obiektem strukturalnym, dlatego kuszące jest używanie go do przechowywania całego stanu aplikacji. W wielu przypadkach lepiej jest jednak przechowywać ten stan w adresie URL.

Jeśli chcesz, aby stan był zachowywany, gdy użytkownik udostępnia adres URL innemu użytkownikowi, zapisz go w adresie URL. W innym przypadku lepszym rozwiązaniem jest obiekt stanu.

Dostęp do wszystkich wpisów

„Bieżący wpis” to jednak nie wszystko. Interfejs API umożliwia też dostęp do całej listy wpisów, po których użytkownik poruszał się podczas korzystania z Twojej witryny, za pomocą wywołania navigation.entries(), które zwraca tablicę wpisów w formie migawki. Może to służyć np. do wyświetlania innego interfejsu w zależności od tego, jak użytkownik dotarł na daną stronę, lub po prostu do przeglądania poprzednich adresów URL lub ich stanów. W przypadku obecnego interfejsu History API jest to niemożliwe.

Możesz też nasłuchiwać zdarzenia "dispose" w poszczególnych elementach NavigationHistoryEntry, które jest wywoływane, gdy wpis nie jest już częścią historii przeglądarki. Może się to zdarzyć w ramach ogólnego czyszczenia, ale także podczas nawigacji. Jeśli na przykład cofniesz się o 10 miejsc, a potem przejdziesz do przodu, te 10 pozycji historii zostanie usuniętych.

Przykłady

Zdarzenie "navigate" jest wywoływane w przypadku wszystkich typów nawigacji, jak wspomnieliśmy powyżej. (W długim załączniku do specyfikacji znajdziesz wszystkie możliwe typy).

W przypadku wielu witryn najczęstszym przypadkiem będzie kliknięcie przez użytkownika <a href="...">, ale warto omówić 2 bardziej złożone typy nawigacji.

Nawigacja automatyczna

Pierwszy to nawigacja programowa, w której nawigacja jest wywoływana przez wywołanie metody w kodzie po stronie klienta.

Funkcję navigation.navigate('/another_page') możesz wywołać w dowolnym miejscu w kodzie, aby spowodować nawigację. Zajmie się tym scentralizowany detektor zdarzeń zarejestrowany w detektorze "navigate", a Twój scentralizowany detektor zostanie wywołany synchronicznie.

Jest to ulepszona agregacja starszych metod, takich jak location.assign() i podobne, oraz metod interfejsu History API – pushState()replaceState().

Metoda navigation.navigate() zwraca obiekt, który zawiera 2 instancje Promise{ committed, finished }. Dzięki temu wywołujący może poczekać, aż przejście zostanie „zatwierdzone” (widoczny adres URL ulegnie zmianie i będzie dostępny nowy NavigationHistoryEntry) lub „zakończone” (wszystkie obietnice zwrócone przez intercept({ handler }) zostaną spełnione lub odrzucone z powodu błędu albo wyprzedzenia przez inną nawigację).

Metoda navigate ma też obiekt opcji, w którym możesz ustawić:

  • state: stan nowego wpisu w historii, dostępny za pomocą metody .getState() w obiekcie NavigationHistoryEntry.
  • history: można ustawić wartość "replace", aby zastąpić bieżący wpis w historii.
  • info: obiekt do przekazania do zdarzenia navigate za pomocą navigateEvent.info.

W szczególności parametr info może być przydatny np. do oznaczenia konkretnej animacji, która powoduje wyświetlenie następnej strony. (Alternatywą może być ustawienie zmiennej globalnej lub uwzględnienie jej w #hash. Obie opcje są nieco niezręczne). Co ważne, infonie zostanie on odtworzony ponownie, jeśli użytkownik później spowoduje nawigację, np. za pomocą przycisków Wstecz i Dalej. W takich przypadkach zawsze będzie to undefined.

Demonstracja otwierania od lewej lub prawej strony

navigation ma też kilka innych metod nawigacji, które zwracają obiekt zawierający { committed, finished }. Wspomniałem już o traverseTo() (który akceptuje key oznaczający konkretny wpis w historii użytkownika) i navigate(). Obejmuje też back(), forward() i reload(). Wszystkie te metody są obsługiwane – podobnie jak navigate() – przez scentralizowany detektor zdarzeń "navigate".

Przesłane formularze

Po drugie, przesyłanie HTML <form> za pomocą żądania POST to specjalny rodzaj nawigacji, który może zostać przechwycony przez interfejs Navigation API. Chociaż zawiera dodatkowy ładunek, nawigacja jest nadal obsługiwana centralnie przez "navigate" odbiornik.

Przesłanie formularza można wykryć, sprawdzając właściwość formData w obiekcie NavigateEvent. Oto przykład, który za pomocą fetch() przekształca każde przesłanie formularza w przesłanie, które pozostaje na bieżącej stronie:

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

Czego brakuje?

Pomimo scentralizowanego charakteru modułu nasłuchiwania zdarzeń "navigate" bieżąca specyfikacja interfejsu Navigation API nie wywołuje zdarzenia "navigate" podczas pierwszego wczytania strony. W przypadku witryn, które we wszystkich stanach używają renderowania po stronie serwera (SSR), może to być w porządku – serwer może zwracać prawidłowy stan początkowy, co jest najszybszym sposobem na dostarczanie treści użytkownikom. Jednak witryny, które do tworzenia stron wykorzystują kod po stronie klienta, mogą potrzebować dodatkowej funkcji do inicjowania strony.

Kolejnym celowym wyborem projektowym w przypadku interfejsu Navigation API jest to, że działa on tylko w ramach jednej ramki, czyli strony najwyższego poziomu lub jednej konkretnej ramki <iframe>. Ma to wiele ciekawych konsekwencji, które są dokładniej opisane w specyfikacji, ale w praktyce zmniejszy zamieszanie wśród deweloperów. Poprzedni interfejs History API miał wiele niejasnych przypadków brzegowych, np. obsługę ramek, a przeprojektowany interfejs Navigation API od początku radzi sobie z tymi przypadkami.

Nie ma jeszcze konsensusu w sprawie programowego modyfikowania lub przestawiania listy wpisów, po których użytkownik się poruszał. Ta kwestia jest obecnie omawiana, ale jedną z opcji może być zezwolenie tylko na usuwanie: albo wpisów historycznych, albo „wszystkich przyszłych wpisów”. To drugie rozwiązanie umożliwia przechowywanie stanu tymczasowego. Jako deweloper mogę na przykład:

  • zadawać użytkownikowi pytania, przechodząc do nowego adresu URL lub stanu;
  • umożliwiać użytkownikowi dokończenie pracy (lub powrót);
  • usuwać wpis z historii po ukończeniu zadania,

To idealne rozwiązanie w przypadku tymczasowych okien modalnych lub reklam pełnoekranowych: nowy adres URL umożliwia użytkownikowi powrót do poprzedniej strony za pomocą gestu, ale nie może on przypadkowo przejść do przodu, aby ponownie otworzyć stronę (ponieważ wpis został usunięty). Nie jest to możliwe w przypadku obecnego interfejsu History API.

Wypróbuj interfejs Navigation API

Interfejs Navigation API jest dostępny w Chrome 102 bez flag. Możesz też wypróbować wersję demonstracyjną przygotowaną przez Domenica Denicolę.

Klasyczny interfejs History API wydaje się prosty, ale nie jest zbyt dobrze zdefiniowany i ma wiele problemów związanych z przypadkami brzegowymi i różnicami w implementacji w poszczególnych przeglądarkach. Mamy nadzieję, że podzielisz się z nami opinią o nowym interfejsie Navigation API.

Odniesienia

Podziękowania

Dziękujemy Thomasowi Steinerowi, Domenicowi Denicoli i Nate’owi Chapinowi za sprawdzenie tego posta.