Przejścia widoku między dokumentami w aplikacjach wielostronicowych

Przejście między dwoma różnymi dokumentami jest nazywane przejściem widoku wielu dokumentów. Dzieje się tak zazwyczaj w przypadku aplikacji wielostronicowych (MPA). Przejścia między dokumentami są obsługiwane w Chrome od wersji Chrome 126.

Obsługa przeglądarek

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

Przejścia między widokami różnych dokumentów opierają się na tych samych elementach składowych i zasadach co przejścia widoku tego samego dokumentu, co jest bardzo zamierzone:

  1. Przeglądarka wykonuje migawki elementów z unikalnym atrybutem view-transition-name zarówno na starej, jak i nowej stronie.
  2. DOM jest aktualizowany podczas pomijania renderowania.
  3. I wreszcie przejścia są efektem animacji CSS.
.

Różnica w porównaniu z przejściami wyświetlania tego samego dokumentu polega na tym, że w przypadku przejścia w widoku różnych dokumentów nie trzeba wywoływać funkcji document.startViewTransition, aby rozpocząć przejście widoku. Wywołaniem przejścia z jednej strony do drugiej jest przejście z jednej strony na drugą, czyli działanie wykonywane przez użytkownika witryny, który zazwyczaj klika link.

Oznacza to, że nie ma żadnego interfejsu API, który trzeba wywołać, aby rozpocząć przejście między dwoma dokumentami. Należy jednak spełnić 2 warunki:

  • Oba dokumenty muszą istnieć w tym samym źródle.
  • Na obu stronach trzeba wyrazić zgodę na przeniesienie.

Oba te warunki zostały wyjaśnione w dalszej części tego dokumentu.


Przejścia między dokumentami są ograniczone do nawigacji z tej samej domeny

Przejścia między dokumentami są ograniczone tylko do nawigacji z tej samej domeny. Nawigacja jest uznawana za tę samą pochodzenie, jeśli pochodzenie obu stron uczestniczących w projekcie jest takie samo.

Źródło strony to połączenie użytego schematu, nazwy hosta i portu, zgodnie z informacjami na stronie web.dev.

Przykładowy adres URL ze schematem, nazwą hosta i portem. Łącznie tworzą one początek.
Przykładowy adres URL ze schematem, nazwą hosta i portem. Razem tworzą one początek.

Możesz na przykład przełączyć widok między dokumentami podczas przechodzenia z developer.chrome.com do developer.chrome.com/blog, ponieważ pochodzą one z tej samej domeny. Tego przejścia nie można dokonać podczas przechodzenia z developer.chrome.com na www.chrome.com, ponieważ dotyczy to zasobów z innej domeny i z tej samej witryny.


Przejścia między dokumentami są dobrowolne

Aby możliwe było przełączanie widoku między dwoma dokumentami, obie strony muszą wyrazić na to zgodę. Można to zrobić, korzystając z reguły @view-transition at w CSS.

W regule @view-transition ustaw deskryptor navigation na auto, aby umożliwić przejścia między widokami dla nawigacji w tej samej domenie w innych dokumentach.

@view-transition {
  navigation: auto;
}

Ustawiając deskryptor navigation na auto, zezwalasz na przełączanie widoków w przypadku tych elementów NavigationType:

  • traverse
  • push lub replace, jeśli aktywacja nie została zainicjowana przez użytkownika za pomocą mechanizmów interfejsu przeglądarki.

Nawigacja wyłączona w usłudze auto to na przykład nawigacja za pomocą paska adresu URL, klikanie zakładki, a także dowolna forma ponownego załadowania zainicjowanego przez użytkownika lub skrypt.

Jeśli nawigacja trwa zbyt długo (w przypadku Chrome przekracza 4 sekundy), przejście do widoku jest pomijane za pomocą parametru TimeoutError DOMException.

Wersja demonstracyjna przejścia między widokami różnych dokumentów

Obejrzyj prezentację narzędzia Stack Navigator, w której wykorzystano przejścia między widokami. Nie ma tu żadnych wywołań funkcji document.startViewTransition(). Przełączanie widoków jest wywoływane przez przechodzenie z jednej strony na inną.

