发布时间:2021 年 8 月 17 日;上次更新时间:2024 年 9 月 25 日
针对单个文档运行视图转换称为“同文档视图转换”。在使用 JavaScript 更新 DOM 的单页应用 (SPA) 中,通常会出现这种情况。从 Chrome 111 开始,Chrome 支持同一文档视图转换。
如需触发同一文档视图转换,请调用 document.startViewTransition
:
function handleClick(e) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow();
return;
}
// With a View Transition:
document.startViewTransition(() => updateTheDOMSomehow());
}
调用此方法时,浏览器会自动捕获声明了 view-transition-name
CSS 属性的所有元素的快照。
然后,它会执行传入的用于更新 DOM 的回调,之后会拍摄新状态的快照。
然后,这些快照会排列在伪元素树中,并使用强大的 CSS 动画功能进行动画处理。旧状态和新状态的两对快照会从其旧位置和大小平滑过渡到新位置,同时其内容会交叉淡出淡入。您可以根据需要使用 CSS 自定义动画。
默认过渡:淡入淡出
默认的视图转换是交叉淡出,因此非常适合用作 API 的简介:
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
// With a transition:
document.startViewTransition(() => updateTheDOMSomehow(data));
}
其中,updateTheDOMSomehow
用于将 DOM 更改为新状态。您可以随意选择合适的方式。例如,您可以添加或移除元素、更改类名称或更改样式。
这样一来,页面就会交叉淡出淡入:
好的,交叉淡化效果不是很出色。幸运的是,您可以自定义转场效果,但首先,您需要了解这种基本交叉淡化效果的运作方式。
这些转换的运作方式
我们来更新一下上一个代码示例。
document.startViewTransition(() => updateTheDOMSomehow(data));
调用 .startViewTransition()
时,该 API 会捕获页面的当前状态。这包括拍摄快照。
完成后,系统会调用传递给 .startViewTransition()
的回调。DOM 就是在该位置发生变化的。然后,API 会捕获页面的新状态。
捕获新状态后,该 API 会构建一个伪元素树,如下所示:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
::view-transition
位于页面上的所有其他内容之上,呈现为叠加层。如果您想为转场效果设置背景颜色,这会非常有用。
::view-transition-old(root)
是旧视图的屏幕截图,而 ::view-transition-new(root)
是新视图的实时表示形式。这两者都会渲染为 CSS“替换内容”(例如 <img>
)。
旧视图会以动画形式从 opacity: 1
到 opacity: 0
,而新视图会从 opacity: 0
以动画形式呈现到 opacity: 1
,从而创建淡入淡出。
所有动画都是使用 CSS 动画执行的,因此可以使用 CSS 进行自定义。
自定义转场效果
所有视图转换伪元素都可以使用 CSS 进行定位,并且由于动画是使用 CSS 定义的,因此您可以使用现有的 CSS 动画属性对其进行修改。例如:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 5s;
}
只需进行这项更改,淡出效果现在就非常缓慢了:
好的,这仍然不够理想。而以下代码会实现 Material Design 的共享轴转场:
@keyframes fade-in {
from { opacity: 0; }
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes slide-from-right {
from { transform: translateX(30px); }
}
@keyframes slide-to-left {
to { transform: translateX(-30px); }
}
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
结果如下:
对多个元素进行转换
在上一个演示中,整个页面都参与了共享轴转场。这对网页的大部分内容都适用,但对标题似乎不太合适,因为它会滑出,然后又滑回来。
为避免出现这种情况,您可以从网页的其余部分提取页眉,以便单独为其添加动画效果。具体方法是向元素分配 view-transition-name
。
.main-header {
view-transition-name: main-header;
}
view-transition-name
的值可以是您想要的任何值(none
除外,这表示没有转场名称)。它用于在转换期间唯一地标识元素。
结果如下:
现在,标题会保持在原位并进行交叉淡化。
该 CSS 声明导致伪元素树发生变化:
::view-transition
├─ ::view-transition-group(root)
│ └─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root)
│ └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
└─ ::view-transition-image-pair(main-header)
├─ ::view-transition-old(main-header)
└─ ::view-transition-new(main-header)
现在有两个转换组。一个用于标题,另一个用于其余内容。可以使用 CSS 单独定位这些元素,并采用不同的转场方式。不过,在本例中,main-header
保留了默认转场效果,即交叉淡入淡出。
好的,默认转场效果不仅仅是交叉淡出,::view-transition-group
也会进行转场:
- 位置和转换(使用
transform
) - 宽度
- 高度
到目前为止,这并不重要,因为标题在 DOM 更改的两侧具有相同的大小和位置。不过,您也可以提取标题中的文本:
.main-header-text {
view-transition-name: main-header-text;
width: fit-content;
}
使用 fit-content
可使元素的大小与文本相同,而不是延伸到剩余的宽度。如果不这样做,返回箭头会缩小标题文本元素的大小,而不是在两个页面中使用相同的大小。
现在,我们有三个部分可以玩:
::view-transition
├─ ::view-transition-group(root)
│ └─ …
├─ ::view-transition-group(main-header)
│ └─ …
└─ ::view-transition-group(main-header-text)
└─ …
不过,还是使用默认值吧:
现在,标题文本会以令人满意的滑动方式滑动,为返回按钮留出空间。
使用 view-transition-class
以相同的方式为多个伪元素添加动画效果
浏览器支持
假设您有一个包含一堆卡片的视图转换,但页面上还有一个标题。如需为标题以外的所有卡片添加动画效果,您必须编写一个定位到每个单独卡片的选择器。
h1 {
view-transition-name: title;
}
::view-transition-group(title) {
animation-timing-function: ease-in-out;
}
#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
…
#card20 { view-transition-name: card20; }
::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),
…
::view-transition-group(card20) {
animation-timing-function: var(--bounce);
}
有 20 个元素吗?也就是说,您需要编写 20 个选择器。要添加新元素吗?然后,您还需要扩展应用动画样式的选择器。扩展性不太理想。
view-transition-class
可在视图转换伪元素中使用,以应用相同的样式规则。
#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }
…
#card20 { view-transition-name: card20; }
#cards-wrapper > div {
view-transition-class: card;
}
html::view-transition-group(.card) {
animation-timing-function: var(--bounce);
}
以下卡片示例利用了上一个 CSS 代码段。所有卡片(包括新添加的卡片)都会使用一个选择器(html::view-transition-group(.card)
)应用相同的时间设置。
调试转换
由于视图转换是基于 CSS 动画构建的,因此 Chrome DevTools 中的 Animations 面板非常适合调试转换。
您可以使用动画面板暂停下一个动画,然后在动画中前后拖动。在此过程中,您可以在元素面板中找到转换伪元素。
转换元素不必是相同的 DOM 元素
到目前为止,我们已使用 view-transition-name
为标题和标题中的文本分别创建了转换元素。从概念上讲,DOM 更改前后这些元素是相同的,但您也可以创建不符合这种情况的转换。
例如,您可以为主要视频嵌入代码指定 view-transition-name
:
.full-embed {
view-transition-name: full-embed;
}
然后,在用户点击缩略图时,系统仅在转场期间为其提供相同的 view-transition-name
:
thumbnail.onclick = async () => {
thumbnail.style.viewTransitionName = 'full-embed';
document.startViewTransition(() => {
thumbnail.style.viewTransitionName = '';
updateTheDOMSomehow();
});
};
结果如下:
缩略图现在会转换为主图片。虽然它们在概念上(以及字面上)是不同的元素,但转场 API 会将它们视为同一元素,因为它们共享相同的 view-transition-name
。
此过渡的实际代码比上一个示例稍微复杂一点,因为它还会处理返回到缩略图页面的过渡。如需查看完整实现,请参阅源代码。
自定义进入和退出过渡
看看下面这个示例:
边栏是过渡的一部分:
.sidebar {
view-transition-name: sidebar;
}
但与上例中的标题不同,边栏不会显示在所有网页上。如果这两种状态都有边栏,则转换伪元素如下所示:
::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
└─ ::view-transition-image-pair(sidebar)
├─ ::view-transition-old(sidebar)
└─ ::view-transition-new(sidebar)
不过,如果边栏仅在新页面上显示,则不会显示 ::view-transition-old(sidebar)
伪元素。由于边栏没有“旧”图片,因此图片对中只有 ::view-transition-new(sidebar)
。同样,如果边栏仅在旧版页面上,则图片对中只会显示 ::view-transition-old(sidebar)
。
在前面的演示中,边栏的转换方式因其是进入、退出还是同时处于这两种状态而异。它会从右侧滑入并淡入,从右侧滑出并淡出,并且在处于这两种状态时保持原位。
如需创建特定的进入和退出转场效果,您可以使用 :only-child
伪类定位旧或新伪元素(当它是图片对中的唯一子元素时):
/* Entry transition */
::view-transition-new(sidebar):only-child {
animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
/* Exit transition */
::view-transition-old(sidebar):only-child {
animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}
在这种情况下,当边栏在两种状态下均显示时,没有特定的过渡,因为默认状态是完美的。
异步 DOM 更新和等待内容
传递给 .startViewTransition()
的回调可以返回一个 Promise,以允许异步 DOM 更新,并等待重要内容准备就绪。
document.startViewTransition(async () => {
await something;
await updateTheDOMSomehow();
await somethingElse;
});
只有在 promise 执行完毕后,转换才会开始。在此期间,网页会冻结,因此应尽可能减少此处的延迟时间。具体而言,应在调用 .startViewTransition()
之前(页面仍处于完全交互状态时)执行网络提取,而不是在 .startViewTransition()
回调中执行。
如果您决定等待图片或字体准备就绪,请务必使用较宽的超时设置:
const wait = ms => new Promise(r => setTimeout(r, ms));
document.startViewTransition(async () => {
updateTheDOMSomehow();
// Pause for up to 100ms for fonts to be ready:
await Promise.race([document.fonts.ready, wait(100)]);
});
但在某些情况下,最好完全避免延迟,并使用您已有的内容。
充分利用您现有的内容
如果缩略图转换为较大的图片,请执行以下操作:
默认的转场效果是交叉淡出,这意味着缩略图可能会与尚未加载的完整图片交叉淡出。
处理此问题的一种方法是,等待整个图片加载完毕,然后再开始过渡。理想情况下,应在调用 .startViewTransition()
之前执行此操作,以便页面保持可互动状态,并显示一个旋转图标以向用户表明内容正在加载。但在这种情况下,有一种更好的方法:
::view-transition-old(full-embed),
::view-transition-new(full-embed) {
/* Prevent the default animation,
so both views remain opacity:1 throughout the transition */
animation: none;
/* Use normal blending,
so the new view sits on top and obscures the old view */
mix-blend-mode: normal;
}
现在,缩略图不会淡出,而是位于完整图片下方。也就是说,如果新视图尚未加载,缩略图将在整个转换过程中显示。这意味着,过渡可以立即开始,完整图片可以在自己的时间加载。
如果新视图具有透明度,此方法将不起作用,但在本例中,我们知道它没有透明度,因此可以进行此优化。
处理宽高比的变化
幸运的是,到目前为止,所有转换都是针对宽高比相同的元素,但情况并不总是如此。如果缩略图的宽高比为 1:1,主图片的宽高比为 16:9,会怎么样?
在默认的转换中,组会从之前的大小动画化为之后的大小。旧视图和新视图的宽度均为组的 100%,高度为自动高度,这意味着无论组的大小如何,它们都会保持宽高比。
这是一个不错的默认值,但在本例中,这并不是我们想要的。因此:
::view-transition-old(full-embed),
::view-transition-new(full-embed) {
/* Prevent the default animation,
so both views remain opacity:1 throughout the transition */
animation: none;
/* Use normal blending,
so the new view sits on top and obscures the old view */
mix-blend-mode: normal;
/* Make the height the same as the group,
meaning the view size might not match its aspect-ratio. */
height: 100%;
/* Clip any overflow of the view */
overflow: clip;
}
/* The old view is the thumbnail */
::view-transition-old(full-embed) {
/* Maintain the aspect ratio of the view,
by shrinking it to fit within the bounds of the element */
object-fit: contain;
}
/* The new view is the full image */
::view-transition-new(full-embed) {
/* Maintain the aspect ratio of the view,
by growing it to cover the bounds of the element */
object-fit: cover;
}
这意味着,当宽度扩大时,缩略图会保持在元素的中心,但全图会从 1:1 转换为 16:9,从而“取消剪裁”。
如需了解详情,请参阅视图过渡:处理宽高比变化
使用媒体查询针对不同设备状态更改转换
您可能希望在移动设备和桌面设备上使用不同的转场效果,例如下例中,在移动设备上执行从侧边完全滑动,但在桌面设备上执行更细微的滑动:
这可以使用常规媒体查询来实现:
/* Transitions for mobile */
::view-transition-old(root) {
animation: 300ms ease-out both full-slide-to-left;
}
::view-transition-new(root) {
animation: 300ms ease-out both full-slide-from-right;
}
@media (min-width: 500px) {
/* Overrides for larger displays.
This is the shared axis transition from earlier in the article. */
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
}
根据匹配的媒体查询,您可能还需要更改要为哪些元素分配 view-transition-name
。
对“减少动画效果”偏好设置做出响应
用户可以通过操作系统指明他们更喜欢减少动画效果,并且该偏好设置会在 CSS 中公开。
您可以选择禁止为以下用户进行任何转换:
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
不过,首选“减少动作”并不意味着用户不需要任何动作。您可以选择更细致的动画,但仍要能表达元素之间的关系和数据流。
使用视图转换类型处理多种视图转换样式
浏览器支持
有时,从一个特定视图转换到另一个视图时,应采用专门定制的转换效果。例如,在分页序列中前往下一页或上一页时,您可能希望将内容滑动到不同的方向,具体取决于您是前往序列中的上一个页面还是下一个页面。
为此,您可以使用视图转换类型,以便为有效的视图转换分配一种或多种类型。例如,在分页序列中转换到更高页面时,请使用 forwards
类型;在转换到更低页面时,请使用 backwards
类型。这些类型仅在捕获或执行过渡时有效,并且每种类型都可以通过 CSS 进行自定义,以使用不同的动画。
如需在同一文档视图转换中使用类型,您需要将 types
传递给 startViewTransition
方法。为此,document.startViewTransition
还接受一个对象:update
是用于更新 DOM 的回调函数,types
是包含类型的数组。
const direction = determineBackwardsOrForwards();
const t = document.startViewTransition({
update: updateTheDOMSomehow,
types: ['slide', direction],
});
如需响应这些类型,请使用 :active-view-transition-type()
选择器。将要定位到的 type
传递给选择器。这样,您就可以将多个视图转换的样式彼此分开,而不必担心其中一个声明会干扰另一个声明。
由于类型仅在捕获或执行转换时应用,因此您可以使用选择器仅针对具有相应类型的视图转换,在元素上设置或取消设置 view-transition-name
。
/* 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 (using the default root snapshot) */
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;
}
}
在以下分页演示中,网页内容会根据您要导航到的页码向前或向后滑动。这些类型在点击时确定,然后传递到 document.startViewTransition
。
如需定位到任何 Active View 过渡(无论其类型如何),您都可以改用 :active-view-transition
伪类选择器。
html:active-view-transition {
…
}
使用视图转换根目录上的类名称处理多种视图转换样式
有时,从一种特定类型的视图转换到另一种视图时,应采用专门定制的转换效果。或者,“返回”导航应与“前进”导航不同。
在转场类型之前,处理这些情况的方法是在转场根目录上临时设置类名称。调用 document.startViewTransition
时,此转场根元素为 <html>
元素,可在 JavaScript 中使用 document.documentElement
访问:
if (isBackNavigation) {
document.documentElement.classList.add('back-transition');
}
const transition = document.startViewTransition(() =>
updateTheDOMSomehow(data)
);
try {
await transition.finished;
} finally {
document.documentElement.classList.remove('back-transition');
}
如需在转换完成后移除类,此示例使用 transition.finished
,这是一个在转换达到结束状态后解析的 promise。API 参考文档中介绍了此对象的其他属性。
现在,您可以在 CSS 中使用该类名称来更改转换效果:
/* 'Forward' transitions */
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
animation-name: fade-out, slide-to-right;
}
.back-transition::view-transition-new(root) {
animation-name: fade-in, slide-from-left;
}
与媒体查询一样,这些类的存在也可以用于更改哪些元素会获得 view-transition-name
。
运行转场效果,而不会冻结其他动画
请观看此视频转场位置演示:
你看到什么问题了吗?如果您没有看到,也别担心。下面是放慢速度的视频:
在转换期间,视频似乎会卡住,然后播放的视频版本会逐渐淡入。这是因为 ::view-transition-old(video)
是旧视图的屏幕截图,而 ::view-transition-new(video)
是新视图的实时图片。
您可以解决此问题,但首先要问自己是否值得解决。如果您在以正常速度播放转场效果时没有看到“问题”,那就没必要更改了。
如果您确实想要解决此问题,则不要显示 ::view-transition-old(video)
;请直接切换到 ::view-transition-new(video)
。您可以通过替换默认样式和动画来实现此目的:
::view-transition-old(video) {
/* Don't show the frozen old view */
display: none;
}
::view-transition-new(video) {
/* Don't fade the new view in */
animation: none;
}
这样就大功告成了!
现在,视频会在整个过渡期间播放。
与 Navigation API(及其他框架)集成
视图过渡的指定方式应能够与其他框架或库集成。例如,如果您的单页应用 (SPA) 使用的是路由器,您可以调整路由器的更新机制,以使用视图转换更新内容。
在此分页演示中提取的以下代码段中,Navigation API 的拦截处理脚本已调整为在支持视图转换时调用 document.startViewTransition
。
navigation.addEventListener("navigate", (e) => {
// Don't intercept if not needed
if (shouldNotIntercept(e)) return;
// Intercept the navigation
e.intercept({
handler: async () => {
// Fetch the new content
const newContent = await fetchNewContent(e.destination.url, {
signal: e.signal,
});
// The UA does not support View Transitions, or the UA
// already provided a Visual Transition by itself (e.g. swipe back).
// In either case, update the DOM directly
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
// Update the content using a View Transition
const t = document.startViewTransition(() => {
setContent(newContent);
});
}
});
});
当用户执行滑动手势进行导航时,部分(但非全部)浏览器会提供自己的转场效果。在这种情况下,您不应触发自己的视图转换,因为这会导致用户体验不佳或令人困惑。用户会看到两个连续运行:一个由浏览器提供,另一个由您提供。
因此,建议在浏览器提供自己的视觉转换时,阻止视图转换的开始。为此,请检查 NavigateEvent
实例的 hasUAVisualTransition
属性的值。当浏览器提供视觉转换时,该属性会设置为 true
。此 hasUIVisualTransition
属性也存在于 PopStateEvent
实例中。
在上面的代码段中,用于确定是否运行视图转换的检查会考虑此属性。如果不支持同文档视图转换,或者浏览器已经提供自己的转换,则会跳过视图转换。
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
在以下录制内容中,用户滑动以返回上一页。左侧的屏幕截图未包含对 hasUAVisualTransition
标志的检查。右侧的录制内容包含该检查,因此跳过了手动视图转换,因为浏览器提供了视觉转换。
使用 JavaScript 创建动画
到目前为止,所有转换均使用 CSS 定义,但有时 CSS 并不足以满足需求:
单靠 CSS 无法实现这种转变:
- 动画从点击位置开始。
- 动画结束时,圆形的半径会达到最远角。不过,希望未来 CSS 能够实现这一点。
幸运的是,您可以使用 Web 动画 API 创建转场效果!
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)
);
// With 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)',
}
);
});
}
此示例使用了 transition.ready
,这是一个在成功创建过渡伪元素后解析的 promise。API 参考文档中介绍了此对象的其他属性。
作为增强功能的转场效果
View Transition API 旨在“封装” DOM 更改并为其创建转场。不过,过渡应被视为一种增强功能,也就是说,如果 DOM 更改成功,但过渡失败,应用不应进入“错误”状态。理想情况下,转换不应失败,但如果失败,也不应破坏其余的用户体验。
为了将转场效果视为增强功能,请务必谨慎使用转场 promise,以免在转场失败时导致应用抛出异常。
async function switchView(data) { // Fallback for browsers that don't support this API: if (!document.startViewTransition) { await updateTheDOM(data); return; } const transition = document.startViewTransition(async () => { await updateTheDOM(data); }); await transition.ready; document.documentElement.animate( { clipPath: [`inset(50%)`, `inset(0)`], }, { duration: 500, easing: 'ease-in', pseudoElement: '::view-transition-new(root)', } ); }
此示例存在的问题是,如果转换无法达到 ready
状态,switchView()
将拒绝,但这并不意味着视图未能切换。DOM 可能已成功更新,但存在重复的 view-transition-name
,因此系统跳过了转换。
相反:
async function switchView(data) { // Fallback for browsers that don't support this API: if (!document.startViewTransition) { await updateTheDOM(data); return; } const transition = document.startViewTransition(async () => { await updateTheDOM(data); }); animateFromMiddle(transition); await transition.updateCallbackDone; } async function animateFromMiddle(transition) { try { await transition.ready; document.documentElement.animate( { clipPath: [`inset(50%)`, `inset(0)`], }, { duration: 500, easing: 'ease-in', pseudoElement: '::view-transition-new(root)', } ); } catch (err) { // You might want to log this error, but it shouldn't break the app } }
此示例使用 transition.updateCallbackDone
等待 DOM 更新,并在更新失败时进行拒绝。switchView
不再在转换失败时拒绝,而是在 DOM 更新完成时解析,如果更新失败,则拒绝。
如果您希望在新视图“稳定”后(即任何动画过渡已完成或跳至结束)解析 switchView
,请将 transition.updateCallbackDone
替换为 transition.finished
。
不是 polyfill,而是…
这不是一个易于实现的功能。不过,此辅助函数可以大大简化不支持视图转换的浏览器中的操作:
function transitionHelper({
skipTransition = false,
types = [],
update,
}) {
const unsupported = (error) => {
const updateCallbackDone = Promise.resolve(update()).then(() => {});
return {
ready: Promise.reject(Error(error)),
updateCallbackDone,
finished: updateCallbackDone,
skipTransition: () => {},
types,
};
}
if (skipTransition || !document.startViewTransition) {
return unsupported('View Transitions are not supported in this browser');
}
try {
const transition = document.startViewTransition({
update,
types,
});
return transition;
} catch (e) {
return unsupported('View Transitions with types are not supported in this browser');
}
}
其使用方式如下:
function spaNavigate(data) {
const types = isBackNavigation ? ['back-transition'] : [];
const transition = transitionHelper({
update() {
updateTheDOMSomehow(data);
},
types,
});
// …
}
在不支持视图过渡的浏览器中,系统仍会调用 updateDOM
,但不会提供动画过渡效果。
您还可以提供一些 classNames
以在转换期间添加到 <html>
,从而更轻松地根据导航栏类型更改转换。
如果您不想动画,也可以将 true
传递给 skipTransition
,即使在支持视图转换的浏览器中也是如此。如果您的网站设有用户偏好设置以停用转换,此设置会非常有用。
使用框架
如果您使用的库或框架会抽象化 DOM 更改,那么棘手之处在于如何知道 DOM 更改何时完成。下面是一组在各种框架中使用上述帮助程序的示例。
- React - 此处的关键是
flushSync
,它会同步应用一组状态更改。是的,系统会针对使用该 API 发出一条大警告,但 Dan Abramov 向我保证,在这种情况下使用该 API 是适当的。与 React 和异步代码一样,使用startViewTransition
返回的各种 promise 时,请注意确保代码以正确的状态运行。 - Vue.js - 这里的键是
nextTick
,它会在 DOM 更新后执行。 - Svelte - 与 Vue 非常相似,但等待下一次更改的方法是
tick
。 - Lit - 这里的关键是组件中的
this.updateComplete
承诺,该承诺会在 DOM 更新后执行。 - Angular - 这里的关键是
applicationRef.tick
,用于刷新待处理的 DOM 更改。从 Angular 版本 17 开始,您可以使用@angular/router
附带的withViewTransitions
。
API 参考文档
const viewTransition = document.startViewTransition(update)
启动新的
ViewTransition
。update
是一个函数,在捕获文档的当前状态后会被调用。然后,当
updateCallback
返回的 promise 执行时,转换会在下一帧开始。如果updateCallback
返回的 promise 被拒绝,则会放弃转换。const viewTransition = document.startViewTransition({ update, types })
使用指定类型启动新的
ViewTransition
捕获文档的当前状态后,系统会调用
update
。types
会在捕获或执行转换时为转换设置活动类型。它最初为空。如需了解详情,请参阅下文中的viewTransition.types
。
ViewTransition
的实例成员:
viewTransition.updateCallbackDone
一个 promise,在
updateCallback
返回的 promise 执行时执行,在该 promise 被拒绝时被拒绝。View Transition API 可封装 DOM 更改并创建转场。但是,有时您并不关心转换动画的成功或失败,您只想知道 DOM 更改是否发生以及何时发生。
updateCallbackDone
就是为此用例而设计的。viewTransition.ready
一旦创建了转场的伪元素且动画即将开始播放,系统就会执行该 promise。
如果无法开始转换,则会拒绝。这可能是因为配置错误(例如
view-transition-name
重复),或者updateCallback
返回遭拒的 promise。这对于使用 JavaScript 为过渡伪元素添加动画效果非常有用。
viewTransition.finished
在结束状态完全可见且可与用户互动后执行的 promise。
仅当
updateCallback
返回被拒绝的 promise 时,它才会拒绝,因为这表明未创建结束状态。否则,如果转换未能开始或在转换期间被跳过,系统仍会达到结束状态,因此
finished
会执行。viewTransition.types
一个类似于
Set
的对象,用于保存活跃视图过渡的类型。如需操纵这些条目,请使用其实例方法clear()
、add()
和delete()
。如需在 CSS 中响应特定类型,请对转场根使用
:active-view-transition-type(type)
伪类选择器。视图转换完成后,系统会自动清理类型。
viewTransition.skipTransition()
跳过过渡动画部分。
这不会跳过调用
updateCallback
,因为 DOM 更改与转换是分开的。
默认样式和过渡参考文档
::view-transition
- 用于填充视口并包含每个
::view-transition-group
的根伪元素。 ::view-transition-group
采用绝对定位。
在“之前”和“之后”状态之间转换
width
和height
。在“之前”和“之后”视口空间四边形之间进行转换
transform
。::view-transition-image-pair
绝对定位,填充组。
具有
isolation: isolate
,以限制mix-blend-mode
对旧版和新版视图的影响。::view-transition-new
和::view-transition-old
绝对定位到封装容器的左上角。
填充组宽度的 100%,但高度为自动,因此会保持其宽高比,而不是填充组。
具有
mix-blend-mode: plus-lighter
,以允许进行真正的交叉淡化。旧版视图从
opacity: 1
转换为opacity: 0
。新视图从opacity: 0
转换为opacity: 1
。
反馈
我们一如既往地衷心期待您的反馈。为此,请在 GitHub 上向 CSS 工作组提交问题,并附上建议和问题。请为问题添加 [css-view-transitions]
前缀。
如果您遇到 bug,请改为提交 Chromium bug。