Переходы между представлениями документов для многостраничных приложений

Переход между двумя разными документами называется междокументным переходом . Обычно это происходит в многостраничных приложениях (MPA). Междокументные переходы поддерживаются в Chrome начиная с версии 126.

Browser Support

  • Chrome: 126.
  • Edge: 126.
  • Firefox: не поддерживается.
  • Сафари: 18.2.

Source

Переходы между окнами просмотра в разных документах основаны на тех же самых принципах и элементах, что и переходы между окнами просмотра в одном документе , и это сделано намеренно:

  1. Браузер делает снимки элементов, имеющих уникальное view-transition-name как на старой, так и на новой странице.
  2. DOM обновляется, в то время как рендеринг подавляется.
  3. И наконец, переходы между сценами обеспечиваются CSS-анимацией.

Отличие от переходов между страницами в пределах одного документа заключается в том, что при переходах между страницами в разных документах вам не нужно вызывать document.startViewTransition для запуска перехода. Вместо этого триггером для перехода между страницами в разных документах является навигация с одной страницы на другую с использованием того же источника, действие, которое обычно выполняется пользователем вашего веб-сайта при нажатии на ссылку.

Иными словами, не существует API, который можно было бы вызвать для запуска перехода между двумя документами. Однако необходимо выполнить два условия:

  • Оба документа должны находиться в одном и том же месте.
  • Для включения функции плавного перехода между страницами необходимо дать согласие обеим сторонам.

Оба эти условия подробно описаны далее в этом документе.


Переходы между представлениями в разных документах ограничены навигацией из одного источника.

Переходы между представлениями документов ограничены только навигацией с одним источником . Навигация считается навигацией с одним источником, если источник обеих участвующих страниц один и тот же.

Источник страницы определяется комбинацией используемой схемы, имени хоста и порта, как подробно описано на web.dev .

Пример URL-адреса с выделенными схемой, именем хоста и портом. В совокупности они образуют источник.
Пример URL-адреса с выделенными схемой, именем хоста и портом. В совокупности они образуют источник.

Например, при переходе с developer.chrome.com на developer.chrome.com/blog можно настроить переход между документами, поскольку это сайты с одним и тем же доменом. Однако при переходе с developer.chrome.com на www.chrome.com такой переход невозможен, поскольку это сайты с разными доменами и расположенные на одном сайте.


Переходы между окнами просмотра документов являются опциональными.

Для обеспечения плавного перехода между двумя документами обе участвующие страницы должны дать согласие на это. Это делается с помощью правила @view-transition в CSS.

В правиле @view-transition установите дескриптор navigation в auto , чтобы включить переходы между представлениями для навигации по нескольким документам с одним источником.

@view-transition {
  navigation: auto;
}

Установив параметр navigation descriptor в auto , вы разрешаете переходы между представлениями для следующих типов навигации (NavigationType ):

  • traverse
  • Если активация не была инициирована пользователем через механизмы пользовательского интерфейса браузера, необходимо push или replace .

К навигациям, исключенным из auto относятся, например, навигация с использованием адресной строки URL или щелчок по закладке, а также любые перезагрузки, инициированные пользователем или скриптом.

Если навигация занимает слишком много времени — более четырех секунд в случае Chrome — то переход между представлениями пропускается с ошибкой TimeoutError DOMException .

Демонстрация переходов между представлениями документов

Посмотрите следующий пример, в котором используются переходы между представлениями для создания демонстрации Stack Navigator . Здесь нет вызовов document.startViewTransition() , переходы между представлениями запускаются при переходе с одной страницы на другую.

Запись демонстрации Stack Navigator . Требуется Chrome версии 126 и выше.

Настройка переходов между окнами просмотра разных документов.

Для настройки переходов между окнами просмотра разных документов можно использовать некоторые функции веб-платформы.

Эти функции не являются частью самой спецификации API перехода между представлениями, но предназначены для использования совместно с ней.

События pageswap и pagereveal

Browser Support

  • Chrome: 124.
  • Edge: 124.
  • Firefox: не поддерживается.
  • Сафари: 18.2.

Source

Для настройки переходов между окнами просмотра в разных документах спецификация HTML включает два новых события, которые вы можете использовать: pageswap и pagereveal .

Эти два события срабатывают при каждой навигации между документами с одним источником, независимо от того, произойдет ли переход между представлениями или нет. Если переход между двумя страницами вот-вот произойдет, вы можете получить доступ к объекту ViewTransition , используя свойство viewTransition в этих событиях.

  • Событие pageswap срабатывает перед отрисовкой последнего кадра страницы. Вы можете использовать его для внесения последних изменений на исходящей странице непосредственно перед тем, как будут сделаны старые снимки.
  • Событие pagereveal срабатывает на странице после ее инициализации или повторной активации, но до первой возможности рендеринга. С его помощью вы можете настроить новую страницу до того, как будут сделаны новые снимки экрана.

Например, вы можете использовать эти события для быстрой установки или изменения значений view-transition-name , а также для передачи данных из одного документа в другой путем записи и чтения данных из sessionStorage , чтобы настроить переход между представлениями до его фактического выполнения.

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

При желании вы можете пропустить переходный период в обоих случаях.

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

