Nowoczesny routing po stronie klienta: interfejs API nawigacji

Standaryzacja kierowania po stronie klienta za pomocą zupełnie nowego interfejsu API, który całkowicie zmienia tworzenie aplikacji jednostronicowych.

Obsługa przeglądarek

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

Źródło

Aplikacje jednostronicowe (SPA) to jedna z głównych funkcji aplikacji, których główną funkcją jest dynamiczne przepisywanie treści w miarę interakcji użytkownika z witryną. Nie jest to domyślna metoda wczytywania zupełnie nowych stron z serwera.

Chociaż aplikacje jednoosobowe umożliwiły korzystanie z tej funkcji za pomocą interfejsu History API (lub w niektórych przypadkach poprzez dostosowanie części witryny), to niezbędny interfejs API został opracowany na długo, zanim te aplikacje stały się normą. W sieci wzywa się do zupełnie nowego podejścia. Interfejs Navigation API to proponowany interfejs API, który wymaga całkowitej renowacji tej przestrzeni i nie próbuje po prostu wprowadzać poprawek do najgorszych elementów interfejsu History API. (Na przykład firma Scroll Restoration wprowadziła poprawkę interfejsu History API, zamiast próbować ją ulepszać).

Ten post ogólnie opisuje interfejs Navigation API. Jeśli chcesz zapoznać się z propozycją techniczną, sprawdź 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 umożliwia to zastąpienie domyślnego działania przeglądarki dla danego działania przez kod. W przypadku aplikacji SPA oznacza to prawdopodobnie, że użytkownik pozostaje na tej samej stronie i wczytuje lub zmienia jej zawartość.

Do detektora "navigate", który zawiera informacje o nawigacji, np. docelowy adres URL, jest przekazywany element NavigateEvent, dzięki któremu możesz zareagować na elementy nawigacyjne w jednym miejscu. Podstawowy detektor "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 zarządzać na dwa sposoby:

  • Wywołuję intercept({ handler }) (jak opisano powyżej), aby obsłużyć nawigację.
  • Dzwonię pod numer preventDefault(). Może to spowodować całkowite anulowanie nawigacji.
.

W tym przykładzie wywołujemy w zdarzeniu funkcję intercept(). Przeglądarka wywołuje wywołanie zwrotne handler, które powinno skonfigurować następny stan witryny. Spowoduje to utworzenie obiektu przejścia (navigation.transition), którego inny kod może używać do śledzenia postępów nawigacji.

Zarówno intercept(), jak i preventDefault() są zwykle dozwolone, ale w niektórych przypadkach nie można się do nich zadzwonić. Nie możesz obsługiwać nawigacji za pomocą intercept(), jeśli jest to nawigacja z innej domeny. Nie można też anulować nawigacji w usłudze preventDefault(), jeśli użytkownik naciska przycisk Wstecz lub Dalej w przeglądarce. nie powinno to umożliwiać przyciągania użytkowników do witryny. Jest to omawiane na GitHubie.

Nawet jeśli nie możesz zatrzymać lub przechwycić nawigacji, zdarzenie "navigate" będzie się uruchamiać. Mają one charakter informacyjny, więc Twój kod może np. rejestrować zdarzenie Analytics, aby zasygnalizować, że użytkownik opuszcza Twoją witrynę.

Po co dodawać kolejne zdarzenie na platformie?

Detektor zdarzeń "navigate" centralizuje obsługę zmian adresów URL w ramach SPA. Przy korzystaniu ze starszych interfejsów API jest to trudna propozycja. Jeśli kiedykolwiek zdarzyło Ci się utworzyć kierowanie na własne potrzeby SPA przy użyciu interfejsu History API, możesz dodać taki kod:

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 jest prawidłowe, ale nie wyczerpujące. Linki mogą pojawiać się i znikać na stronie i nie są jedynym sposobem poruszania się użytkowników po stronie. Mogą na przykład przesłać formularz, a nawet użyć mapy zdjęcia. Możesz sobie z nimi poradzić na swojej stronie, ale istnieje wiele możliwości, które można tylko uprościć. Oto niektóre z możliwości, jakie zapewnia nowy interfejs Navigation API.

Dodatkowo powyższy kod nie obsługuje nawigacji wstecz i do przodu. Zostało też utworzone inne wydarzenie: "popstate".

