Использование API перехода между представлениями
В этой статье объясняется теория работы API перехода между представлениями, как создавать переходы между представлениями и настраивать анимации перехода, а также как управлять активными переходами между представлениями. Это охватывает переходы между представлениями как для обновлений состояния DOM в одностраничном приложении (SPA), так и для навигации между документами в многостраничном приложении (MPA).
Процесс перехода между представлениями
Давайте пройдемся по процессу работы перехода между представлениями:
-
Триггером перехода между представлениями служит событие. Способ его запуска зависит от типа перехода:
- В случае переходов внутри одного документа (SPA) переход между представлениями запускается путем передачи функции, которая инициирует обновление DOM, как коллбэк методу
document.startViewTransition(). - В случае междокументных переходов (MPA) переход между представлениями запускается инициированием навигации к новому документу. И текущий, и целевой документы навигации должны находиться на одном источнике, и включить поддержку переходов между представлениями, добавив в свой CSS правило
@view-transitionсnavigationописателемauto.Примечание: Активный переход между представлениями имеет связанный экземпляр
ViewTransition(например, возвращаемыйstartViewTransition()в случае переходов внутри одного документа (SPA)). ОбъектViewTransitionсодержит несколько промисов, позволяющих вам выполнять код в ответ на разные этапы процесса перехода между представлениями. Более подробная информация приведена в разделе Управление переходами между представлениями с помощью JavaScript.
- В случае переходов внутри одного документа (SPA) переход между представлениями запускается путем передачи функции, которая инициирует обновление DOM, как коллбэк методу
-
В текущем (старом) представлении API захватывает снимки элементов, для которых объявлено свойство
view-transition-name. -
Происходит изменение представления:
-
В случае переходов внутри одного документа (SPA) вызывается коллбэк, переданный
startViewTransition(), что приводит к изменению DOM.После успешного выполнения коллбэка, промис
ViewTransition.updateCallbackDoneвыполняется, позволяя вам среагировать на обновление DOM. -
В случае междокументных переходов (MPA) происходит навигация между текущим и целевым документами.
-
-
API захватывает снимки нового представления в виде текущей информации.
На этом этапе переход между представлениями почти готов, и промис
ViewTransition.readyвыполняется, позволяя вам отреагировать, например, выполнив пользовательскую анимацию JavaScript вместо стандартной. -
Старые снимки представления анимируются "наружу", а новые снимки представления анимируются "внутрь". По умолчанию старые снимки представления анимируются от
opacity1 до 0, а новые снимки представления анимируются отopacity0 до 1, что создаёт эффект перехода. -
Когда анимации перехода достигают конечных состояний, выполняется промис
ViewTransition.finished, что позволяет вам отреагировать.
Примечание: Если состояние видимости страницы документа состояние видимости страницы равно hidden (например, если документ скрыт окном, браузер свёрнут или активен другой вкладкой браузера) во время вызова document.startViewTransition(), переход между представлениями пропускается полностью.
Дерево псевдоэлементов перехода между представлениями
Для обработки создания анимаций перехода "внешнего" и "внутреннего" API создает дерево псевдоэлементов со следующей структурой:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
Примечание: Поддерево ::view-transition-group создается для каждой захваченной view-transition-name.
В случае переходов внутри одного документа (SPA) дерево псевдоэлементов становится доступным в документе. В случае междокументных переходов (MPA) дерево псевдоэлементов становится доступным только в целевом документе.
Наиболее интересные части структуры дерева таковы:
-
::view-transitionявляется корнем наложения переходов между представлениями, который содержит все группы снимков переходов между представлениями и располагается поверх всего содержимого страницы. -
::view-transition-groupслужит контейнером для каждой группы снимков перехода между представлениями. Аргументrootопределяет стандартную группу снимков — анимация перехода между представлениями будет применена к снимку, у которогоview-transition-nameравноroot. По умолчанию это элемент:root, потому что стандартные стили браузера так определены::root { view-transition-name: root; }Однако следует учесть, что авторы страниц могут изменить это, сняв вышеуказанное ограничение и задав
view-transition-name: rootдля другого элемента. -
::view-transition-oldуказывает на статический снимок элемента старой страницы, а::view-transition-newуказывает на текущий снимок элемента новой страницы. Оба эти элемента отображаются как элементы замены, аналогично<img>или<video>, что означает, что их можно стилизовать с помощью таких удобных свойств, какobject-fitиobject-position.
Примечание: Можно назначать разные элементы DOM различным пользовательским анимациям переходов между представлениями, задав разное значение view-transition-name для каждого из них. В таких случаях создается ::view-transition-group для каждого из них. Пример см. в разделе Разные анимации для разных элементов.
Примечание: Как вы увидите позже, чтобы настроить анимации "внешнего" и "внутреннего" перехода, вам необходимо указать анимации для псевдоэлементов ::view-transition-old и ::view-transition-new соответственно.
Создание основного перехода между представлениями
В этом разделе показано, как создать базовый переход между представлениями как в случае SPA, так и MPA.
Основной переход между представлениями SPA
В качестве примера, SPA может включать функциональность для получения нового содержимого и обновления DOM в ответ на событие определенного типа, например, нажатие на ссылку навигации или обновление, отправленное сервером. В нашем примере демонстрации переходов между представлениями SPA мы упростили это до функции displayNewImage(), которая отображает новый полноразмерный образ на основе миниатюры, на которую был нажат клик. Мы упаковали это в функцию updateView(), которая вызывает API перехода между представлениями только в том случае, если браузер его поддерживает:
function updateView(event) {
// Handle the difference in whether the event is fired on the <a> or the <img>
const targetIdentifier = event.target.firstChild || event.target;
const displayNewImage = () => {
const mainSrc = `${targetIdentifier.src.split("_th.jpg")[0]}.jpg`;
galleryImg.src = mainSrc;
galleryCaption.textContent = targetIdentifier.alt;
};
// Fallback for browsers that don't support View Transitions:
if (!document.startViewTransition) {
displayNewImage();
return;
}
// With View Transitions:
const transition = document.startViewTransition(() => displayNewImage());
}
Этот код достаточно для обработки переходов между отображаемыми изображениями. В поддерживающих браузерах изменение от старых изображений к новым и подписей будет плавным переходом (стандартный переход между представлениями). Он по-прежнему будет работать в браузерах, которые не поддерживают это, но без красивой анимации.
Основной переход между представлениями MPA
При создании междокументного (MPA) перехода между представлениями процесс еще проще, чем в случае SPA. JavaScript не требуется, так как обновление представления запускается междокументной навигацией на одном источнике, а не изменением DOM, инициированным JavaScript. Для включения основного междокументного перехода между представлениями необходимо указать правило @view-transition в CSS как для текущего, так и для целевого документов, чтобы включить их, как показано ниже:
@view-transition {
navigation: auto;
}
Наш пример перехода между представлениями MPA демонстрирует это правило, а также показывает, как настроить анимации "внешнего" и "внутреннего" переходов между представлениями.
Примечание: В настоящее время междокументные переходы между представлениями могут создаваться только между документами одного источника, но это ограничение может быть снято в будущих реализациях.
Настройка анимаций
Псевдоэлементы переходов представлений имеют по умолчанию применённые CSS-анимации (подробное описание которых находится на их страницах справочника).
Большинство переходов внешнего вида используют анимацию плавного перехода с эффектом кросс-фейда, как упоминалось выше. Есть некоторые исключения:
-
heightиwidthпереходы имеют применённую анимацию плавного масштабирования. -
positionиtransformпереходы имеют применённую анимацию плавного перемещения.
Вы можете изменить значения по умолчанию для анимаций любым способом, используя обычный CSS — для анимации «от» используйте ::view-transition-old, а для анимации «к» — ::view-transition-new.
Например, чтобы изменить скорость обеих анимаций:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.5s;
}
Рекомендуется применять такие стили к ::view-transition-group() в случаях, когда вы хотите применить их к ::view-transition-old() и ::view-transition-new(). Из-за иерархии псевдоэлементов и стилей по умолчанию браузера стили будут унаследованы обоими. Например:
::view-transition-group(root) {
animation-duration: 0.5s;
}
Примечание: Это также хороший вариант для защиты вашего кода — ::view-transition-group() также анимируется, и у вас может получиться различная продолжительность для псевдоэлементов group/image-pair по сравнению с псевдоэлементами old и new.
В случае переходов между документами (MPA) псевдоэлементы должны быть включены только в целевой документ для работы перехода представлений. Если вы хотите использовать переход представлений в обоих направлениях, вам, конечно же, нужно включить их в оба.
Наша демонстрация переходов представлений MPA включает указанный CSS, но идёт дальше, определяя пользовательские анимации и применяя их к псевдоэлементам ::view-transition-old(root) и ::view-transition-new(root). В результате, переход по умолчанию с эффектом кросс-фейда заменяется переходом «свайп вверх» при переходе навигации:
/* Create a custom animation */
@keyframes move-out {
from {
transform: translateY(0%);
}
to {
transform: translateY(-100%);
}
}
@keyframes move-in {
from {
transform: translateY(100%);
}
to {
transform: translateY(0%);
}
}
/* Apply the custom animation to the old and new page states */
::view-transition-old(root) {
animation: 0.4s ease-in both move-out;
}
::view-transition-new(root) {
animation: 0.4s ease-in both move-in;
}
Разные анимации для разных элементов
По умолчанию все различные элементы, которые меняются во время обновления представления, используют одну и ту же анимацию. Если вы хотите, чтобы некоторые элементы анимировались по-другому, чем по умолчанию root анимация, вы можете разделить их с помощью свойства view-transition-name. Например, в нашей демонстрации переходов представлений SPA элементам <figcaption> задаётся view-transition-name значение figure-caption, чтобы отделить их от остальной части страницы в контексте переходов представлений:
figcaption {
view-transition-name: figure-caption;
}
После применения этого CSS, сгенерированное дерево псевдоэлементов будет выглядеть так:
::view-transition
├─ ::view-transition-group(root)
│ └─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root)
│ └─ ::view-transition-new(root)
└─ ::view-transition-group(figure-caption)
└─ ::view-transition-image-pair(figure-caption)
├─ ::view-transition-old(figure-caption)
└─ ::view-transition-new(figure-caption)
Существование второй группы псевдоэлементов позволяет применять отдельные стили перехода представления только к <figcaption>. Разные захват старого и нового представлений обрабатываются раздельно.
Примечание: Значение view-transition-name может быть любым, кроме none — значение none специально означает, что элемент не будет участвовать в переходе представления.
Значения view-transition-name также должны быть уникальными. Если два рендеренных элемента имеют одинаковое значение view-transition-name в одно и то же время, ViewTransition.ready отклонит и переход будет пропущен.
Следующий код применяет пользовательскую анимацию только к <figcaption>:
@keyframes grow-x {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
@keyframes shrink-x {
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
}
::view-transition-group(figure-caption) {
height: auto;
right: 0;
left: auto;
transform-origin: right center;
}
::view-transition-old(figure-caption) {
animation: 0.25s linear both shrink-x;
}
::view-transition-new(figure-caption) {
animation: 0.25s 0.25s linear both grow-x;
}
Здесь мы создали пользовательскую анимацию CSS и применили её к псевдоэлементам ::view-transition-old(figure-caption) и ::view-transition-new(figure-caption). Мы также добавили ряд других стилей для обоих, чтобы сохранить их в одном месте и предотвратить вмешательство стилей по умолчанию в наши пользовательские анимации.
Примечание: Вы можете использовать * в качестве идентификатора псевдоэлемента для определения всех псевдоэлементов снимка, независимо от их имени. Например:
::view-transition-group(*) {
animation-duration: 2s;
}
Использование стилей анимации по умолчанию
Обратите внимание, что мы также обнаружили другой вариант перехода, который проще и дал лучший результат, чем описанный выше. Наш окончательный <figcaption> переход представления выглядел так:
figcaption {
view-transition-name: figure-caption;
}
::view-transition-group(figure-caption) {
height: 100%;
}
Это работает, потому что по умолчанию ::view-transition-group переходы width и height между старым и новым представлениями с плавным масштабированием. Нам просто нужно было установить фиксированное значение height для обоих состояний, чтобы это сработало.
Примечание: Плавные переходы с помощью API переходов представлений содержит несколько других примеров настройки.
Управление переходами представлений с помощью JavaScript
Переход представления имеет связанный объект ViewTransition, который содержит несколько членов обещания, позволяющих выполнять JavaScript в ответ на различные стадии достижения перехода. Например, ViewTransition.ready выполняется, когда дерево псевдоэлементов создано, и анимация собирается начаться, в то время как ViewTransition.finished выполняется, когда анимация завершена, и новое представление страницы становится видимым и интерактивным для пользователя.
Доступ к ViewTransition можно получить следующим образом:
-
В случае переходов в пределах одного документа (SPA), метод
document.startViewTransition()возвращаетViewTransitionсвязанный с переходом. -
В случае переходов между документами (MPA):
- Событие
pageswapсрабатывает, когда документ собирается загружаться из-за навигации. Его объект события (PageSwapEvent) предоставляет доступ кViewTransitionчерез свойствоPageSwapEvent.viewTransition, а такжеNavigationActivationчерезPageSwapEvent.activation, содержащий тип навигации и текущие и целевые записи истории документа.Примечание: Если навигация имеет URL из другого источника в цепочке переадресации, свойство
activationвозвращаетnull. - Событие
pagerevealсрабатывает, когда документ впервые рендерится, либо при загрузке нового документа из сети, либо при активации документа (либо из кеша обратного/вперед (bfcache), либо из предварительной рендеризации (prerender)). Его объект события (PageRevealEvent) предоставляет доступ кViewTransitionчерез свойствоPageRevealEvent.viewTransition.
- Событие
Давайте рассмотрим пример кода, чтобы продемонстрировать, как можно использовать эти функции.
Анимация перехода по JavaScript (внутри одного документа — SPA)
Следующий JavaScript-код может быть использован для создания циклического перехода раскрытия представления, исходящего из позиции курсора пользователя при нажатии, с анимацией, предоставленной API веб-анимаций.
// Store the last click event
let lastClick;
addEventListener("click", (event) => (lastClick = event));
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
// Get the click position, or fallback to the middle of the screen
const x = lastClick?.clientX ?? innerWidth / 2;
const y = lastClick?.clientY ?? innerHeight / 2;
// Get the distance to the furthest corner
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
);
// Create a transition:
const transition = document.startViewTransition(() => {
updateTheDOMSomehow(data);
});
// Wait for the pseudo-elements to be created:
transition.ready.then(() => {
// Animate the root's new view
document.documentElement.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
],
},
{
duration: 500,
easing: "ease-in",
// Specify which pseudo-element to animate
pseudoElement: "::view-transition-new(root)",
},
);
});
}
Для этой анимации также требуется следующий CSS, чтобы отключить стандартную CSS-анимацию и остановить смешивание старого и нового состояний представления (новое состояние «перекрывает» старое, а не переходит):
::view-transition-image-pair(root) {
isolation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
display: block;
}
Анимация перехода по JavaScript (между документами — MPA)
Демонстрация членов команды DevRel Chrome предоставляет базовый набор страниц профилей команды и демонстрирует, как использовать события pageswap и pagereveal для настройки исходящих и входящих анимаций перехода представления между документами, основываясь на URL «откуда» и «куда».
Обработчик события pageswap выглядит следующим образом. Он устанавливает имена переходов представлений на элементах на исходящей странице, которые ведут на страницы профилей. При переходе с домашней страницы на страницу профиля пользовательские анимации предоставляются только для связанного элемента, на который кликают в каждом случае.
window.addEventListener("pageswap", async (e) => {
// Only run this if an active view transition exists
if (e.viewTransition) {
const currentUrl = e.activation.from?.url
? new URL(e.activation.from.url)
: null;
const targetUrl = new URL(e.activation.entry.url);
// Going from profile page to homepage
// ~> The big img and title are the ones!
if (isProfilePage(currentUrl) && isHomePage(targetUrl)) {
// Set view-transition-name values on the elements to animate
document.querySelector(`#detail main h1`).style.viewTransitionName =
"name";
document.querySelector(`#detail main img`).style.viewTransitionName =
"avatar";
// Remove view-transition-names after snapshots have been taken
// Stops naming conflicts resulting from the page state persisting in BFCache
await e.viewTransition.finished;
document.querySelector(`#detail main h1`).style.viewTransitionName =
"none";
document.querySelector(`#detail main img`).style.viewTransitionName =
"none";
}
// Going to profile page
// ~> The clicked items are the ones!
if (isProfilePage(targetUrl)) {
const profile = extractProfileNameFromUrl(targetUrl);
// Set view-transition-name values on the elements to animate
document.querySelector(`#${profile} span`).style.viewTransitionName =
"name";
document.querySelector(`#${profile} img`).style.viewTransitionName =
"avatar";
// Remove view-transition-names after snapshots have been taken
// Stops naming conflicts resulting from the page state persisting in BFCache
await e.viewTransition.finished;
document.querySelector(`#${profile} span`).style.viewTransitionName =
"none";
document.querySelector(`#${profile} img`).style.viewTransitionName =
"none";
}
}
});
Примечание: Мы удаляем значения view-transition-name после того, как снимки будут сделаны в каждом случае. Если мы оставим их, они сохранятся в состоянии страницы, сохранённом в bfcache при навигации. Если затем нажать кнопку «назад», обработчик события pagereveal страницы, к которой происходит навигация назад, попытается установить те же значения view-transition-name на разных элементах. Если у нескольких элементов установлены одинаковые значения view-transition-name, переход представления будет пропущен.
Обработчик события pagereveal выглядит следующим образом. Он работает аналогично обработчику события pageswap, хотя имейте в виду, что здесь мы настраиваем анимацию «к», для элементов страницы на новой странице.
window.addEventListener("pagereveal", async (e) => {
// If the "from" history entry does not exist, return
if (!navigation.activation.from) return;
// Only run this if an active view transition exists
if (e.viewTransition) {
const fromUrl = new URL(navigation.activation.from.url);
const currentUrl = new URL(navigation.activation.entry.url);
// Went from profile page to homepage
// ~> Set VT names on the relevant list item
if (isProfilePage(fromUrl) && isHomePage(currentUrl)) {
const profile = extractProfileNameFromUrl(fromUrl);
// Set view-transition-name values on the elements to animate
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";
}
// Went to profile page
// ~> Set VT names on the main title and image
if (isProfilePage(currentUrl)) {
// Set view-transition-name values on the elements to animate
document.querySelector(`#detail main h1`).style.viewTransitionName =
"name";
document.querySelector(`#detail main 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(`#detail main h1`).style.viewTransitionName =
"none";
document.querySelector(`#detail main img`).style.viewTransitionName =
"none";
}
}
});
Стабилизация состояния страницы для согласованных переходов между документами
Перед запуском перехода между документами желательно дождаться стабилизации состояния страницы, полагаясь на блокировку рендеринга, чтобы убедиться, что:
- Загружены и применены критические стили.
- Загружены и выполнены критические скрипты.
- HTML, видимый для начального просмотра страницы пользователем, был пропарсен, так что он отображается последовательно.
Стили по умолчанию блокируют рендеринг, а скрипты можно заблокировать с помощью атрибута blocking="render".
Чтобы убедиться, что ваш начальный HTML был пропарсен и всегда отображается последовательно до запуска анимации перехода, можно использовать <link rel="expect">. В этом элементе вы включаете следующие атрибуты:
-
rel="expect"для указания того, что вы хотите использовать этот элемент<link>для блокировки рендеринга части HTML на странице. -
href="#element-id"для указания ID элемента, который вы хотите заблокировать. -
blocking="render"для блокировки рендеринга указанного HTML.
Давайте рассмотрим пример HTML документа:
<!doctype html>
<html lang="en">
<head>
<!-- This will be render-blocking by default -->
<link rel="stylesheet" href="style.css" />
<!-- Marking critical scripts as render blocking will
ensure they're run before the view transition is activated -->
<script async href="layout.js" blocking="render"></script>
<!-- Use rel="expect" and blocking="render" to ensure the
#lead-content element is visible and fully parsed before
activating the transition -->
<link rel="expect" href="#lead-content" blocking="render" />
</head>
<body>
<h1>Page title</h1>
<nav>...</nav>
<div id="lead-content">
<section id="first-section">The first section</section>
<section>The second section</section>
</div>
</body>
</html>
Результат заключается в том, что рендеринг документа заблокирован до тех пор, пока не будет пропарсен основной контент <div>, гарантируя последовательный переход отображения.
Вы также можете указать атрибут media на элементах <link rel="expect">. Например, при загрузке страницы на устройстве с узким экраном вы, возможно, захотите заблокировать рендеринг меньшего количества контента, чем на устройстве с широким экраном. Это логично — на мобильном устройстве будет отображено меньше контента при первой загрузке страницы, чем на настольном компьютере.
Это можно реализовать с помощью следующего HTML:
<link rel="expect" href="#lead-content" blocking="render" media="screen and (min-width: 641px)" /> <link rel="expect" href="#first-section" blocking="render" media="screen and (max-width: 640px)" />
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using