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 niewygodne API, które zostało opracowane 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. rozszerzenie Scroll Restoration naprawiło interfejs History API zamiast próbować go na nowo wymyślić).

W tym poście opisujemy ogólnie interfejs API Nawigacja. Aby zapoznać się z propozycją techniczną, zapoznaj się z wersją roboczą raportu w repozytorium WICG.

Przykład użycia

Aby korzystać z interfejsu Navigation API, najpierw dodaj detektor "navigate" w globalnym obiekcie navigation. Zdarzenie to jest zasadniczo scentralizowane: jest wywoływane we wszystkich typach nawigacji, niezależnie od tego, czy użytkownik wykonał działanie (np.kliknął link, przesłał formularz lub cofnął się do przodu), czy też gdy nawigacja jest uruchamiana automatycznie (tj. 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łujemy w zdarzeniu 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 w niektórych przypadkach nie można się do nich zadzwonić. Nie można obsługiwać nawigacji za pomocą intercept(), jeśli nawigacja odbywa się między domenami. Nie możesz też anulować nawigacji w usłudze preventDefault(), jeśli użytkownik naciska przycisk Wstecz lub Dalej w przeglądarce. Nie powinno być też możliwości zablokowania użytkowników witryny. (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 na jednym ekranie. Przy korzystaniu ze starszych interfejsów API jest to trudna propozycja. 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 użytkowników po stronach. Mogą na przykład przesłać formularz, a nawet użyć mapy zdjęcia. Twoja strona może sobie z nimi poradzić, 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 często wydaje się być przydatny w tym zakresie. 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 to fałsz, nie można 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ść prawda, 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 trzeba tego robić.
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ładowy sposób obsługi przesłanych formularzy znajdziesz w dalszej części tego artykułu.
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 być podobna do tej:

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 związku z tym ten interfejs API wprowadza semantyczne koncepcję rozumieną przez przeglądarkę: obecnie nawigacja w SPA toczy się w czasie, powodując zmianę adresu URL i stanu dokumentu na nowy. 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 natywny wskaźnik ładowania i umożliwia użytkownikowi interakcję z przyciskiem zatrzymania. (Obecnie nie dzieje się tak, gdy użytkownik korzysta z przycisków Wstecz i dalej, ale wkrótce zostanie to rozwiązane.

W przypadku przechwytywania elementów nawigacyjnych nowy adres URL zaczyna obowiązywać tuż przed wywołaniem zwrotnym handler. Jeśli nie zaktualizujesz DOM od razu, zostanie utworzony okres, w którym stara treść jest wyświetlana 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);
      },
    });
  }
});

Dzięki temu nie tylko unikasz problemów z rozpoznawaniem adresów URL, lecz także szybko reagujesz.

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 takim przypadku stara nawigacja zostanie porzucona i 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 przez strony oznacza to przywrócenie pozycji przewijania do pozycji, w której 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 zrezygnować z automatycznej obsługi przewijania, ustawiając opcję scroll w ustawieniach intercept() 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ócona wartość Promise zostanie zrealizowana (lub nie wywołano intercept()), interfejs Navigation API uruchomi "navigatesuccess" z Event.
  • Jeśli zwrócona wartość Promise odrzuci, interfejs API uruchomi się "navigateerror" z ErrorEvent.

Te zdarzenia umożliwiają kodowi reagowanie na sukces lub porażkę w scentralizowany sposób. 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. Ten klucz pozostaje taki sam nawet po zmianie adresu URL lub stanu bieżącego wpisu. Nadal jest w tym samym gnieździe. Jeśli użytkownik naciśnie Wstecz i ponownie otworzy tę samą stronę, key zmieni się, ponieważ ten nowy wpis utworzy nowy boks.

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 informacje podawane 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 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 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 detektor zdarzeń "navigate" może odebrać tę zmianę za pomocą usługi navigateEvent.destination:

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

Synchroniczne aktualizowanie stanu

Zazwyczaj lepiej jest zaktualizować stan asynchronicznie za pomocą navigation.reload({state: newState}), a następnie odbiorca "navigate" może zastosować ten stan. Zdarza się jednak, że zmiana stanu będzie już w pełni stosowana do momentu, gdy usłyszy o niej kod, np. gdy użytkownik przełączy element <details> lub zmieni stan danych wejściowych formularza. W takich przypadkach może być konieczna aktualizacja stanu, aby zmiany te zostały zachowane przez ponowne załadowania i przemierzanie. 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 strukturalnym, więc łatwo jest go używać w całym stanie aplikacji. W wielu przypadkach lepiej jednak przechowywać stan w adresie URL.

Jeśli oczekujesz, że stan zostanie 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żesz na przykład wyświetlić inny interfejs na podstawie tego, jak użytkownik dotarł do określonej strony, lub po prostu sprawdzić poprzednie adresy URL lub ich stany. 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 ma to też miejsce podczas korzystania z 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).

Choć w przypadku wielu witryn użytkownik najczęściej klika przycisk <a href="...">, warto wspomnieć o 2 ciekawszych, bardziej złożonych typach 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.

Ma to na celu ulepszenie agregacji starszych metod, takich jak location.assign() i znajomi, oraz metod pushState() i replaceState() interfejsu History API.

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 będzie „zatwierdzone” (widoczny adres URL uległ zmianie i dostępny jest nowy element NavigationHistoryEntry) lub „ukończony” (wszystkie obietnice zwracane przez funkcję intercept({ handler }) są kompletne – lub odrzucone z powodu niepowodzenia lub wywłaszczenia przez inną nawigację).

Metoda navigate ma też obiekt options, 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 przekazywany do zdarzenia nawigacji przez 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 wartość to zawsze undefined.

Prezentacja otwierania od lewej lub prawej

navigation ma też kilka innych metod nawigacji, które zwracają obiekt zawierający { committed, finished }. Wspomnałem już o funkcji traverseTo() (która przyjmuje parametr key oznaczający konkretny wpis w historii użytkownika) i funkcji navigate(). Obejmuje też back(), forward() i reload(). Wszystkie te metody są obsługiwane (tak samo 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 on dodatkowy ładunek, nawigacja jest nadal obsługiwana centralnie przez detektor "navigate".

Przesłanie formularza można wykryć, szukając w uzyskanym NavigateEvent właściwości formData. Oto przykład, który po prostu zamienia dowolny przesłany formularz w taki, który pozostaje na bieżącej stronie przez fetch():

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 we wszystkich stanach, które używają renderowania po stronie serwera (SSR), nie ma problemu – serwer może zwrócić poprawny stan początkowy, co jest najszybszym sposobem dostarczenia treści do użytkowników. 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 szereg ciekawych konsekwencji, które są bardziej udokumentowane w specyfikacji, ale w praktyce zmniejszają dezorientację programistów. Poprzedni interfejs History API zawierał wiele mylących przypadków skrajnych, np. obsługę ramek. Nowy interfejs Navigation API obsługuje je 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 rzadko występującymi przypadkami i 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.

O ile nie stwierdzono inaczej, treść tej strony jest objęta licencją Creative Commons – uznanie autorstwa 4.0, a fragmenty kodu są dostępne na licencji Apache 2.0. Szczegółowe informacje na ten temat zawierają zasady dotyczące witryny Google Developers. Java jest zastrzeżonym znakiem towarowym firmy Oracle i jej podmiotów stowarzyszonych.

Ostatnia aktualizacja: 2021-08-25 UTC.