Web Api 之 View Transition API:打造文档跳转间丝滑的视图转换

"动画" 是前端开发中老生常谈的话题了;

无论是原生还是框架,实现动画效果的插件有很多种,比如说:

Animate.css:CSS动画库,可以很容易地通过类名来添加预定义的动画效果。

GSAP:非常强大且灵活的动画库,可以创建复杂和高性能的动画,支持时间线功能,可以精细控制动画序列。

jQuery.animate():jQuery的内置方法,可以通过编写简单的代码来创建动画效果,比CSS动画具有更好的兼容性,但性能上不如专门的动画库。

Velocity.js:快速的JavaScript动画引擎,与jQuery的.animate()方法有着类似的API,但提供了更高的性能。

and so on......


以上说了那么多实现动画的工具,那么这些工具与今天主角View Transition API有什么区别呢?接下来让我们来感受一下。

什么是View Transition API (vta) ?

下面我们来看几个例子:

这种动画在 Native 端我们看的比较多,在 Web 端可以实现,但略麻烦,尤其是遇到路由切换 View 渲染的都是两个完全不一样的组件(页面内 DOM 全变了) 的情况下。

View Transitions API提供了一种机制,可轻松创建不同网站视图之间的动画过渡。这包括在单页应用 (SPA) 中为 DOM 状态之间添加动画,以及在多页应用 (MPA) 中为文档之间的导航添加动画。

vta 设计上是为了处理视图(或路由)间的转换效果。

  • 方法与属性 startViewTransition() updateCallbackDone ready finished skipTransition()
  • css规则与伪元素 view-transition-name ::view-transition ::view-transition-group() ::view-transition-image-pair() ::view-transition-old() 接下来让我用一个简单的demo来深入了解下 View Transition API

Background Transition

我们先来做一个点击按钮背景切换的效果:

Html:

html 复制代码
<button id="btn">切换</button>

Css:

css 复制代码
 :root {
      --bg-color: #fff;
      background-color: var(--bg-color);
    }

    :root.dark {
      --bg-color: #000;
    }

Js:

javascript 复制代码
btn.addEventListener('click',(e)=>{
    const transition = document.startViewTransition(()=>{     
    document.documentElement.classList.toggle('dark')
  )}
)}

开启动画 document.startViewTransition(updateCallback) ,调用startViewTransition后,我们会得到一个ViewTransition的实例对象 transition; 实例属性: 分别代表过渡过程的三个状态节点,各自均返回一个Promise对象 · updateCallbackDone:回调函数执行完毕 · ready:准备就绪,即将开始播放动效 · finished:动效播放完毕 实例方法: skipTransition() : 跳过视图过渡的动画部分,但不会跳过运行document.startViewTransition()的回调,该回调会更新 DOM;

可以看到,背景颜色的改变有了淡入淡出的效果。

视图过渡过程

让我们来看看这是如何工作的:

  1. 当调用 document.startViewTransition() 时,API 会截取一「帧」。

  2. 执行传入 startViewTransition(callback) 的回调函数,并等待界面响应更新。

  3. API 会捕获页面的新状态并实时展示,更新后,再截取一「帧」。

  4. api 构造了一个伪元素树

html 复制代码
::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)
// 只用去控制这个伪元素树,即可实现过渡动画效果。 

此时过渡动画即将运行,ViewTransition.ready Promise兑现,你可以响应它进行一些操作,例如运行自定义的 JavaScript 动画,而不是默认的动画

  1. 旧页面视图的opacity从 1 过渡到 0,而新视图从 0 过渡到 1,这就是默认的交叉淡入淡出效果。

  2. 当过渡动画结束时,ViewTransition.finished Promise 兑现,你可以响应它进行一些操作。

自定义动画

需要注意的是 vta 的默认动画是:cross-fade ,那么如何取消默认动画呢?

css 复制代码
::view-transition-old(root),
::view-transition-new(root) {
    animation: none; //取消默认动画
}

还是用以上背景切换的demo为例,我们来完成一个背景以当前点击的位置开始的圆形过渡动画

javascript 复制代码
 btn.addEventListener('click', (e) => {
    const transition = document.startViewTransition(() => {
      document.documentElement.classList.toggle('dark')
    })
    const x = e.clientX
    const y = e.clientY
    // 从点击点到窗口最远边缘的距离,这个距离即为圆的半径,用于确定一个圆形裁剪路径
    const radius = Math.sqrt(Math.max(x, (window.innerWidth - x)) ** 2 + Math.max(y, (window.innerHeight - y)) ** 2)
    
    // transition.ready:一个在伪元素树创建且过渡动画即将开始时兑现的 Promise
    transition.ready.then(() => {
      // 实现过渡的过程 circle
      document.documentElement.animate(
        {
          clipPath: [
            `circle(0 at ${x}px ${y}px)`,
            `circle(${radius}px at ${x}px ${y}px)`,
          ]
        },
        {
          duration: 300,
          pseudoElement: '::view-transition-new(root)',
        }
      )
    })
  })

效果如下:

为不同元素应用不同动画