.
Nagranie wersji demonstracyjnej Nawigatora. Wymaga Chrome w wersji 126 lub nowszej.

Dostosowywanie przejść między widokami między dokumentami

Aby dostosować przejścia między widokami różnych dokumentów, możesz skorzystać z pewnych funkcji platformy internetowej.

Funkcje te nie są częścią specyfikacji interfejsu View Transfer API, ale zostały zaprojektowane do używania razem z nimi.

Wydarzenia pageswap i pagereveal

Obsługa przeglądarek

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

Źródło

Aby można było dostosować przejścia między widokami różnych dokumentów, specyfikacja HTML zawiera 2 nowe zdarzenia, których możesz użyć: pageswap i pagereveal.

Te 2 zdarzenia są wywoływane przy każdej nawigacji między dokumentami w tej samej domenie niezależnie od tego, czy nastąpi przejście do wyświetlenia. Jeśli nastąpi przeniesienie między tymi 2 stronami, dostęp do obiektu ViewTransition uzyskasz za pomocą właściwości viewTransition w tych zdarzeniach.

  • Zdarzenie pageswap jest wywoływane przed wyrenderowaniem ostatniej klatki strony. Pozwala to wprowadzić w ostatniej chwili zmiany na stronie wychodzącej, tuż przed wykonaniem starych zrzutów.
  • Zdarzenie pagereveal uruchamia się na stronie po tym, jak została zainicjowana lub ponownie aktywowana, ale przed pierwszą możliwością renderowania. Dzięki niemu można dostosować nową stronę przed wykonaniem nowych zrzutów.

Za pomocą tych zdarzeń możesz na przykład szybko ustawić lub zmienić niektóre wartości view-transition-name albo przekazywać dane z jednego dokumentu do innego, zapisując i odczytując dane z sessionStorage, aby dostosować przejście widoku przed jego faktycznym uruchomieniem.

let lastClickX, lastClickY;
document.addEventListener('click', (event) => {
  if (event.target.tagName.toLowerCase() === 'a') return;
  lastClickX = event.clientX;
  lastClickY = event.clientY;
});

// Write position to storage on old page
window.addEventListener('pageswap', (event) => {
  if (event.viewTransition && lastClick) {
    sessionStorage.setItem('lastClickX', lastClickX);
    sessionStorage.setItem('lastClickY', lastClickY);
  }
});

// Read position from storage on new page
window.addEventListener('pagereveal', (event) => {
  if (event.viewTransition) {
    lastClickX = sessionStorage.getItem('lastClickX');
    lastClickY = sessionStorage.getItem('lastClickY');
  }
});

Jeśli chcesz, możesz pominąć przejście w obu wydarzeniach.

