Nowoczesny routing po stronie klienta: interfejs API nawigacji

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

Obsługa przeglądarek

  • Chrome: 102.
  • Edge: 102.
  • Firefox: nieobsługiwane.
  • Safari: nieobsługiwane.

Źródło

Aplikacje jednostronicowe (SPA) są zdefiniowane przez główną funkcję: dynamiczne przepisywanie treści w miarę interakcji użytkownika z witryną zamiast domyślnej metody wczytywania zupełnie 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 #hash witryny), to jest to nieporędny interfejs API, który został opracowany na długo przed tym, zanim aplikacje SPA stały się normą. Internet potrzebuje zupełnie nowego podejścia. Interfejs Navigation API to proponowany interfejs API, który całkowicie zmienia tę przestrzeń, zamiast próbować po prostu naprawić błędy interfejsu History API. (np. Scroll Restoration naprawił interfejs History API, zamiast próbować go na nowo).

W tym artykule ogólnie omawiamy interfejs API Nawigacji. Aby zapoznać się z propozycją techniczną, przeczytaj raport Draft Report w repozytorium WICG.

Przykład użycia

Aby korzystać z interfejsu API Nawigacja, zacznij od dodania odbiornika "navigate" do obiektu globalnego navigation. To zdarzenie jest centralne: będzie się uruchamiać w przypadku wszystkich typów nawigacji, niezależnie od tego, czy użytkownik wykonał jakieś działanie (np.kliknął link, przesłał formularz lub przewinął stronę w górę lub w dół), czy też nawigacja została wywołana programowo (np. za pomocą kodu witryny). W większości przypadków pozwala on Twojemu kodowi zastąpić domyślne działanie przeglądarki w przypadku danego działania. W przypadku aplikacji SPA 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, np. adres URL miejsca docelowego. Umożliwia to reagowanie na nawigację w jednym scentralizowanym miejscu. Podstawowy odbiorca "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});
  }
});

Możesz poruszać się po stronie na 2 sposoby:

  • wywołanie funkcji intercept({ handler }) (jak opisano powyżej) w celu obsługi nawigacji;
  • wywołanie preventDefault(), co może spowodować całkowite anulowanie nawigacji;

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

