创建 Vue.js 过渡和动画

Avatar of Nicolas Udy
Nicolas Udy

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 的免费信用!

我的最后两个项目猛烈地将我带入了 JAMstack。单页应用程序、无头内容管理、静态生成……应有尽有。更重要的是,它们给了我学习 Vue.js 的机会。不只是“构建一个待办事项应用程序”的 Vue.js,我还要交付真实的、可用于生产的 Vue 应用程序。

Snipcart 背后的机构 (Spektrum) 希望开始将解耦的 JavaScript 框架用于中小型网站。然而,在将其用于客户项目之前,他们选择先在自己身上进行实验。在我的一些同事使用 React 遭遇了不成功的经历之后,我获得了使用 Vue 构建几个应用程序原型的许可。这些原型最终演变成 Spektrum 的完整 Vue 应用程序,连接到无头 CMS。首先,我花了一些时间弄清楚如何适当地对我们的数据进行建模和渲染。然后,我一头扎进了 Vue 的转换,为我们的两个项目添加了急需的修饰层。

我已经准备好了 CodePen 上的实时演示GitHub 仓库,与本文配合使用。

这篇文章深入探讨了 Vue.js 及其过渡系统提供的工具。假设您已经熟悉 Vue.js 的基础知识 和 CSS 过渡。为了简洁和清晰,我们不会深入探讨演示中使用的“逻辑”。

处理 Vue.js 过渡和动画

动画和过渡可以为您的网站增添活力,并吸引用户探索。动画和过渡是 UX 和 UI 设计中不可或缺的一部分。但是,它们很容易出错。在处理列表等复杂情况下,如果依赖原生 JavaScript 和 CSS,就几乎不可能进行推理。每当我问后端开发人员为什么他们如此强烈地不喜欢前端时,他们的回答通常是“……动画”。

即使对于那些因渴望创建复杂的微交互和流畅的页面过渡而被吸引到这个领域的人来说,这也不是一件容易的事。我们经常需要出于性能原因依赖 CSS,即使是在主要使用 JavaScript 的环境中,这种环境之间的断裂也很难管理。

这就是 Vue.js 等框架发挥作用的地方,它们消除了过渡中的猜测和笨拙的 setTimeout 函数链。

过渡和动画之间的区别

过渡动画这两个术语经常被互换使用,但实际上是不同的东西。

  • 过渡是指元素在单个步骤中完成样式属性变化的过程。它们通常完全通过 CSS 处理。
  • 动画更复杂。它们通常是多步骤的,有时会连续运行。动画通常会调用 JavaScript 来接管 CSS 逻辑不足的地方。

这可能会令人困惑,因为添加类可能是触发过渡动画的原因。但是,在进入 Vue 的世界时,这是一个重要的区别,因为它们都有非常不同的方法和工具箱。

以下是在 Spektrum 网站上使用过渡的示例

使用过渡

在页面上实现过渡效果的最简单方法是通过 Vue 的 <transition> 组件。它使事情变得如此简单,几乎感觉像作弊。Vue 会检测是否正在使用任何 CSS 动画或过渡,并会自动在过渡内容上切换类,从而实现完美定时的过渡系统和完全控制。

第一步是确定我们的范围。例如,通过设置组件的 name 属性,我们告诉 Vue 在过渡类之前添加前缀 modal。然后,要触发过渡,您只需使用 v-ifv-show 属性切换内容的可见性。Vue 会相应地添加/删除类。

过渡有两个“方向”:enter(对于从隐藏到可见的元素)和 leave(对于从可见到隐藏的元素)。然后 Vue 提供了 3 个“钩子”,代表过渡的不同时间段

  • .modal-enter-active / .modal-leave-active: 这些将在整个过渡过程中存在,应该用于应用您的 CSS 过渡声明。您还可以声明需要从开始到结束应用的样式。
  • .modal-enter / .modal-leave: 使用这些类来定义元素在过渡开始之前的样式。
  • .modal-enter-to / .modal-leave-to: 您可能已经猜到,这些决定了您希望过渡到的样式,即“完成”状态。