Osobiście często mam wrażeniejakby interfejs History API mógł pomóc w tych możliwościach. W rzeczywistości witryna zawiera jednak tylko 2 obszary: odpowiada, gdy użytkownik naciśnie Wstecz lub Dalej w przeglądarce, oraz będzie przesuwać i zastępować adresy URL. Nie ma analogii ze zdarzeniem "navigate" z wyjątkiem sytuacji, gdy ręcznie skonfigurujesz detektory zdarzeń kliknięcia, jak pokazano powyżej.

Podejmowanie decyzji o sposobie obsługi nawigacji

navigateEvent zawiera wiele informacji o nawigacji, których możesz użyć przy podejmowaniu decyzji, jak obsłużyć daną nawigację.

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

canIntercept
Jeśli to fałsz, nie można przechwycić nawigacji. Nie można przechwytywać nawigacji między domenami ani przemierzania między dokumentami.
destination.url
To prawdopodobnie najważniejsza informacja, którą należy wziąć pod uwagę podczas obsługi nawigacji.
hashChange
Prawda, jeśli nawigacją jest ten sam dokument, a hasz to jedyna część adresu URL, która różni się od bieżącego. We współczesnych aplikacjach SPA hasz powinien być używany do tworzenia linków do różnych części bieżącego dokumentu. Jeśli więc hashChange ma wartość prawda, prawdopodobnie nie musisz przechwytywać tej nawigacji.
downloadRequest
Jeśli to prawda, nawigacja została zainicjowana przez link z atrybutem download. W większości przypadków nie trzeba tego robić.
formData
Jeśli nie jest to wartość null, ta nawigacja jest częścią przesłania formularza POST. Pamiętaj, aby wziąć to pod uwagę podczas obsługi nawigacji. Jeśli chcesz obsługiwać tylko nawigację 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 artykułu.
navigationType
To jest "reload", "push", "replace" lub "traverse". Jeśli jest to "traverse", tej nawigacji nie można anulować w: 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 intercept({ handler }) z detektora "navigate", informuje przeglądarkę, że przygotowuje teraz stronę do nowego, zaktualizowanego stanu, a nawigacja może trochę potrwać.

Przeglądarka zaczyna od zarejestrowania pozycji przewijania w bieżącym stanie, więc można ją później przywrócić, a następnie wywołuje wywołanie zwrotne handler. Jeśli handler zwraca obietnicę (co dzieje się automatycznie w przypadku funkcji asynchronicznych), informuje ona przeglądarkę, ile czasu zajmuje nawigacja i czy się udaje.

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 zalet, m.in. ułatwienia dostępu: przeglądarki mogą wyświetlać początek, koniec lub potencjalne błędy 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/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 przykład na względną rozdzielczość adresów URL podczas pobierania danych lub wczytywania nowych zasobów podrzędnych.

Sposób opóźnienia zmiany adresu URL jest dyskutowany na GitHubie, ale zwykle zalecamy natychmiastowe zaktualizowanie strony, dodając jakiś obiekt zastępczy 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);
      },
    });
  }
});

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

Przerwij sygnały

Ponieważ można wykonywać pracę asynchroniczną z użyciem modułu obsługi intercept(), nawigacja może stać się nadmiarowym. Dzieje się tak, gdy:

  • Użytkownik klika inny link lub jakaś część kodu wykonuje inną nawigację. W takim przypadku stara nawigacja zostanie porzucona i zastąpiona nową.
  • Użytkownik klika przycisk „Zatrzymaj” w przeglądarce.

Aby uwzględnić każdą z tych możliwości, zdarzenie przekazywane do detektora "navigate" zawiera właściwość signal, czyli AbortSignal. Więcej informacji znajdziesz w artykule Przerwane pobieranie.

W skrócie chodzi o obiekt, który wywołuje zdarzenie, gdy należy zatrzymać pracę. W szczególności możesz przekazać AbortSignal podczas każdego połączenia z numerem fetch(), co spowoduje anulowanie przesyłanych żądań sieciowych, jeśli nawigacja będzie uprzedzona. Zaoszczędzi to przepustowość łącza użytkownika i odrzuci żądanie Promise zwrócone przez fetch(). Zapobiegnie to wykonaniu poniższych działań, takich jak aktualizowanie modelu DOM w celu wyświetlania nieprawidłowej nawigacji na stronie.

Oto poprzedni przykład, który zawiera tekst getArticleContent w tekście i pokazuje, jak używać właściwości AbortSignal z tabelą 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

Jeśli intercept() dostosujesz nawigację, przeglądarka spróbuje automatycznie obsłużyć przewijanie.

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

W przypadku ponownego załadowania i przemierzania oznacza to przywrócenie pozycji przewijania do miejsca, w którym została ostatnio wyświetlona dany wpis historii.