Zarówno intercept(), jak i preventDefault() są zwykle dozwolone, ale są przypadki, w których nie można ich wywołać. Nie można obsługiwać nawigacji za pomocą intercept(), jeśli nawigacja odbywa się między domenami. Nie możesz też anulować nawigacji za pomocą preventDefault(), jeśli użytkownik naciska w przeglądarce przyciski Wstecz lub Wprzód. Nie możesz też zablokować użytkowników w Twojej witrynie. (temat jest omawiany 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 np. rejestrować zdarzenie Analytics, aby wskazać, że użytkownik opuszcza Twoją witrynę.

Dlaczego warto dodać kolejne zdarzenie do platformy?

Detektor zdarzeń "navigate" centralizuje obsługę zmian adresów URL w aplikacji SPA. Jest to trudna propozycja, która korzysta ze starszych interfejsów API. Jeśli masz już zaimplementowane przekierowywanie w ramach własnej aplikacji SPA za pomocą interfejsu History API, możesz mieć dodany 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 w porządku, ale nie jest wyczerpujące. Linki mogą pojawiać się i znikać ze strony, a nie są jedynym sposobem nawigacji po stronach. Może to być np. wypełnienie formularza lub użycie mapy obrazkowej. Twoja strona może obsługiwać te funkcje, ale istnieje wiele możliwości, które można uprościć. Nowy interfejs Navigation API właśnie to umożliwia.

Ponadto nie obsługuje ona przewijania wstecz i do przodu. Jest jeszcze inne wydarzenie, "popstate".

Osobiście uważam, że interfejs History API może w pewnym stopniu pomóc w realizacji tych możliwości. W rzeczywistości ma ona tylko 2 obszary powierzchni: reaguje, gdy użytkownik naciśnie w przeglądarce przyciski Wstecz lub Do przodu, a także gdy przesyła i zastępuje adresy URL. Nie ma analogii do "navigate", chyba że ręcznie skonfigurujesz odsłuchiwanie zdarzeń kliknięcia, jak pokazano powyżej.

Wybieranie sposobu obsługi nawigacji

Plik navigateEvent zawiera wiele informacji na temat nawigacji, które pomogą Ci określić, jak sobie poradzić z konkretną nawigacją.

Najważniejsze właściwości to:

canIntercept
Jeśli ta wartość jest fałszem, nie możesz przechwycić nawigacji. Przechodzenie między domenami i przechodzenie między dokumentami nie może być przechwytywane.
destination.url
Prawdopodobnie najważniejsza informacja, którą należy wziąć pod uwagę podczas obsługi nawigacji.
hashChange
Wartość Prawda, jeśli nawigacja jest w tym samym dokumencie, a podpis cyfrowy jest jedyną częścią adresu URL, która różni się od bieżącego adresu URL. W nowoczesnych SPA skrót powinien służyć do łączenia 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 równa True, oznacza to, że nawigacja została zainicjowana przez link z atrybutem download. W większości przypadków nie musisz ich przechwytywać.
formData
Jeśli to pole nie jest puste, oznacza, że ta nawigacja jest częścią przesyłania formularza POST. Pamiętaj o tym, gdy zarządzasz nawigacją. Jeśli chcesz obsługiwać tylko nawigacje GET, nie przechwytuj nawigacji, w których formData nie jest równe null. Przykład obsługi przesłań formularza znajdziesz dalej w tym artykule.
navigationType
Jedno z tych wartości: "reload", "push", "replace" lub "traverse". Jeśli jest to "traverse", nawigacji nie można anulować 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 Twój kod wywołuje funkcję intercept({ handler }) z poziomu swojego odbiornika "navigate", informuje przeglądarkę, że przygotowuje stronę do nowego, zaktualizowanego stanu i że nawigacja może trochę potrwać.

Najpierw przeglądarka rejestruje pozycję przewijania dla bieżącego stanu, aby można było ją opcjonalnie przywrócić później, a potem wywołuje funkcję handler. Jeśli funkcja handler zwraca obietnicę (co dzieje się automatycznie w przypadku funkcji asynchronicznych), ta obietnica informuje przeglądarkę, ile czasu zajmuje nawigacja i czy została ona zakończona sukcesem.

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 ramach tego interfejsu API wprowadzamy pojęcie semantyczne, które rozumie przeglądarka: w ramach nawigacji SPA w ciągu czasu zmienia się adres URL dokumentu i jego stan. Ma to wiele potencjalnych zalet, w tym ułatwienia dostępu: przeglądarki mogą wyświetlać początek, koniec lub potencjalną awarię nawigacji. Na przykład Chrome aktywuje swój natywny wskaźnik wczytywania i pozwala użytkownikowi na interakcję z przyciskiem Zatrzymaj. (Obecnie nie występuje to, gdy użytkownik przechodzi 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 wywołania zwrotnego handler. Jeśli nie zaktualizujesz DOM-u od razu, przez pewien czas będzie wyświetlana stara treść wraz z nowym adresem URL. Ma to wpływ na takie kwestie jak rozwiązywanie adresów URL bezwzględnych podczas pobierania danych lub wczytywania nowych zasobów podrzędnych.

Sposób opóźnienia zmiany adresu URL jest omawiany na GitHubie, ale ogólnie zalecamy natychmiastowe zaktualizowanie strony za pomocą jakiegoś zastępnika dla nadchodzą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 uniknąć problemów z rozpatrywaniem adresów URL, a także sprawia, że działanie wydaje się szybkie, ponieważ użytkownik otrzymuje natychmiastową odpowiedź.

Przerywanie sygnałów

Ponieważ w obiekcie intercept() możesz wykonywać zadania asynchroniczne, nawigacja może stać się zbędna. Dzieje się tak, gdy:

  • Użytkownik klika inny link lub kod powoduje inną nawigację. W tym przypadku stara nawigacja zostaje zastąpiona nową.
  • Użytkownik klika przycisk „Stop” w przeglądarce.

Aby uwzględnić wszystkie te możliwości, zdarzenie przekazywane do odbiornika "navigate" zawiera właściwość signal, która jest obiektem AbortSignal. Więcej informacji znajdziesz w sekcji Anulowanie pobierania.

Krótko mówiąc, jest to obiekt, który uruchamia zdarzenie, gdy należy przerwać pracę. W szczególności możesz przekazać AbortSignal do wszystkich wywołań do fetch(), co spowoduje anulowanie żądań sieci w trakcie ich przesyłania, jeśli nawigacja zostanie zablokowana. Pozwoli to zaoszczędzić przepustowość użytkownika i odrzucić wartość Promise zwracaną przez fetch(), uniemożliwiając dalszemu kodowi wykonywanie takich działań jak aktualizowanie interfejsu DOM w celu wyświetlenia nieprawidłowej nawigacji po stronie.

Oto poprzedni przykład, ale z użyciem funkcji getArticleContent, który pokazuje, jak można używać funkcji AbortSignal z funkcją 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 automatycznie spróbuje przewinąć stronę.

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

W przypadku ponownego wczytania i przechodzenia oznacza to przywrócenie pozycji przewijania do miejsca, w którym ostatnio wyświetlano ten wpis historii.

Domyślnie dzieje się to po spełnieniu obietnicy zwracanej przez handler, ale jeśli chcesz przewinąć wcześniej, możesz wywołać 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 wyłączyć automatyczne przewijanie, ustawiając opcję scrollintercept() na "manual":

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

Obsługa ostrości

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

Możesz zrezygnować z tego zachowania, ustawiając opcję focusReset w sekcji intercept() na "manual":

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

Zdarzenia powodzenia i niepowodzenia

Gdy wywołasz metodę obsługi intercept(), może się zdarzyć jedno z tych 2 wydarzeń:

  • Jeśli zwrócony element Promise spełnia kryteria (lub nie wywołano metody intercept()), interfejs Navigation API wywoła element "navigatesuccess" z wartością Event.
  • Jeśli zwrócona wartość Promise jest odrzucana, interfejs API wywoła "navigateerror" z wartością ErrorEvent.

Te zdarzenia umożliwiają scentralizowane przetwarzanie przez kod informacji o powodzeniu lub niepowodzeniu. Możesz na przykład ukryć wyświetlany wcześniej wskaźnik postępu:

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

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

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

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

navigation.currentEntry zapewnia dostęp do bieżącego wpisu. Jest to obiekt opisujący, gdzie znajduje się użytkownik. Ten wpis zawiera bieżący adres URL, metadane, które można wykorzystać do identyfikacji tego wpisu w ciągu czasu, oraz stan podany przez dewelopera.

Metadane obejmują key, unikalną właściwość ciągu znaków każdego wpisu, która reprezentuje bieżący wpis i jego miejsce. Klucz pozostaje taki sam, nawet jeśli adres URL lub stan bieżącego wpisu ulegnie zmianie. Nadal jest w tym samym gnieździe. Jeśli natomiast użytkownik naciśnie Wstecz, a następnie ponownie otworzy tę samą stronę, wartość key się zmieni, ponieważ nowy wpis utworzy nowy slot.

Dla dewelopera key jest przydatna, ponieważ interfejs Navigation API umożliwia przekierowanie użytkownika bezpośrednio do wpisu o odpowiednim kluczu. Możesz go trzymać, nawet w stanach innych wpisów, aby łatwo przełączać się 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 API Nawigacji udostępnia pojęcie „stanu”, czyli informacji dostarczonych przez dewelopera, które są przechowywane na stałe w bieżącym wpisie historii, ale nie są bezpośrednio widoczne dla użytkownika. Jest to bardzo podobne, ale ulepszone podejście do history.state w History API.

W interfejsie API nawigacji 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 ustawień

Chociaż obiekty stanu mogą ulegać zmianom, te zmiany nie są zapisywane w historii, dlatego:

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 nawigator skryptu:

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

Gdzie newState może być dowolny obiekt możliwy do sklonowania.

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 "navigate" może odebrać tę zmianę za pomocą navigateEvent.destination:

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

Synchroniczna aktualizacja stanu

Zazwyczaj lepiej jest zaktualizować stan asynchronicznie za pomocą navigation.reload({state: newState}), a następnie odbiorca "navigate" może zastosować ten stan. Czasami jednak zmiana stanu jest już w pełni zastosowana, gdy Twój kod ją wykryje, np. gdy użytkownik przełączy element <details> lub zmieni stan pola formularza. W takich przypadkach warto zaktualizować stan, aby zmiany były zachowane podczas ponownego wczytywania i przechodzenia. Można to zrobić za pomocą updateCurrentEntry():

navigation.updateCurrentEntry({state: newState});

Aby dowiedzieć się więcej o tej zmianie, weź udział w wydarzeniu:

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

Jeśli jednak zauważysz, że reagujesz na zmiany stanu w funkcji "currententrychange", możesz dzielić lub nawet powielać kod obsługi stanu między zdarzeniem "navigate" a zdarzeniem "currententrychange", podczas gdy funkcja navigation.reload({state: newState}) pozwoli Ci obsłużyć to w jednym miejscu.

Parametry stanu a parametry adresu URL

Stan może być obiektem ustrukturyzowanym, więc łatwo jest go używać do wszystkich stanów aplikacji. W wielu przypadkach lepiej jednak przechowywać stan w adresie URL.

Jeśli chcesz, aby stan został zachowany, gdy użytkownik udostępni adres URL innemu użytkownikowi, zapisz go w adresie URL. W przeciwnym razie obiekt state jest lepszym rozwiązaniem.

Dostęp do wszystkich wpisów

„Obecny wpis” to jednak nie wszystko. Interfejs API umożliwia też dostęp do całej listy wpisów, po których użytkownik się poruszał podczas korzystania z Twojej witryny. Aby uzyskać tablicę wpisów w postaci migawki, wywołaj metodę navigation.entries(). Można go wykorzystać np. do wyświetlania innego interfejsu w zależności od tego, jak użytkownik dotarł do danej strony, lub do sprawdzania poprzednich adresów URL i ich stanów. Nie jest to możliwe w przypadku obecnego interfejsu History API.

Możesz też nasłuchiwać zdarzenia "dispose" w poszczególnych 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 też podczas nawigacji. Jeśli na przykład przejdziesz 10 miejsc w tył, a potem w przód, 10 ostatnich wpisów w historii zostanie usuniętych.

Przykłady

Zdarzenie "navigate" jest wywoływane w przypadku wszystkich typów nawigacji, jak wspomniano powyżej. (w specyfikacji jest długi aneks ze wszystkimi możliwymi typami).

W przypadku wielu witryn najczęstszym przypadkiem jest kliknięcie przez użytkownika przycisku <a href="...">, ale istnieją 2 warte uwagi bardziej złożone typy nawigacji.

Nawigacja zautomatyzowana

Pierwsza to nawigacja programowa, która jest wywoływana przez metodę w kodzie po stronie klienta.

Aby wywołać nawigację, możesz zadzwonić pod numer navigation.navigate('/another_page') z dowolnego miejsca w kodeksie. Zajmie się tym scentralizowany detektor zdarzeń zarejestrowany w detektorze "navigate", a scentralizowany detektor zostanie wywołany synchronicznie.

Jest to ulepszona metoda agregacji starszych metod, takich jak location.assign() i friends, oraz metod interfejsu History API pushState()replaceState().

Metoda navigation.navigate() zwraca obiekt, który zawiera 2 wystąpienia Promise w obiekcie { committed, finished }. Dzięki temu wywołujący może czekać, aż przejście zostanie „zaakceptowane” (czyli widoczny adres URL zmieni się i będzie dostępny nowy adres NavigationHistoryEntry) lub „zakończone” (czyli wszystkie obietnice zwracane przez intercept({ handler }) są kompletne lub odrzucone z powodu błędu lub 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 go ustawić na "replace", aby zastąpić bieżący wpis w historii.
  • info: obiekt do przekazania zdarzeniu nawigacji za pomocą parametru navigateEvent.info.

W szczególności info może być przydatna np. do oznaczenia animacji, która powoduje wyświetlenie następnej strony. (Możesz też ustawić zmienną globalną lub uwzględnić ją w ramach #hash. Obie opcje są nieco niewygodne. Warto pamiętać, że info nie będzie odtwarzany, jeśli użytkownik przejdzie do innej strony, 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 }. Wspomnałem już o funkcji traverseTo() (która akceptuje parametr key oznaczający konkretny wpis w historii użytkownika) oraz o funkcji 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ą metody POST to specjalny typ nawigacji, który może przechwycić interfejs Navigation API. Chociaż zawiera dodatkowy ładunek, nawigacja jest nadal zarządzana centralnie przez "navigate".

Przesłanie formularza można wykryć, szukając w uzyskanym NavigateEvent właściwości formData. Oto przykład, w którym dowolne przesłanie formularza za pomocą tagu fetch() staje się przesyłaniem formularza, który 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 odbiornika zdarzeń "navigate" obecna specyfikacja interfejsu Navigation API nie powoduje wywołania "navigate" przy pierwszym wczytaniu strony. W przypadku witryn, które używają renderowania po stronie serwera (SSR) we wszystkich stanach, może to być w porządku – serwer może zwrócić prawidłowy stan początkowy, co jest najszybszym sposobem na dostarczenie treści użytkownikom. Jednak w przypadku witryn, które do tworzenia stron używają kodu po stronie klienta, może być konieczne utworzenie dodatkowej funkcji do inicjowania strony.

Kolejnym celowym wyborem w przypadku interfejsu Navigation API jest to, że działa on tylko w ramach jednego elementu – strony najwyższego poziomu lub pojedynczego <iframe>. Ma to kilka ciekawych konsekwencji, które są szczegółowo opisane w specyfikacji, ale w praktyce zmniejszy to dezorientację deweloperów. W poprzedniej wersji interfejsu History API występowało kilka mylących przypadków skrajnych, takich jak obsługa ramek. Zrewidowany interfejs Navigation API obsługuje te przypadki skrajne od samego początku.

Nie ma jeszcze konsensusu co do programowego modyfikowania ani przestawiania listy pozycji, po których użytkownik się poruszał. Ta kwestia jest obecnie analizowana, ale jedną z opcji jest zezwolenie na usuwanie tylko wpisów historycznych lub „wszystkich przyszłych wpisów”. Ta druga opcja pozwoliłaby na tymczasowy stan. Jako programista mogę na przykład:

  • zadać użytkownikowi pytanie, przechodząc do nowego adresu URL lub stanu;
  • zezwala użytkownikowi na dokończenie pracy (lub powrót do poprzedniego ekranu).
  • usunąć pozycję z historii po ukończeniu zadania.

Może to być idealne rozwiązanie w przypadku tymczasowych modali lub reklam przejściowych: nowy adres URL może być zamknięty za pomocą gestów, ale użytkownik nie może przypadkowo przejść do następnej strony, aby otworzyć go ponownie (ponieważ wpis został usunięty). Nie jest to możliwe w ramach 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 Denicola.

Chociaż klasyczne History API wydaje się proste, nie jest ono dobrze zdefiniowane i ma wiele problemów związanych z rzadkimi przypadkami oraz z sposobem jego implementacji w różnych przeglądarkach. Mamy nadzieję, że podzielisz się z nami opinią na temat nowego interfejsu Navigation API.

Pliki referencyjne

Podziękowania

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