介绍
View Transitions API 是一个较新的web API,其实截止目前也已经出现快一年时间了,根据MDN的介绍,该API属于实验性技术,Chrom和Edge的111版本以上已经能够使用了,具体浏览器兼容可以继续往下看。
它提供了一种机制,可以轻松地在不同DOM状态之间创建动画转换,同时还可以在一个步骤中更新DOM内容,简单来说就是他提供了一种类似css transition动画的效果,我们在开发SPA网站应用时,在页面路由切换或页面状态改变时,往往需要写大量的js和css来进行控制。而View Transitions API提供了一种更简单的方法来处理所需的DOM更改和过渡动画。
视图转换原理
当调用document.startViewTransition()时,API会对当前页面进行截图。接下来,调用传递给startViewTransition()的回调,这会导致DOM发生变化。当回调成功运行时ViewTransition.updateCallbackDone 将实现,允许您响应DOM更新。API捕获页面的新状态作为实时表示。
API构造具有以下结构的伪元素树:
ruby
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
::view-transition
是视图转换覆盖的根,它包含所有视图转换并位于所有其他页面内容的顶部。 当过渡动画即将运行时,ViewTransition.ready的回调,允许您通过运行自定义JavaScript动画而不是默认动画来响应。旧的页面视图从opacity
1动画到0,而新视图从opacity
0动画到1,这就是创建默认交叉淡入淡出的原因。当过渡动画到达其结束状态时,ViewTransition.finished将实现,允许您进行响应。
我们通过简单的实现可以实现路由切换时淡入淡出效果
ini
const transition = document.startViewTransition(() => {
router.push('/detail/' + index);
});
为了让渐变效果更明显一点将过渡时间增加一点
ruby
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 1s;
}
实现效果如下:
自定义过渡节点
通过上面的介绍,我们了解了该API是如何实现一个动态效果的,除了我们给整个页面root
增加过渡效果外,我们还能通过view-transition-name: detail
来单独对某一个dom节点进行过渡。
css
@keyframes large {
from {
transform: scale(0);
}
to {
transform: scale(1);
}
}
@keyframes small {
from {
transform: scale(1);
}
to {
transform: scale(0);
}
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 1s;
}
.detail-page::view-transition-old(detail) {
animation: none;
display: none;
}
.detail-page::view-transition-new(detail) {
animation: large 1s;
}
::view-transition-new(detail) {
animation: large 1s;
}
上面代码中,我们自定义了新的路由页面view-transition-name: detail
,然后通过::view-transition-new(detail)
和::view-transition-old(detail)
,页面打开的方式为从缩放打开。这样就实现了一个手机app打开的效果。
实现效果如下:
更多应用
实现路由切换时前进后退的效果
类似于上面的示例,我们在页面路由切换时,改变新老页面的过渡效果,就可以简单地实现原生app切换的效果。具体实现如下: home.vue
xml
<!--
* @Author: fcli
* @Date: 2023-11-10 11:10:36
* @LastEditors: fcli
* @LastEditTime: 2024-02-21 16:45:03
* @FilePath: /viewTransitions/src/pages/home/slide.vue
* @Description:
-->
<template>
<div class="home-content">
<div class="item" @click="(e) => linkToPage(e, i)" v-for="i in 20" :key="i"
:style="{ background: getRandomColor() }">
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
const router = useRouter();
const linkToPage = (e: any, index: any) => {
const transition = document.startViewTransition(() => {
router.push('/detail/' + index);
document.documentElement.classList.remove('router-backing');
});
}
const getRandomColor = () => {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
</script>
<style lang="less" scoped>
@keyframes slide-out {
from {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
@keyframes slide-in {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes slide-out-reverse {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
@keyframes slide-in-reverse {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
.router-backing::view-transition-old(root) {
animation: slide-out-reverse 0.5s;
}
.router-backing::view-transition-new(root) {
animation: slide-in-reverse 0.5s;
}
::view-transition-old(root) {
animation: slide-out .5s;
}
::view-transition-new(root) {
animation: slide-in .5s;
}
.home-content {
padding: 8px;
display: flex;
align-items: center;
justify-content: space-around;
flex-wrap: wrap;
.item {
width: 46%;
height: 120px;
margin-bottom: 8px;
border-radius: 8px;
}
}
</style>
detail.vue页面代码
xml
<!--
* @Author: fcli
* @Date: 2024-02-20 15:10:16
* @LastEditors: fcli
* @LastEditTime: 2024-02-22 13:48:07
* @FilePath: /viewTransitions/src/pages/detail.vue
* @Description:
-->
<template>
<div class="detail">
<div> 这是详情页面 </div>
<div class="back" @click="linkReturn">返回</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
const linkReturn = () => {
const transition = document.startViewTransition(() => {
document.documentElement.classList.add('router-backing');
router.push('/');
});
}
</script>
<style lang="less">
.detail {
text-align: center;
height: 100%;
background: rgb(235, 238, 247);
div {
line-height: 80px;
}
.back {
cursor: pointer;
color: blueviolet
}
}
</style>
这样就能实现如下效果了:
主题切换
在主题切换时我们也可以很方便地实现比较有意思的动态切换效果,实现代码如下:
ini
let isLight = true;
const changeColor = () => {
isLight = !isLight;
let event = document.querySelector('.change-theme')?.getBoundingClientRect();
const x = event?.x || 0;
const y = event?.y || 0;
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
const transition = document.startViewTransition(() => {
const root = document.documentElement;
root.classList.remove(isLight ? 'themeDark' : 'themeLight');
root.classList.add(isLight ? 'themeLight' : 'themeDark');
});
transition.ready.then(() => {
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`];
console.log(!isLight ? [...clipPath].reverse() : clipPath);
document.documentElement.animate(
{
clipPath: !isLight ? clipPath : clipPath.reverse()
},
{
duration: 500,
easing: 'ease-in',
pseudoElement: !isLight ? '::view-transition-new(root)' : '::view-transition-old(root)'
}
);
});
}
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
.themeDark::view-transition-old(root) {
z-index: 1;
}
.themeDark::view-transition-new(root) {
z-index: 999;
}
::view-transition-old(root) {
z-index: 999;
}
::view-transition-new(root) {
z-index: 1;
}
实现效果如下:
浏览器兼容性
根据MDN的介绍,chrome和edge的新版本都支持该属性,在实际应用时需要做好兼容性判断。
总结
以上是对View Transitions API 学习后的一些经验总结和实际应用,这个API还有很多可以使用的场景,能够很简单地实现spa页面的切换过渡效果。