Объекты ViewTransition в pageswap и pagereveal — это два разных объекта. Они также по-разному обрабатывают различные промисы :

  • pageswap : После скрытия документа старый объект ViewTransition пропускается. В этом случае viewTransition.ready отклоняется, а viewTransition.finished разрешается.
  • pagereveal : Промис updateCallBack уже выполнен. Вы можете использовать промисы viewTransition.ready и viewTransition.finished .

Browser Support

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

Source

В событиях pageswap и pagereveal вы также можете выполнять действия на основе URL-адресов старой и новой страниц.

Например, в MPA Stack Navigator тип используемой анимации зависит от пути навигации:

  • При переходе со страницы обзора на страницу с подробным описанием новый контент должен плавно выдвигаться справа налево.
  • При переходе со страницы с подробной информацией на страницу с обзором, старый контент должен плавно переходить слева направо.

Для этого вам потребуется информация о навигации, которая, в случае pageswap , вот-вот произойдет, или, в случае pagereveal только что произошла.

Для этого браузеры теперь могут предоставлять объекты NavigationActivation , которые содержат информацию о навигации из одного источника. Этот объект предоставляет информацию об используемом типе навигации, текущем и конечном пункте назначения, как это видно в методе navigation.entries() из API навигации .

На активированной странице доступ к этому объекту можно получить через navigation.activation . В событии pageswap доступ к нему можно получить через e.activation .

Ознакомьтесь с демонстрацией Profiles , в которой информация NavigationActivation из событий pageswap и pagereveal используется для установки значений view-transition-name для элементов, которые должны участвовать в переходе между представлениями.

Таким образом, вам не нужно заранее добавлять атрибут view-transition-name к каждому элементу списка. Вместо этого это происходит непосредственно во время просмотра с помощью JavaScript, только для тех элементов, которым это необходимо.

Запись демонстрации Profiles . Требуется Chrome версии 126 и выше.

Код выглядит следующим образом:

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

Код также выполняет очистку после себя, удаляя значения view-transition-name после выполнения перехода между представлениями. Таким образом, страница готова к последующим переходам и может обрабатывать обход истории.

Для этого воспользуйтесь этой вспомогательной функцией, которая временно устанавливает 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 = '';
  }
}

Предыдущий код теперь можно упростить следующим образом:

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

Дождитесь загрузки контента, блокируя отрисовку.

Browser Support

  • Chrome: 124.
  • Edge: 124.
  • Firefox: не поддерживается.
  • Safari: не поддерживается.

В некоторых случаях может потребоваться отложить первую отрисовку страницы до тех пор, пока определенный элемент не появится в новом DOM. Это позволит избежать мерцания и обеспечит стабильность состояния, к которому применяется анимация.

В разделе <head> определите один или несколько идентификаторов элементов, которые должны присутствовать до первого отображения страницы, используя следующий метатег.

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

Этот метатег означает, что элемент должен присутствовать в DOM, а не то, что контент должен быть загружен. Например, в случае с изображениями, одного лишь наличия тега <img> с указанным id в дереве DOM достаточно для того, чтобы условие было истинным. Само изображение при этом может продолжать загружаться.

Прежде чем полностью переходить на блокировку рендеринга, помните, что инкрементальный рендеринг является фундаментальным аспектом веб-разработки, поэтому будьте осторожны при выборе блокировки рендеринга. Влияние блокировки рендеринга необходимо оценивать в каждом конкретном случае. По умолчанию избегайте использования параметра blocking=render если вы не можете активно измерить и оценить его влияние на пользователей, измерив влияние на ваши основные веб-показатели .


Типы переходов при просмотре в разных представлениях документа

Переходы между представлениями в разных документах также поддерживают типы переходов между представлениями , позволяющие настраивать анимацию и определять, какие элементы будут захватываться.

Например, при переходе на следующую или предыдущую страницу в постраничной навигации может потребоваться использование различных анимаций в зависимости от того, переходите ли вы на страницу выше или ниже в последовательности.

Чтобы задать эти типы заранее, добавьте их в правило @view-transition :

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

Чтобы задать типы на лету, используйте события pageswap и pagereveal для изменения значения 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);
  }
});

Типы не переносятся автоматически из объекта ViewTransition на старой странице в объект ViewTransition на новой странице. Вам необходимо определить тип(ы), которые будут использоваться, по крайней мере, на новой странице, чтобы анимация работала должным образом.

Для обработки таких типов используйте псевдокласс селектор :active-view-transition-type() так же, как и при переходах между представлениями в пределах одного документа.

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

Поскольку типы применяются только к активному переходу между представлениями, они автоматически удаляются после завершения перехода. Благодаря этому типы хорошо работают с такими функциями, как BFCache .

Демо

В приведенном ниже примере постраничной навигации содержимое страницы сдвигается вперед или назад в зависимости от номера страницы, на которую вы переходите.

Запись демонстрации пагинации (MPA) . В ней используются разные переходы в зависимости от того, на какую страницу вы переходите.

Тип перехода, который следует использовать, определяется в событиях pagereveal и pageswap путем анализа URL-адресов «туда» и «откуда».

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

Обратная связь

Мы всегда рады отзывам разработчиков. Чтобы поделиться своими предложениями и вопросами, создайте заявку в рабочую группу CSS на GitHub . Добавьте к вашей заявке префикс [css-view-transitions] . Если вы обнаружите ошибку, создайте заявку в Chromium .