为了可视化整个过程,请查看 Vue 文档中的此图表

这如何转化为代码?假设我们只是想淡入淡出,将这些部分放在一起会是这样的

<button class="modal__open" @click="modal = true">Help</button>

<transition name="modal">
  <section v-if="modal" class="modal">
    <button class="modal__close" @click="modal = false">&times;</button>
  </section>
</transition>
.modal-enter-active,
.modal-leave-active { transition: opacity 350ms }

.modal-enter,
.modal-leave-to { opacity: 0 }

.modal-leave,
.modal-enter-to { opacity: 1 }

这可能是您遇到的最基本的实现。请记住,此过渡系统还可以处理内容更改。例如,您可以对 Vue 的动态 <component> 更改做出反应。

<transition name="slide">
  <component :is="selectedView" :key="selectedView"/>
</transition>
.slide-enter { transform: translateX(100%) }
.slide-enter-to { transform: translateX(0) }
.slide-enter-active { position: absolute }

.slide-leave { transform: translateX(0) }
.slide-leave-to { transform: translateX(-100%) }

.slide-enter-active,
.slide-leave-active { transition: all 750ms ease-in-out }

每当 selectedView 更改时,旧组件会向左滑动,新组件会从右侧进入!

以下是一个使用这些概念的演示

查看 Pen VueJS 过渡和过渡组演示,作者为 Nicolas Udy (@udyux),网站为 CodePen

列表上的过渡

当我们开始处理列表时,事情变得有趣起来。无论是项目符号列表还是博客文章网格,Vue 都提供了 <transition-group> 组件。

值得注意的是,虽然 <transition> 组件实际上并没有渲染元素,但 <transition-group> 会渲染。默认行为是使用 <span>,但您可以通过设置 <transition-group> 上的 tag 属性来覆盖此行为。

另一个需要注意的是,所有列表项都需要具有唯一的 key 属性。这样 Vue 就可以单独跟踪每个项目并优化其性能。在我们的演示中,我们正在遍历公司列表,每个公司都有一个唯一的 ID。所以我们可以像这样设置我们的列表

<transition-group name="company" tag="ul" class="content__list">
  <li class="company" v-for="company in list" :key="company.id">
    <!-- ... -->
  </li>
</transition-group>

transition-group 最令人印象深刻的功能是 Vue 如何无缝处理列表顺序的更改。为此,可以使用一个额外的过渡类 .company-move(与进入和离开的 active 类非常相似),该类将应用于正在移动但保持可见的列表项。

在演示中,我将其分解得更多,以展示如何利用不同的状态来获得更干净的最终结果。以下是样式的简化和简洁版本

/* base */
.company {
  backface-visibility: hidden;
  z-index: 1;
}

/* moving */
.company-move {
  transition: all 600ms ease-in-out 50ms;
}

/* appearing */
.company-enter-active {
  transition: all 300ms ease-out;
}

/* disappearing */
.company-leave-active {
  transition: all 200ms ease-in;
  position: absolute;
  z-index: 0;
}

/* appear at / disappear to */
.company-enter,
.company-leave-to {
  opacity: 0;
}

即使在没有 3D 变换的情况下,对元素使用 backface-visibility: hidden 也会确保丝滑的 60fps 过渡,并避免在变换期间出现模糊的文本渲染,方法是欺骗浏览器利用硬件加速。

在上面的代码段中,我将基本样式设置为 z-index: 1。这确保了页面上保持可见的元素始终显示在离开页面的元素之上。我还对正在离开的项目应用 absolute 定位,将其从自然流程中移除,从而触发其余项目的 move 过渡。

这就是我们需要做的!结果,坦率地说,几乎是魔术。

使用动画

Vue 中动画的可能性和方法几乎是无限的,所以我选择了一种我最喜欢的技术,来展示如何为数据添加动画。

