在 Web App 上实现跟 Mobile App 一样的页面级切换动效,往往需要编写大量 CSS 动画和 JS 控制逻辑,即使投入大量精力来实现,最终的页面衔接动画还是会出现各种不符合预期的 BUG。
高实现成本意味着无法普及应用,所以 Web APP 在页面切换时普遍不会有 Mobile App 的丝滑体验。
我们可以引入类似 framer-motion 这样的外部动效库来降低实现成本,在 SPA 场景下基本可以满足大部分的动画效果,但在 MPA 场景下仍然无法做的完美的页面级切换动效。
那么 Web App 就不能低成本的提供跟 Mobile App 一样的跨页面丝滑动效吗?🥹
当然可以!View Transitions API 是一种新的帮助 Web 开发者低成本实现视图过渡的 Web API,本文会以“共享元素转场动画”为例,带大家一起感受这个 API 的效果。
效果演示
下面是从卡片页面切换到文章详情页面的效果,可以看到封面图、头像、标题以及整个卡片的边框,都是整体从一个页面直接移动到另一个页面,其中头像元素还有自己独特的回弹动画,整体非常丝滑灵动!🤩
View Transition 的过程
下图是一次 View Transition 的完整过程,我们不需要立刻搞清楚所有涉及到的 API,接下来会展开进行讲解,现在只需简单理解 View Transition 大概工作过程,即:
当我们触发 View Transition 后,浏览器会为所有需要执行过渡动画的元素生成快照,DOM 发生变化后,页面不会立刻进行渲染,浏览器会再次为所有需要执行过渡动画的元素生成快照,最终根据两次快照来执行过渡动画。