上面的例子中,我们总是在思考vta的工作流程以及自定义动画,但是我们也发现了,整个视图(:root)都在参与动画,但是在我们的平时的工作中这样是不合理的,我们来看看如何给不同的元素应用不同的动画

可以给按钮添加一个父容器 .container

css 复制代码
    .container {
      view-transition-name: container;
    }

    ::view-transition-old(container),
    ::view-transition-new(container) {
      animation: none;
    }

此时的伪元素树则会变成

css 复制代码
::view-transition
├─ ::view-transition-group(root)   // 默认图层
│ └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(container)。// container图层
  └─ ::view-transition-image-pair(container)
      ├─ ::view-transition-old(container)
      └─ ::view-transition-new(container)

跨文档(路由)的视图转换(MPA)

通过刚才的案例,我们大致了解了View Transition API 的应用与工作流程; 接下来我们来看下今天的重点,「 跨文档(路由)的视图转换 」,如何利用 View Transition API 在web页面之间实现 Native般丝滑的过渡!

先看demo:View-Transition

涉及的知识点

View Transition API

@view-transition

在跨文档导航的情况下,CSS at -rule用于选择当前文档和目标文档进行视图转换。@view-transition为了实现跨文档视图转换,导航的当前文档和目标文档也需要位于同一来源。

less 复制代码
@view-transition {
  navigation: auto;
}
// auto:文档在参与导航时将经历视图转换,前提是导航是同源的
// none:该文档将不会经历视图转换。

HTML DOM API

PageSwapEvent: 当您跨文档导航时,如果上一个文档即将卸载,则会触发此pageswap事件 PageRevealEvent:在跨文档导航期间,如果导航触发了视图转换,它允许您操作从正在导航到的文档的相关视图转换(提供对相关对象的访问) 实例属性: viewTransition:包含一个ViewTransition代表跨文档导航的活动视图转换的对象

实战分析

首先是css部分,css比较随意,具体布局根据大家心情而定,但是最重要的有一点

css 复制代码
// 开启文档视图转换  
@view-transition {
    navigation: auto;
  }

首页 index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>index</title>
      <link rel="stylesheet" href="../shared/styles.css">
      <script src="../mpa/scripts.js"></script>
</head>
<body id="overview">
      <main>
            <ul class="profiles">
		<li id="wukong">
                   <a href="../mpa/wukong.html">
                      <img src="../shared/img/wukong.png" alt=""> 
                      <span>悟空</span>
                   </a>
                </li>
		<li id="lufei">
                   <a href="../mpa/lufei.html">
                      <img src="../shared/img/lufei.png" alt=""> 
                      <span>路飞</span>
                   </a>
                </li>	
	    </ul>
	</main>
</body>
</html>

子页 以 wukong.html为主

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>悟空</title>
      <link rel="stylesheet" href="../shared/styles.css">
      <script src="../mpa/scripts.js"></script>
</head>
<body id="detail">
     <main>
	<h1>悟空</h1>
	<img src="../shared/img/wukong.png" alt="">
	<p>Hello</p>
	<a href="../mpa/" class="back">&larr; Back!</a>
      </main>
</body>
</html>

下面是JS部分

思路其实非常简单

稍微观察下就能发现,无论是首页->详情页,还是详情页->首页 发生动画的永远都是头像(avatar)、名称(name), 当然也可以有其他,这个具体哪些需要动画全凭自己喜好;

比如,首页->详情页 当我们将要从首页离开的时候,对首页的avatar以及name等Dom设置viewTransitionName; 当我们即将进入详情页的时候,对详情页的avatar以及name等Dom设置与首页对应元素相同的viewTransitionName,对两个页面的元素进行连接,这样是不是就会有一个自然的过渡效果

开始之前我们先来做一些准备工作

javascript 复制代码
// 定义个人资料页面的基础路径,用于页面URL匹配
const basePath = "/view_transition/profiles/mpa";

// 使用输入的URL与主页面的URL模式进行匹配===>确认是否为主页
const homePagePattern = new URLPattern(`${basePath}(/)*`, window.origin);
const isHomePage = (url) => {
    return homePagePattern.exec(url);
};

// 使用输入的URL与详情页面的URL模式进行匹配===>确认是否为详情页
const profilePagePattern = new URLPattern(
    `${basePath}/:profile`,
    window.origin
);
const isProfilePage = (url) => {
    return profilePagePattern.exec(url);
};

// 从详情页URL中提取个人资料名字
const extractProfileNameFromUrl = (url) => {
    const match = profilePagePattern.exec(url);
    return match?.pathname.groups.profile;
};

到这里好像还少了些东西

就像刚才说的,想让两个页面出现元素过渡效果,首先需要元素之间有一个相同的viewTransitionName来建立连接,方便api去获取新旧页面的屏幕截图,但是为了解决资源冲突的问题,我们需要用完viewTransitionName后及时清除

开搞开搞!

我们先写一个用来管理viewTransitionName的函数

javascript 复制代码
const setTemporaryViewTransitionNames = async (entries, vtPromise) => {
  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = name;
  }
  await vtPromise;
  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = "";
  }
};

接下来使用 HTML DOM API 中的PageSwapEvent、PageRevealEvent

PageSwapEvent