我们将使用 GSAP 的 TweenLite 库,将缓动函数应用于状态的更改,并让 Vue 的闪电般快速的响应能力反映在 DOM 上。Vue 与内联 SVG 的兼容性与 HTML 一样出色。

我们将创建一个具有 5 个点的线图,它们沿 X 轴均匀分布,Y 轴代表百分比。您可以在这里查看结果。

查看 Pen 使用 VueJS 和 TweenLite 的 SVG 路径动画,作者为 Nicolas Udy (@udyux),网站为 CodePen

让我们从组件的逻辑开始。

new Vue({
  el: '#app',
  // this is the data-set that will be animated
  data() {
    return {
      points: { a: -1, b: -1, c: -1, d: -1, e: -1 }
    }
  },

  // this computed property builds an array of coordinates that
  // can be used as is in our path
  computed: {
    path() {
      return Object.keys(this.points)
        // we need to filter the array to remove any
        // properties TweenLite has added
        .filter(key => ~'abcde'.indexOf(key))
        // calculate X coordinate for 5 points evenly spread
        // then reverse the data-point, a higher % should
        // move up but Y coordinates increase downwards
        .map((key, i) => [i * 100, 100 - this.points[key]])
    }
  },

  methods: {
    // our randomly generated destination values
    // could be replaced by an array.unshift process
    setPoint(key) {
      let duration = this.random(3, 5)
      let destination = this.random(0, 100)
      this.animatePoint({ key, duration, destination })
    },
    // start the tween on this given object key and call setPoint
    // once complete to start over again, passing back the key
    animatePoint({ key, duration, destination }) {
      TweenLite.to(this.points, duration, {
        [key]: destination,
        ease: Sine.easeInOut,
        onComplete: this.setPoint,
        onCompleteParams: [key]
      })
    },
    random(min, max) {
      return ((Math.random() * (max - min)) + min).toFixed(2)
    }
  },

  // finally, trigger the whole process when ready
  mounted() {
    Object.keys(this.points).forEach(key => {
      this.setPoint(key)
    })
  }
});

现在是模板。

<main id="app" class="chart">
  <figure class="chart__content">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="-20 -25 440 125">
      <path class="chart__path" :d="`M${path}`"
        fill="none" stroke="rgba(255, 255, 255, 0.3)"
        stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>

      <text v-for="([ x, y ]) in path" :x="x - 10" :y="y - 7.5"
        font-size="10" font-weight="200" fill="currentColor">
        {{ 100 - (y | 0) + '%' }}
      </text>
    </svg>
  </figure>
</main>

注意我们如何将 path 计算属性绑定到路径元素的 d 属性。我们对输出该点当前值的文本节点也执行类似的操作。当 TweenLite 更新数据时,Vue 会立即做出反应,并保持 DOM 的同步。

这确实是所有内容!当然,还应用了额外的样式以使事物变得漂亮,此时你可能会意识到这比动画本身需要更多工作!

实时演示 (CodePen) & GitHub 代码库

继续浏览实时演示或分析/重新使用我们开源代码库中的代码!

结论

我一直是网页动画和过渡的粉丝,但我也是性能的坚持者。因此,在依赖 JavaScript 时,我总是非常谨慎。但是,将 Vue 的极快和低成本的响应式与它管理纯 CSS 过渡的能力相结合,你真的需要过度使用才会出现性能问题。

这样一个强大的框架可以提供如此简单却易于管理的 API,令人印象深刻。动画演示(包括样式)仅用 45 分钟构建完成。如果你不计入设置列表过渡中使用的模拟数据所需的时间,它可以在不到 2 小时内完成。我甚至不想想象在没有 Vue 的情况下构建类似设置会让人多么头疼,更不用说需要多长时间了!

现在,去创造吧!用例远远超出了我们在本文中看到的:唯一的真正限制是你的想象力。不要忘记查看 Vue.js 文档 中的过渡和动画部分,以获取更多信息和灵感。


本文最初发表在 Snipcart 博客上。有任何评论或问题?在下方添加你的评论!