了解完 View Transition 的大致工作过程后,接下来我们看一下 API 在 MPA 场景下具体是怎么使用的。
如何在 MPA 中使用 API
假设我们有两个页面,分别对应 Page One 和 Page Two,在不使用 view transition 时的初始效果如下:
开启 @view-transition 规则
首先,我们需要给两个页面的 CSS 都定义 @view-transition 规则,用于开启 View Transition。这里的 navigation: auto 指示浏览器可以在合适的时机启用视图过渡。
@view-transition {
navigation: auto;
}
开启后,View Transition 就会在页面切换时生成默认的过渡动画,效果如下所示,即使后续什么代码也不加,页面间的切换也会有一个很丝滑的淡入淡出过渡。
为不同的元素定义不同的动画
接下来我们要为示例中的头像元素单独定义动画,使其有“共享元素过渡”的效果,在具体实现之前,我们先认识一下 View Transition 的伪元素树。
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
::view-transition是伪元素树的根节点,包含所有视图快照,位于页面最上层;::view-transition-group是一个快照组,参数指明具体是哪个快照组,这里的 root 表示默认的快照组,后续我们可以为头像元素单独定义快照组;::view-transition-image-pair是过渡前、后元素的容器,主要是提供一个隔离的环境;::view-transition-old是过渡前视图的静态快照::view-transition-new是过渡后视图的实时快照
现在我们要给头像元素定义单独的过渡动画,头像元素在两个页面的代码如下:
<!-- Page One -->
<img class="page1-image" src="https://i.pinimg.com/736x/38/68/98/38689825ece04f281e8c16529b1b7588.jpg" alt="image" />
<!-- Page Two -->
<img class="page2-image" src="https://i.pinimg.com/736x/38/68/98/38689825ece04f281e8c16529b1b7588.jpg" alt="image" />
单独为头像元素设置 view-transition-name 属性,值就是快照组的名称,这里我定义成 transition-image:
.page1-image {
view-transition-name: transition-image;
/* 其他样式 */
}
.page2-image {
view-transition-name: transition-image;
/* 其他样式 */
}
然后给这个快照组自定义不同的动画曲线:
::view-transition-group(transition-image) {
animation-duration: .45s;
animation-timing-function: cubic-bezier(0.42, 0, 0, 1.3);
}
对应到伪元素树上,就会出现单独的快照组:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
└─ ::view-transition-group(transition-image)
└─ ::view-transition-image-pair(transition-image)
├─ ::view-transition-old(transition-image)
└─ ::view-transition-new(transition-image)
最终效果如下,是不是很简单!
高级用法
使用 JS 来控制 View Transition
View Transitions API 允许我们基于 pageswap 和 pagereveal 这两个事件,更加精细化的自定义视图过渡,接下来我们仍然拿前文中的例子进行演示,不过要使用 JS 来实现一样的效果。
pageswap 事件
触发时机:页面因为导航跳转即将被卸载时
event 对象中可以拿到 viewTransition 实例,我们可以根据实例的状态
// 例如 Page One 即将被卸载时
window.addEventListener("pageswap", async (e) => {
if (!e.viewTransition) return;
// 这里可以做一些业务逻辑处理
// ... ...
// 为头像元素添加视图转换名称,跟前文中 CSS 的 view-transition-name 属性是相同的效果
document.querySelector(`.transition-image`).style.viewTransitionName = "transition-image";
// 等待视图过渡动画结束
await e.viewTransition.ready;
// 为头像元素清除视图转换名称
// 主要是为了防止由于页面状态在 BFCache 中持久化而导致的命名冲突
document.querySelector(`#transition-image`).style.viewTransitionName = "none";
});
pagereveal 事件
触发时机:页面首次渲染后
// 例如 Page Two 首先渲染后
window.addEventListener("pageswap", async (e) => {
if (!e.viewTransition) return;
// 这里可以做一些业务逻辑处理
// ... ...
// 为头像元素添加视图转换名称,跟前文中 CSS 的 view-transition-name 属性是相同的效果
document.querySelector(`.transition-image`).style.viewTransitionName = "transition-image";
// 等待视图过渡动画即将开始
await e.viewTransition.ready;
// 为头像元素清除视图转换名称
// 主要是为了防止由于页面状态在 BFCache 中持久化而导致的命名冲突
document.querySelector(`#transition-image`).style.viewTransitionName = "none";
// 这里可以做一些业务逻辑处理
// ... ...
});
以上逻辑也可以实现相应视图过渡效果,需要注意的是,需要保证后一个页面状态的稳定下来后,才能使用 JS 控制 VIew Transition。
稳定页面状态
设想一下,DOM 已经被解析并渲染过一次了, JS 才加载完成,那么 pagereveal 事件永远不会被触发,也就不会执行 View Transition,因此我们要保证 DOM 在第一次渲染之前,核心的 CSS 和 JS 都要被加载并完成解析。
- CSS 是默认阻塞渲染的,我们不需要做任何操作
- 关键的 JS 脚本要放到
<head>中,并添加blocking="render",保证能够阻塞 DOM 渲染 - 添加
<link rel="expect" blocking="render" />保证需要 View Transition 的目标元素在解析完成之前,阻塞 DOM 渲染
以 Page One 为例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Page One</title>
<!-- CSS,默认阻塞渲染 -->
<link rel="stylesheet" href="./style//common.css" />
<link rel="stylesheet" href="./style/page-one.css" />
<!-- JS,添加 blocking="render" 阻塞渲染 -->
<script src="./script/index.js" blocking="render"></script>
<!-- transition-image 元素解析完成前,阻塞渲染 -->
<link rel="expect" href="#transition-image" blocking="render" />
</head>
<body>
<div class="list">
<a href="./page-two.html">
<img id="transition-image" class="image" src="https://i.pinimg.com/736x/38/68/98/38689825ece04f281e8c16529b1b7588.jpg" alt="image" />
</a>
</div>
</body>
</html>
你可以在这里查看完整源码,并进行体验:https://codesandbox.io/p/sandbox/sharp-aj-3jsl88
兼容性
现代浏览器中,Chrome、Edge、Safari 均已支持,Firefox 还不支持。

总结
View Transitions API 是一个使用简单且能力强大的 Web API,在 MPA 场景下只需要几行代码就能实现原来无法实现或者实现成本极高的跨页面过渡动画,使得 Web 应用能够轻松为用户提供与 Native App 一致的交互体验。