当跨文档导航时,如果上一个文档即将卸载,则会触发此pageswap事件 在跨文档导航期间,如果导航触发了视图转换,则事件对象允许从正在导航的文档PageSwapEvent操纵相关视图转换(提供对相关对象的访问)。它还提供对有关导航类型以及当前文档和目标文档的信息的访问。ViewTransition

注意:对于pageswap事件来说,viewTransitionName的清除没有那么重要,因为页面即将离开,返回时必重新渲染,所以是否清除viewTransitionName无所谓了

javascript 复制代码
window.addEventListener("pageswap", async (e) => {
  if (e.viewTransition) {
    const currentUrl = e.activation.from?.url ? new URL(e.activation.from.url) : null;
    const targetUrl = new URL(e.activation.entry.url);

    //仅转换到相同的basePath,不满足则跳过动画
    if (!targetUrl.pathname.startsWith(basePath)) {
      e.viewTransition.skipTransition();
    }

    //从主页转到详情页
    if (isProfilePage(targetUrl)) {
      const profile = extractProfileNameFromUrl(targetUrl).split(".")[0];
      setTemporaryViewTransitionNames(
        [
          [document.querySelector(`#${profile} span`), "name"],
          [document.querySelector(`#${profile} img`), "avatar"],
        ],
        e.viewTransition.ready
      );
    }

    //从详情页面转到主页
    if (isProfilePage(currentUrl) && isHomePage(targetUrl)) {
      setTemporaryViewTransitionNames(
        [
          [document.querySelector(`#detail main h1`), "name"],
          [document.querySelector(`#detail main img`), "avatar"],
        ],
        e.viewTransition.ready
      );
    }
  }
});

PageRevealEvent

首次呈现文档时会触发此pagereveal事件,无论是从网络加载新文档还是激活文档

注意:对于当前案例来说pagereveal事件必须要清除viewTransitionName,因为文档呈现与文档离开的操作会在同一页面发生,如果viewTransitionName存在冲突则会影响屏幕对当前页面状态的截图,导致冲突;

另外,可能会有同学思考,既然pagereveal需要清除viewTransitionName,那么应该在ready还是在finished后完成?其实都可以,因为无论ready还是finished,当添加viewTransitionName的同时,api已经获得了新、旧页面的截图,无论ready还是finished都不会影响过渡动画的进行。

javascript 复制代码
window.addEventListener("pagereveal", async (e) => {
  if (!navigation.activation.from) return;
  if (e.viewTransition) {
    const fromUrl = new URL(navigation.activation.from.url);
    const currentUrl = new URL(navigation.activation.entry.url);

    //仅转换到相同的basePath
    //>>跳过!
    if (!fromUrl.pathname.startsWith(basePath)) {
      e.viewTransition.skipTransition();
    }

    //从个人资料页面转到主页
    if (isProfilePage(fromUrl) && isHomePage(currentUrl)) {
      const profile = extractProfileNameFromUrl(fromUrl).split(".")[0];
      setTemporaryViewTransitionNames(
        [
          [document.querySelector(`#${profile} span`), "name"],
          [document.querySelector(`#${profile} img`), "avatar"],
        ],
        e.viewTransition.finished
      );
    }

    //转到个人资料页
    if (isProfilePage(currentUrl)) {
      setTemporaryViewTransitionNames(
        [
          [document.querySelector(`#detail main h1`), "name"],
          [document.querySelector(`#detail main img`), "avatar"],
        ],
        e.viewTransition.finished
      );
    }
  }
});

到这里 一个完整的 MPA下跨文档之间的过渡动画 就全部完成了! 完整的demo:github.com/iamlvvvvvv/...

兼容性

View Transitions API

PageSwapEvent、PageRevealEvent

@view-transition

参考/阅读更多

View Transition API - Web API | MDN

官方教程 - 更详细的解释、更多有趣的demo

寸志大佬 - 用 View Transition API 在 Web 做出 Native 般丝滑的动画

相关推荐
abc80021170341 小时前
前端Bug 修复手册
前端·bug
Best_Liu~1 小时前
el-table实现固定列,及解决固定列导致部分滚动条无法拖动的问题
前端·javascript·vue.js
_斯洛伐克2 小时前
下降npm版本
前端·vue.js
苏十八3 小时前
前端进阶:Vue.js
前端·javascript·vue.js·前端框架·npm·node.js·ecmascript
st紫月4 小时前
用MySQL+node+vue做一个学生信息管理系统(四):制作增加、删除、修改的组件和对应的路由
前端·vue.js·mysql
乐容4 小时前
vue3使用pinia中的actions,需要调用接口的话
前端·javascript·vue.js
似水明俊德5 小时前
ASP.NET Core Blazor 5:Blazor表单和数据
java·前端·javascript·html·asp.net
至天6 小时前
UniApp 中 Web/H5 正确使用反向代理解决跨域问题
前端·uni-app·vue3·vue2·vite·反向代理
与墨学长6 小时前
Rust破界:前端革新与Vite重构的深度透视(中)
开发语言·前端·rust·前端框架·wasm
H-J-L6 小时前
Web基础与HTTP协议
前端·http·php