? 转载请注明出处。
欢迎来到新玩具乐园,今天我们来上手把玩 View Transition API。
一、Why View Transition
Transition 指的是 UI 层面的状态过渡,恰当的过渡效果能带给用户舒适感,提升产品的品质。
一般来说,我们有三种方式可以实现过渡动效:
- CSS Transition
- CSS Animation
- Web Animation API
那么 View Transition API 的用武之地在哪?
我们先来看一个示例:
当点击列表页的影片封面切换到详情页时,影片封面图在切换过程中是逐渐放大进入详情页的。
假如让你来操刀,你会怎么实现?
经典思路也许是:专门设置一个 DOM 元素,用来模拟过渡状态中的封面图,配合 绝对定位、CSS 动画和 JavaScript 计算来实现。
确实行得通,但总感觉稍欠优雅,略显狼狈。
这其实暴露了传统实现方式的短板:应对过渡中间态比较吃力。
我们的确可以增加额外的 DOM 元素来模拟中间态,但冗余的环节让代码复杂度升高,交互过程更不可控。所谓节外多生枝,夜长易梦多。
在这样的成本面前,跨页面过渡动效更是不敢想。
而能让我们轻松应对中间态的,正是 View Transition API。
? View Transition API,曾用名 Shared Element Transition、Page Transition,目前是 W3C 标准的工作组草稿,目前仅在 Chrome/Edge 111+ 版本中支持。
二、认识 API
View Transition API 带来的新知识并不多:
- JavaScript 方法与属性
- startViewTransition()
- updateCallbackDone
- ready
- finished
- skipTransition()
- CSS 规则和伪元素
- view-transition-name
- ::view-transition
- ::view-transition-group()
- ::view-transition-image-pair()
- ::view-transition-old()
- ::view-transition-new()
下面我们来逐一领略。
startViewTransition
开启 View Transition 动效,只需要一行代码:
document.startViewTransition(updateCallback);
startViewTransition
方法接收一个回调函数作为参数,在回调函数 updateCallback
里,我们执行变更 DOM 状态的逻辑,比如切换页面路由、更新页面内容等。
来看一个最小示例:
左列直接新增列表项,而右侧的新增逻辑用 startViewTransition
包装。(完整例子见 view-transition-demos/01-basic.html)
三个状态
调用 startViewTransition
后,我们会得到一个实例对象:
const vt = document.startViewTransition(updateCallback);
实例对象具有三个属性,分别代表过渡过程中的三个状态节点:
updateCallbackDone
:回调函数执行完毕ready
:准备就绪,即将开始播放动效finished
:动效播放完毕
这三个属性值都是 Promise 对象,这就方便了我们精细掌控动效过程。
await vt.updateCallbackDone;
// Do whatever you want
await vt.ready;
// Do whatever you want
await vt.finished;
// Do whatever you want
skipTransition
实例对象还具有一个方法,可以跳过动效过程。
vt.skipTransition();
需要指出的是,调用 skipTransition
只会跳过动效,但 updateCallback
仍会正常执行。这正是 View Transition API 的设计理念:锦上添花,不喧宾夺主。
另外,在不同的状态节点调用 skipTransition
,效果也不同。这里卖个关子,热切邀请大家运行一下示例代码(view-transition-demos/02-api.html),看看到底有什么不同之处。
view-transition-name
API 在 CSS 规则中新增了一个属性 view-transition-name
,该属性可以应用于任何元素,属性值也可以是任何不重复的字符串名称。
.box {
view-transition-name: box;
}
对于设置了 view-transition-name
的元素,浏览器会将其识别为需要应用过渡动效的元素。
回顾一下本文的第一个最小示例(view-transition-demos/01-basic.html),在实现代码里,我们并未给任何元素设置 view-transition-name
,但仍然产生了过渡动效。这是因为在缺省情况下,浏览器会给根节点 —— html 节点 —— 隐式地设置为 view-transition-name: root;
。
伪元素树
当浏览器识别到了动效的目标元素,就会针对目标生成一系列伪元素,构建成树形结构,插入到 html 第一个子节点的位置上。
伪元素树的 z-index
层级是最高的,覆盖在所有其他元素之上。这确保了在过渡过程中,页面是不可交互的。
如果页面中有多个元素都设置了 view-transition-name
,那么在树根 ::view-transition
之下,就会出现多个 ::view-transition-group
,与 root group 平级。
三、原理 & 生命周期
View Transition API 的设计足够简洁,因此其实现原理也并不艰深。我们将聚焦于过渡动效从始至终的全过程,也就是着眼于其整个生命周期,来探究它的动效到底是如何实现的。
1.startViewTransition
一段生命周期的起始,源自于我们调用 startViewTransition
。
2. 抓拍旧状态快照
在 DOM 状态变化前,浏览器抓拍一张旧状态的快照。我们可以理解为,浏览器帮我们截了一张图。
3. 暂停渲染
此时,浏览器会暂停页面渲染,updateCallback
引发的页面变化,要等到动效执行时才会显现出来。
4. 执行 updateCallback
到这里,我们的回调函数会执行,更新 DOM 状态。当然从用户的视角,是看不见变化的。
5. updateCallbackDone
回调执行完毕,实例对象的 updateCallbackDone
状态达成。
6. 抓拍新状态快照
浏览器抓拍新 DOM 状态的快照,同样地,也可以理解为截一张图,但这张图是直接对照新状态绘制的,并不是页面显现的内容。
7. 构建伪元素树
利用刚刚生成的两张快照,浏览器创建一系列伪元素,插入到 html 节点中。旧、新快照,对应着上文提到的两个伪元素:::view-transition-old
、::view-transition-new
。
8. 恢复渲染
伪元素应有尽有,就可以恢复页面渲染、将伪元素显示在页面上了。
9. ready
万事俱备,Transition 的 ready
状态达成。
10. 播放动效
浏览器安排新旧快照两个“演员”登台,导演一场从旧状态到新状态的过渡效果。
11. 移除伪元素
动效演出落幕,“演员”退场,浏览器把伪元素移除。
12、 finished 状态达成
在 W3C 标准文档中,有一个步进式 Demo,可以帮我们更直观地了解生命周期的每个阶段。
四、最佳实践
由于 View Transition API 还是新鲜出炉的功能,所以在使用过程中,还是有一些需要注意之处。
1. 平稳降级
当前只有 Chrome/Edge 111+ 支持 View Transition API,所以我们需要兼顾浏览器差异。
if (!document.startViewTransition) {
updateCallback();
return;
}
document.startViewTransition(updateCallback);
2. 精简 updateCallback
在过渡过程中,由于页面不可交互,所以这一阶段的时长要可控。
// Bad ❌
document.startViewTransition(async () => {
await fetchLargeFile();
await changeDOM();
await doSomething();
});
在上面的代码示例中,updateCallback
要等到大文件加载完才能执行。结合上文提及的生命周期阶段可知,此时页面停止渲染,用户将会看到一个卡死的页面。
因此,我们要将有可能阻塞过渡过程的逻辑,移到 updateCallback
之外。
3. 防止重名
在页面中,如果有多个元素的 view-transition-name
的值相同,浏览器会自动跳过这些元素的过渡动效,因此要保证同一时刻下,DOM 中每个 view-transition-name
仅对应唯一的元素
4. 防止状态中断
view-transition-name
重名导致动效被跳过,因此 Transition 的 ready
状态也就永远无法达到。
这种情况下,我们预期在 ready
状态后执行的一些逻辑,就不能得以执行,比如:
.box {
view-transition-name: box;
}
.spot {
view-transition-name: box;
}
// Bad ❌
const vt = document.startViewTransition(updateCallback);
await vt.ready;
doSomething(); // Never run.
为了避免逻辑异常,我们需要用 try...catch
等手段捕获异常,保持逻辑通顺。
// Good ✅
const vt = document.startViewTransition(updateCallback);
try {
await vt.ready;
}
doSomething();
5. 仅限 SPA
在单页应用(SPA)中,我们可以用 View Transition API 去处理路由切换的过渡,但在跨页面应用(MPA)的路由切换中,由于新状态承载于新页面,也就是新的 document
对象,所以过渡效果无法跨过页面之间的阻隔。
不过,在 View Transition API 的最初设想中,作者们是有计划支持 MPA 的:
所以,让我们拭目以待吧。
总结
在了解和使用 View Transition API 的过程中,我能切实感受到,它在帮助提升开发者的开发体验,也能在更复杂的场景中提升用户体验,实在是一把简单却足够强大的利器。
但其 W3C 标准还处于草稿阶段,功能完善度、浏览器支持面还需要长足发展。
我想,View Transition API 的设计初衷,就是对体验提升的不懈追求。正是这种精神,让 Web 开发不断萌新芽、开新花,这片土地,总是生机勃勃,总是日新月异。
如果你想调试更多 Demo,请访问 github.com/Baddyo/view…