Domyślnie dzieje się tak po zrealizowaniu obietnicy zwróconej przez handler. Jeśli jednak warto przewinąć stronę 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 zwrócona przez element handler zostanie rozstrzygnięta, przeglądarka ustawi pierwszy element z ustawionym autofocus atrybutem lub element <body>, jeśli żaden z elementów nie ma tego atrybutu.

Możesz zrezygnować z tej funkcji, ustawiając opcję focusReset dla intercept() na "manual":

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

Zdarzenia sukcesu i niepowodzenia

Po wywołaniu modułu obsługi intercept() zajdzie jedna z tych sytuacji:

  • Jeśli zwrócona wartość Promise zostanie wykonana (lub nie wywołasz intercept()), interfejs Navigation API uruchomi "navigatesuccess" z Event.
  • Jeśli zwrócona wartość Promise odrzuci, interfejs API uruchomi "navigateerror" z ErrorEvent.

Te zdarzenia umożliwiają kodowi reagowanie na sukces lub porażkę w scentralizowany sposób. Aby odnieść sukces, możesz na przykład ukryć wyświetlany wcześniej wskaźnik postępu, np.:

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

W przypadku niepowodzenia może się też wyświetlić komunikat o błędzie:

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

Szczególnie przydatny jest detektor zdarzeń "navigateerror", który odbiera sygnał ErrorEvent, ponieważ gwarantuje otrzymywanie wszelkich błędów związanych z kodem powodującym skonfigurowanie nowej strony. 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, który opisuje bieżącą lokalizację użytkownika. Ten wpis zawiera bieżący adres URL, metadane, których można użyć do jego identyfikacji na przestrzeni czasu, oraz stan podany przez dewelopera.

Metadane zawierają key – unikalną właściwość ciągu znaków każdego wpisu, która reprezentuje aktualny wpis i jego boks. Ten klucz pozostaje taki sam nawet po zmianie adresu URL lub stanu bieżącego wpisu. Nadal jest w tym samym miejscu. Jeśli użytkownik naciśnie Wstecz i ponownie otworzy tę samą stronę, key zmieni się, ponieważ ten nowy wpis utworzy nowy boks.

Interfejs key jest przydatny dla programistów, ponieważ interfejs Navigation API umożliwia bezpośrednie przechodzenie użytkownika do wpisu z pasującym kluczem. Możesz przytrzymać go nawet w przypadku innych elementó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 wyświetla pojęcie „state” (stan), czyli przekazane przez programistę informacji, które są trwale przechowywane w bieżącym wpisie historii, ale nie są widoczne bezpośrednio dla użytkownika. Jest bardzo podobny do history.state w interfejsie History API, ale został rozszerzony.

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 jest to undefined.

Stan ustawienia

Chociaż obiekty stanu mogą być mutowane, te zmiany nie są zapisywane z powrotem we wpisie 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 ustawienia stanu to podczas nawigacji po skryptach:

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

Gdzie newState może być dowolnym obiektem możliwym do sklonowania.

Jeśli chcesz zaktualizować stan bieżącego wpisu, najlepiej jest przeprowadzić 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

Ogólnie rzecz biorąc, lepiej aktualizować stan asynchronicznie za pomocą funkcji navigation.reload({state: newState}), wtedy odbiornik "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. Umożliwia to: updateCurrentEntry()

navigation.updateCurrentEntry({state: newState});

Pojawi się też powiadomienie o tej zmianie:

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

Jeśli jednak stwierdzisz, że reagujesz na zmiany stanu w "currententrychange", możliwe, że dzielisz lub zduplikujesz kod obsługi stanu między zdarzenie "navigate" i "currententrychange", natomiast navigation.reload({state: newState}) umożliwia obsługę tego stanu w jednym miejscu.

Stan a parametry adresów 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ć ten 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 lepszą opcją jest obiekt stanu.

Dostęp do wszystkich wpisów

„Bieżący wpis” to jednak nie wszystko. Interfejs API zapewnia również dostęp do całej listy wpisów, które przeszedł użytkownik podczas korzystania z Twojej witryny, za pomocą wywołania navigation.entries(), które zwraca tablicę zrzutów wpisów. Pozwala to na przykład wyświetlać różne UI w zależności od tego, jak użytkownik przeszedł na określoną stronę, albo po prostu wrócić do poprzednich adresów URL lub ich stanów. Nie jest to możliwe przy obecnym interfejsie History API.

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 ma to też miejsce podczas korzystania z nawigacji. Jeśli na przykład przeskoczysz o 10 miejsc, a następnie przejdziesz dalej, tych 10 wpisów z historii zostanie usuniętych.