window.addEventListener("pagereveal", async (e) => {
  if (e.viewTransition) {
    if (goodReasonToSkipTheViewTransition()) {
      e.viewTransition.skipTransition();
    }
  }
}

Obiekt ViewTransition w obiekcie pageswap i pagereveal to 2 różne obiekty. Obsługują też różne obietnice w różny sposób:

  • pageswap: po ukryciu dokumentu stary obiekt ViewTransition jest pomijany. W takim przypadku viewTransition.ready odrzuca, a viewTransition.finished zamyka.
  • pagereveal: obietnica updateCallBack została już zrealizowana. Możesz wykorzystać obietnice viewTransition.ready i viewTransition.finished.

Obsługa przeglądarek

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

Źródło

Zarówno w zdarzeniach pageswap, jak i pagereveal możesz też podejmować działania na podstawie adresów URL starej i nowej strony.

Na przykład w Nawigatorze stosu MPA typ animacji, który zostanie użyty, zależy od ścieżki nawigacji:

  • Podczas przechodzenia ze strony Przegląd na stronę z informacjami nowe treści muszą wsuwać się z prawej do lewej.
  • Podczas przechodzenia ze strony z informacjami na stronę Przegląd stara treść musi zostać przesunięta w lewo w prawo.

Aby to zrobić, potrzebujesz informacji o nawigacji, która ma miejsce w przypadku zdarzenia pageswap lub, w przypadku pagereveal, właśnie nastąpi.

W tym celu przeglądarki mogą teraz udostępniać obiekty NavigationActivation, które zawierają informacje o nawigacji po tej samej domenie. Ten obiekt ujawnia używany typ nawigacji oraz bieżące i końcowe wpisy historii miejsc docelowych, jak w narzędziu navigation.entries() interfejsu Navigation API.

Na aktywowanej stronie możesz uzyskać dostęp do tego obiektu za pomocą usługi navigation.activation. W wydarzeniu pageswap masz do niego dostęp za pomocą adresu e.activation.

Obejrzyj tę prezentację profili, w której wykorzystano informacje NavigationActivation w zdarzeniach pageswap i pagereveal, aby ustawić wartości view-transition-name w elementach, które muszą uczestniczyć w przejściu widoku.

Dzięki temu nie będzie trzeba dekorować każdego produktu z listy za pomocą z góry view-transition-name. Dzięki JavaScriptowi od razu dzieje się to tylko w elementach, które go potrzebują.

.
Nagranie prezentacji profili. Wymaga Chrome w wersji 126 lub nowszej.

Kod wygląda następująco:

// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
  if (e.viewTransition) {
    const targetUrl = new URL(e.activation.entry.url);

    // Navigating to a profile page
    if (isProfilePage(targetUrl)) {
      const profile = extractProfileNameFromUrl(targetUrl);

      // Set view-transition-name values on the clicked row
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';

      // Remove view-transition-names after snapshots have been taken
      // (this to deal with BFCache)
      await e.viewTransition.finished;
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
    }
  }
});

// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
    const fromURL = new URL(navigation.activation.from.url);
    const currentURL = new URL(navigation.activation.entry.url);

    // Navigating from a profile page back to the homepage
    if (isProfilePage(fromURL) && isHomePage(currentURL)) {
      const profile = extractProfileNameFromUrl(currentURL);

      // Set view-transition-name values on the elements in the list
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';

      // Remove names after snapshots have been taken
      // so that we're ready for the next navigation
      await e.viewTransition.ready;
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
    }
  }
});

Kod czyści się też po sobie, usuwając wartości view-transition-name po uruchomieniu przejścia. W ten sposób strona jest gotowa do kolejnych nawigacji i umożliwia poruszanie się po historii.

Aby Ci w tym pomóc, użyj tej funkcji narzędzia, która tymczasowo ustawia view-transition-name.

const setTemporaryViewTransitionNames = async (entries, vtPromise) => {
  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = name;
  }

  await vtPromise;

  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = '';
  }
}

Poprzedni kod można teraz uprościć w ten sposób:

// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
  if (e.viewTransition) {
    const targetUrl = new URL(e.activation.entry.url);

    // Navigating to a profile page
    if (isProfilePage(targetUrl)) {
      const profile = extractProfileNameFromUrl(targetUrl);

      // Set view-transition-name values on the clicked row
      // Clean up after the page got replaced
      setTemporaryViewTransitionNames([
        [document.querySelector(`#${profile} span`), 'name'],
        [document.querySelector(`#${profile} img`), 'avatar'],
      ], e.viewTransition.finished);
    }
  }
});

// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
    const fromURL = new URL(navigation.activation.from.url);
    const currentURL = new URL(navigation.activation.entry.url);

    // Navigating from a profile page back to the homepage
    if (isProfilePage(fromURL) && isHomePage(currentURL)) {
      const profile = extractProfileNameFromUrl(currentURL);

      // Set view-transition-name values on the elements in the list
      // Clean up after the snapshots have been taken
      setTemporaryViewTransitionNames([
        [document.querySelector(`#${profile} span`), 'name'],
        [document.querySelector(`#${profile} img`), 'avatar'],
      ], e.viewTransition.ready);
    }
  }
});

Oczekiwanie na wczytanie treści z blokowaniem renderowania

Obsługa przeglądarek

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

W niektórych przypadkach możesz chcieć wstrzymać pierwsze wyrenderowanie strony do czasu, aż dany element znajdzie się w nowym DOM. Pozwala to uniknąć migania i zapewnia stabilny stan animacji.