Przykłady

Zdarzenie "navigate" uruchamia się w przypadku wszystkich rodzajów nawigacji, o których mowa powyżej. (W specyfikacji wszystkich możliwych typów znajduje się długi dodatek).

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

Automatyczna nawigacja

Pierwsza z nich to zautomatyzowana nawigacja, w której nawigacja jest wywoływana przez wywołanie metody w kodzie po stronie klienta.

Aby uruchomić nawigację, możesz wywołać funkcję navigation.navigate('/another_page') z dowolnego miejsca w kodzie. Będzie ona obsługiwana przez scentralizowany detektor zdarzeń zarejestrowany w detektorze "navigate", a scentralizowany detektor będzie wywoływany 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 instancje Promise w tabeli { 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 zwrócone przez intercept({ handler }) są kompletne – lub odrzucone z powodu niepowodzenia lub zastąpienia w innej nawigacji).

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 NavigationHistoryEntry.
  • history: można ustawić wartość "replace", aby zastąpić bieżący wpis w historii.
  • info: obiekt przekazywany do zdarzenia nawigacji przez navigateEvent.info.

Zastosowanie info może być np. przydatne do wskazania konkretnej animacji, która powoduje wyświetlenie następnej strony. Możesz też ustawić zmienną globalną lub uwzględnić ją w #hash. Obie opcje są nieco niezręczne). Co ważne, ten element info nie zostanie odtworzony ponownie, jeśli użytkownik później włączy nawigację, np. za pomocą przycisków Wstecz i Dalej. W takich przypadkach wartość to zawsze undefined.

Demonstracja otwarcia od lewej lub prawej
.

navigation ma też wiele innych metod nawigacji, które zwracają obiekt zawierający { committed, finished }. Wspomniałem już wcześniej traverseTo() (która akceptuje key, który oznacza konkretny wpis w historii użytkownika) oraz navigate(). Uwzględnia 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 kodu HTML <form> za pomocą metody POST to specjalny typ nawigacji, który może zostać przechwycony przez 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łaściwości formData w: NavigateEvent. 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 detektora zdarzeń "navigate" bieżąca specyfikacja interfejsu Navigation API nie wyzwala 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 witrynach, które do tworzenia stron używają kodu po stronie klienta, może być konieczne utworzenie dodatkowej funkcji inicjującej stronę.

Innym zamierzonym wyborem interfejsu Navigation API jest to, że działa on tylko w jednej ramce, czyli na stronie najwyższego poziomu lub w pojedynczej konkretnej ramce <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.

Co więcej, nie ma jeszcze konsensusu w sprawie programowej modyfikacji lub zmiany kolejności wpisów, które przeglądał użytkownik. Ta kwestia jest obecnie w trakcie dyskusji, ale istnieje możliwość zezwolenia tylko na usuwanie – zarówno wpisów historycznych, jak i „wszystkich przyszłych zgłoszeń”. Ten drugi zezwala na stosowanie stanu tymczasowego. Jako programista mogę na przykład:

  • zadaj użytkownikowi pytanie, przechodząc do nowego adresu URL lub stanu
  • zezwolić użytkownikowi na ukończenie pracy (lub przejście wstecz)
  • usuń wpis w historii po ukończeniu zadania

To idealne rozwiązanie w przypadku tymczasowych modałów i reklam pełnoekranowych: użytkownik może opuścić nowy adres URL, używając gestu Wstecz, ale nie będzie mógł przypadkowo użyć przycisku Dalej, aby ponownie go otworzyć (ponieważ wpis został usunięty). Nie jest to jednak możliwe przy obecnym interfejsie History API.

Wypróbuj interfejs Navigation API

Interfejs Navigation API jest dostępny w Chrome 102 bez flag. Możesz też wypróbować prezentację autorstwa Domenica Denicoli.

Chociaż klasyczny interfejs History API wydaje się prosty, nie jest zbyt sprecyzowany i zawiera dużą liczbę problemów w różnych przypadkach jego implementacji w różnych przeglądarkach. Liczymy, że zechcesz podzielić się z nami opinią na temat nowego interfejsu Navigation API.

Pliki referencyjne

Podziękowania

Dziękujemy Thomasowi Steinerowi, Domenicowi Denicoli i Natemu Chapinowi za opinię o tym poście. Baner powitalny z aplikacji Unsplash, którego autorem jest Jeremy Zero.