W elemencie <head> zdefiniuj co najmniej 1 identyfikator elementu, który musi być widoczny przed pierwszym renderowaniem strony. Aby to zrobić, użyj poniższego metatagu.

<link rel="expect" blocking="render" href="#section1">

Ten metatag oznacza, że element powinien być obecny w modelu DOM, a nie ładować treści. Na przykład w przypadku obrazów wystarczy obecność tagu <img> z parametrem id w drzewie DOM, aby warunek został spełniony. Obraz może wciąż być wczytywany.

Zanim skupisz się na blokowaniu renderowania, pamiętaj, że renderowanie przyrostowe to podstawowy aspekt internetu, więc jeśli chcesz je blokować, zachowaj rozwagę. Wpływ renderowania blokującego trzeba ocenić indywidualnie. Domyślnie unikaj używania narzędzia blocking=render, chyba że możesz aktywnie zmierzyć i zmierzyć wpływ tej zmiany na użytkowników, mierząc jej wpływ na podstawowe wskaźniki internetowe.


Wyświetlanie typów przejść podczas przejść w widoku różnych dokumentów

Przejścia widoku różnych dokumentów obsługują też typy przejść widoku, co pozwala dostosować animacje i przechwytywane elementy.

Gdy na przykład przechodzisz na następną lub poprzednią stronę w podziale na strony, możesz użyć różnych animacji w zależności od tego, czy przejdziesz na wyższą czy na niższą stronę w sekwencji.

Aby od razu ustawić te typy, dodaj je w regule @view-transition:

@view-transition {
  navigation: auto;
  types: slide, forwards;
}

Aby na bieżąco określać ich typy, używaj zdarzeń pageswap i pagereveal do modyfikowania wartości parametru e.viewTransition.types.

window.addEventListener("pagereveal", async (e) => {
  if (e.viewTransition) {
    const transitionType = determineTransitionType(navigation.activation.from, navigation.activation.entry);
    e.viewTransition.types.add(transitionType);
  }
});

Typy nie są automatycznie przenoszone z obiektu ViewTransition na starej stronie do obiektu ViewTransition na nowej stronie. Musisz określić, których typów użyjesz na co najmniej nowej stronie, by animacje działały zgodnie z oczekiwaniami.

Aby zareagować na te typy, użyj selektora pseudoklasy :active-view-transition-type() w taki sam sposób jak przy przejściach widoku tego samego dokumentu.

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

Typy mają zastosowanie tylko do przejść w widoku aktywnym, więc są automatycznie czyszczone po zakończeniu przejścia w widoku. Z tego powodu typy dobrze współpracują z takimi funkcjami jak BFCache.

Prezentacja

W tej prezentacji o podziale na strony zawartość strony przesuwa się do przodu lub do tyłu w zależności od otwieranego numeru strony.

.
Nagranie prezentacji podziału na strony (MPA). Wykorzystywane są różne przejścia w zależności od strony, na którą się otworzysz.

Typ przejścia jest określany w zdarzeniach pagereveal i pageswap na podstawie adresów URL prowadzących do i z powrotem.

const determineTransitionType = (fromNavigationEntry, toNavigationEntry) => {
  const currentURL = new URL(fromNavigationEntry.url);
  const destinationURL = new URL(toNavigationEntry.url);

  const currentPathname = currentURL.pathname;
  const destinationPathname = destinationURL.pathname;

  if (currentPathname === destinationPathname) {
    return "reload";
  } else {
    const currentPageIndex = extractPageIndexFromPath(currentPathname);
    const destinationPageIndex = extractPageIndexFromPath(destinationPathname);

    if (currentPageIndex > destinationPageIndex) {
      return 'backwards';
    }
    if (currentPageIndex < destinationPageIndex) {
      return 'forwards';
    }

    return 'unknown';
  }
};

Prześlij opinię

Opinie deweloperów są dla nas bardzo ważne. Aby udostępnić ten plik, zgłoś problem zespołowi roboczej ds. usług CSS na GitHubie, przesyłając sugestie i pytania. Dodaj do problemu prefiks [css-view-transitions]. Jeśli napotkasz błąd, zgłoś błąd w Chromium.