这是关于 JavaScript 框架 Vue.js 的五部分系列中的第五部分。在本系列的最后一部分中,我们将介绍动画(如果你了解我,你可能知道这篇文章的主题)。这不是一本完整的指南,而是一个关于基础知识的概述,帮助你快速上手,了解 Vue.js 以及该框架提供的功能。

文章系列
- 渲染、指令和事件
- 组件、Props 和插槽
- Vue-cli
- Vuex
- 动画 (您当前位置!)
背景介绍
Vue 内置了 <transition>
和 <transition-group>
组件,允许使用 CSS 和 JS 钩子。如果你来自 React,那么 transition 组件背后的概念对你来说应该很熟悉,因为它与 ReactCSSTransitionGroup
在生命周期钩子方面的作用类似,但它也有一些显著的差异,让像我这样的极客感到兴奋。
我们将首先讨论 CSS 过渡,然后转向 CSS 动画,然后我们将讨论 JS 动画钩子,最后讨论使用生命周期方法进行动画。状态过渡不在本文的讨论范围之内,但它是可行的。 我制作了一个带有详细注释的 CodePen 演示,展示了如何实现。如果让我好好睡一觉,我或许可以考虑写一篇关于状态过渡的文章。
过渡与动画
以防你对本文为什么将过渡和动画放在不同的章节中感到困惑,让我解释一下,尽管它们听起来很相似,但它们之间存在一些差异。过渡基本上是通过在状态之间插值来工作的。我们可以用它们做很多很棒的事情,但它们相当简单。从这里到那里,然后再回来。
动画则有所不同,因为你可以在一个声明中让多个状态发生。例如,你可以将一个关键帧设置在动画的 50% 处,然后在 70% 处发生完全不同的事件,以此类推。你甚至可以将多个动画与延迟链接起来,以实现非常复杂的运动。动画能够像过渡一样“表现”,我们只需要在两个状态之间进行插值,但是过渡不能像动画那样有多个步骤(除非使用一些疯狂的、不建议使用的黑科技)。
在工具方面,两者都很实用。可以将过渡想象成一把手锯,而将动画想象成一把电锯。有时你只需要锯一个东西,去买非常昂贵的设备就显得有点傻了。对于其他更强大的项目,投资购买电锯更有意义。
现在我们已经掌握了这些基础知识,让我们来谈谈 Vue!
CSS 过渡
假设我们有一个简单的模态框。单击按钮即可显示和隐藏模态框。根据前面的章节,我们已经知道我们可以:创建一个带有按钮的 Vue 实例,从该实例创建一个子组件,在状态上设置数据以便切换某种布尔值,并添加一个事件处理程序来显示和隐藏此子组件。我们可以使用 v-if
或 v-show
来切换可见性。我们甚至可以使用插槽将按钮切换传递到模态框中。
<div id="app">
<h3>Let's trigger this here modal!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Hide child</span>
<span v-else>Show child</span>
</button>
<app-child v-if="isShowing" class="modal">
<button @click="toggleShow">
Close
</button>
</app-child>
</div>
<script type="text/x-template" id="childarea">
<div>
<h2>Here I am!</h2>
<slot></slot>
</div>
</script>
const Child = {
template: '#childarea'
};
new Vue({
el: '#app',
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
},
components: {
appChild: Child
}
});
查看 Sarah Drasner 的 CodePen 演示。
这可以工作,但模态框就这样突然出现在我们眼前,看起来很突兀。😳
我们已经使用 v-if
来挂载和卸载子组件,因此如果我们将该条件包装在 transition 组件中,Vue 就可以让我们跟踪该事件上的更改。
<transition name="fade">
<app-child v-if="isShowing" class="modal">
<button @click="toggleShow">
Close
</button>
</app-child>
</transition>
现在,我们可以直接使用 <transition>
。这将为我们提供一些可以在 CSS 中使用的过渡钩子的 v-
前缀。它将提供 enter/leave
(动画在第一帧开始时的位置)、enter-active/leave-active
(动画运行期间)——**这是你放置动画属性本身的地方**,以及 enter-to/leave-to
(指定元素在最后一帧应该在哪里)。
我将使用文档中的一个图形来展示这一点,因为我认为它对这些类的描述非常完美且清晰。

就我个人而言,我通常不使用默认的 v-
前缀。我总是给过渡命名,这样如果我最终要应用另一个动画就不会发生冲突。这并不难做到,如上所示,我们只需向 transition 组件添加一个 name
属性:name="fade"
。
现在我们有了钩子,就可以使用它们创建过渡了。
.fade-enter-active, .fade-leave-active {
transition: opacity 0.25s ease-out;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.fade-enter-active
和 .fade-leave-active
类将是我们应用实际过渡的地方。这是普通的 CSS,你可以传入三次贝塞尔曲线来设置缓动函数、延迟,或者指定其他需要过渡的属性。实际上,如果你将过渡放在组件类本身的这些类上作为默认值,它也能正常工作。这些不一定需要由 transition 组件钩子定义。它们只会在那里等待该属性发生变化,如果发生变化就会使用它来进行过渡。(因此你仍然需要 transition 组件和 .fade-enter、.fade-leave-to)。我使用 enter-active 和 leave-active 类的一个原因是,我可以将相同的过渡重新用于其他元素,而不必在代码库中四处应用相同的默认 CSS 到每个实例。
这里需要注意的另一点:我同时对两个 active 类使用了 ease-out
。对于像不透明度这样的属性,这可以正常工作,并且看起来不错。但是你可能会发现,如果你正在过渡像 transform 这样的属性,你可能希望将这两个类分开,对 enter-active 类使用 ease-out
,对 enter-leave 类使用 ease-in
(或大致遵循相同曲线的三次贝塞尔曲线)。我发现这会让动画看起来更……优雅(哈哈)。
你可以看到,我们还将 .fade-enter 和 .fade-to 设置为 opacity: 0
。这些将是动画的第一和最后一个位置,挂载时的初始状态,卸载时的最终状态。你可能认为需要在 .fade-enter-to
和 .fade-leave
上设置 opacity: 1
,但这是不必要的,因为这是组件的默认状态,所以是冗余的。除非另有说明,否则 CSS 过渡和动画将始终使用默认状态。
查看 Sarah Drasner 的 CodePen 演示。
这工作得很好!但是如果我们想让背景内容淡出视野,以便模态框占据中心位置,而背景失去焦点,会发生什么情况呢?我们不能使用 <transition>
组件,因为该组件基于某个元素的挂载或卸载来工作,而背景只是保持原样。我们可以根据状态切换类,并使用这些类来创建更改背景的 CSS 过渡。
<div v-bind:class="[isShowing ? blurClass : '', bkClass]">
<h3>Let's trigger this here modal!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Hide child</span>
<span v-else>Show child</span>
</button>
</div>
.bk {
transition: all 0.1s ease-out;
}
.blur {
filter: blur(2px);
opacity: 0.4;
}
new Vue({
el: '#app',
data() {
return {
isShowing: false,
bkClass: 'bk',
blurClass: 'blur'
}
},
...
});
查看 Sarah Drasner 的 CodePen 演示。
CSS 动画
现在我们了解了过渡的工作原理,就可以在此基础上构建一些漂亮的 CSS 动画。我们仍然会使用 <transition>
组件,并且仍然会为它命名,从而允许我们使用相同的类钩子。这里的区别在于,我们不会只设置最终状态并说明我们希望它如何在开始和结束之间进行插值,而是会使用 CSS 中的 @keyframes
来创建有趣且美观的视觉效果。
在上一节中,我们简要讨论了如何为 transition 组件指定一个特殊名称,然后我们可以将其用作类钩子。但在本节中,我们将更进一步,并将不同的类钩子应用于动画的不同部分。你可能还记得,enter-active 和 leave-active 是所有动画的核心所在。我们可以在每个类钩子上设置不同的属性,但我们可以更进一步,为每个实例提供特殊的类。
enter-active-class="toasty"
leave-active-class="bounceOut"
这意味着我们可以重用这些类,甚至可以从 CSS 动画库中引入这些类。
假设我们希望一个球弹跳进入并滚出。
<div id="app">
<h3>Bounce the Ball!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Get it gone!</span>
<span v-else>Here we go!</span>
</button>
<transition
name="ballmove"
enter-active-class="bouncein"
leave-active-class="rollout">
<div v-if="isShowing">
<app-child class="child"></app-child>
</div>
</transition>
</div>
对于弹跳,如果我们想在 CSS 中实现,则需要很多关键帧(尽管在 JS 中这可能只需要一行代码),我们还会使用 SASS 混合宏来保持我们的样式 DRY(不要重复自己)。我们还指定了 .ballmove-enter
类,让组件知道它应该从屏幕外开始。
@mixin ballb($yaxis: 0) {
transform: translate3d(0, $yaxis, 0);
}
@keyframes bouncein {
1% { @include ballb(-400px); }
20%, 40%, 60%, 80%, 95%, 99%, 100% { @include ballb() }
30% { @include ballb(-80px); }
50% { @include ballb(-40px); }
70% { @include ballb(-30px); }
90% { @include ballb(-15px); }
97% { @include ballb(-10px); }
}
.bouncein {
animation: bouncein 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
.ballmove-enter {
@include ballb(-400px);
}
对于球滚出,你可以看到我们需要嵌套两个不同的动画。这是因为变换被应用于整个子组件,旋转整个组件会导致巨大的旋转。因此,我们将使用平移将组件移动到屏幕上,并使用旋转在内部旋转球。
@keyframes rollout {
0% { transform: translate3d(0, 300px, 0); }
100% { transform: translate3d(1000px, 300px, 0); }
}
@keyframes ballroll {
0% { transform: rotate(0); }
100% { transform: rotate(1000deg); }
}
.rollout {
width: 60px;
height: 60px;
animation: rollout 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
div {
animation: ballroll 2s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
}
}
查看 Sarah Drasner 在 CodePen 上的 CodePen 演示 使用 Vue 过渡和 CSS 动画实现球的弹跳 (@sdras)。
甜蜜的过渡模式
你还记得我之前说过 Vue 在过渡方面提供了一些非常棒的“糖果”功能,让像我这样的极客感到快乐吗?这里有一个我非常喜欢的例子。如果你尝试在另一个组件离开时切换一个组件进入,你最终会遇到一个非常奇怪的时刻,两个组件同时存在然后突然跳回原位(Vue 文档中的这个小例子)

Vue 提供了过渡模式,它允许你在切换一个组件进入的同时,让另一个组件离开,而不会出现任何奇怪的位置闪烁或阻塞。它通过对过渡进行排序来实现,而不是让它们同时发生。有两种模式可供选择
进入-退出:当前元素等待新元素过渡完成之后再触发。
退出-进入:当前元素过渡退出,然后新元素过渡进入。
查看下面的演示。你可以看到过渡组件上的模式 - out-in
,这样看起来只有一个部分在翻转
<transition name="flip" mode="out-in">
<slot v-if="!isShowing"></slot>
<img v-else src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/cartoonvideo14.jpeg" />
</transition>
查看 CodePen 上 Sarah Drasner 的作品 Vue in-out modes (@sdras) 。
如果我们去掉那个模式,你可以看到一个翻转会遮挡另一个,动画看起来很突兀,根本不是我们想要的效果
查看 CodePen 上 Sarah Drasner 的作品 Vue in-out modes – no modes contrast (@sdras) 。
JS 动画
我们有一些非常易于使用的 JS 钩子,也可以根据需要在动画中使用或不使用。除了实际的动画钩子(进入和离开)之外,所有钩子都传递 el
参数(元素的缩写),这两个钩子还传递 done
作为参数,正如你猜到的那样,它用于告诉 Vue 动画已完成。你会注意到我们还在将 CSS 绑定到一个假值,以让组件知道我们将使用 JavaScript 而不是 CSS。
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-Leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
:css="false">
</transition>
在最基本的层面上,这实际上是进入和退出动画所需的全部内容,包括相应的函数
<transition
@enter="enterEl"
@leave="leaveEl"
:css="false">
<!-- put element here-->
</transition>
methods: {
enterEl(el, done) {
//entrance animation
done();
},
leaveEl(el, done) {
//exit animation
done();
},
}
这是一个关于如何将其插入 GreenSock 时间线的示例
new Vue({
el: '#app',
data() {
return {
message: 'This is a good place to type things.',
load: false
}
},
methods: {
beforeEnter(el) {
TweenMax.set(el, {
transformPerspective: 600,
perspective: 300,
transformStyle: "preserve-3d",
autoAlpha: 1
});
},
enter(el, done) {
...
tl.add("drop");
for (var i = 0; i < wordCount; i++) {
tl.from(split.words[i], 1.5, {
z: Math.floor(Math.random() * (1 + 150 - -150) + -150),
ease: Bounce.easeOut
}, "drop+=0." + (i/ 0.5));
...
}
}
});
查看 CodePen 上 Sarah Drasner 的作品 Vue Book Content Typer (@sdras) 。
在上面的动画中,有两点需要注意,我将 {onComplete:done}
作为参数传递给 Timeline 实例,并且我使用 beforeEnter
钩子放置我的 TweenMax.set
代码,这允许我在动画发生之前设置单词上所需的任何属性,在本例中,例如 transform-style: preserve-3d
。
需要注意的是,你也可以将动画所需的属性直接设置为 CSS 中的默认状态。人们有时会问我如何决定在 CSS 中设置什么,在 TweenMax.set
中设置什么。根据经验,我通常将动画所需的任何属性都放在 TweenMax.set
中。这样,如果动画中的某些内容发生变化,并且我需要更新它,它已经成为我工作流程的一部分了。
生命周期钩子中的动画
所有这些都非常棒,但是如果你需要动画化一些非常复杂的东西,一些与大量 DOM 元素一起工作的东西会发生什么?这是一个使用一些生命周期方法的绝佳时机。在下面的动画中,我们同时使用了 <transition>
组件和 mounted()
方法来创建一些动画。
查看 CodePen 上 Sarah Drasner 的作品 Vue Weather Notifier (@sdras) 。
当我们过渡单个元素时,我们将使用过渡组件,例如,当电话按钮周围的描边出现时
<transition
@before-enter="beforeEnterStroke"
@enter="enterStroke"
:css="false"
appear>
<path class="main-button" d="M413,272.2c5.1,1.4,7.2,4.7,4.7,7.4s-8.7,3.8-13.8,2.5-7.2-4.7-4.7-7.4S407.9,270.9,413,272.2Z" transform="translate(0 58)" fill="none"/>
</transition>
beforeEnterStroke(el) {
el.style.strokeWidth = 0;
el.style.stroke = 'orange';
},
enterStroke(el, done) {
const tl = new TimelineMax({
onComplete: done
});
tl.to(el, 0.75, {
strokeWidth: 1,
ease: Circ.easeOut
}, 1);
tl.to(el, 4, {
strokeWidth: 0,
opacity: 0,
ease: Sine.easeOut
});
},
但是当组件第一次出现并且我们有 30 个或更多元素需要动画时,再将每个元素都包装在一个单独的 transition
组件中就不再高效了。因此,我们将使用我们在本系列第 3 部分中提到的生命周期方法来连接到过渡钩子正在使用的相同事件:mounted()
const Tornadoarea = {
template: '#tornadoarea',
mounted () {
let audio = new Audio('https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/tornado.mp3'),
tl = new TimelineMax();
audio.play();
tl.add("tornado");
//tornado timeline begins
tl.staggerFromTo(".tornado-group ellipse", 1, {
opacity: 0
}, {
opacity: 1,
ease: Sine.easeOut
}, 0.15, "tornado");
...
}
};
我们确实可以根据哪个更有效来使用其中任何一个,正如你所看到的,你可以创建非常复杂的效果。Vue 提供了一个非常漂亮且灵活的 API,不仅用于创建可组合的前端架构,还用于流畅的运动和视图之间的无缝过渡。
结论
本系列文章并非旨在作为文档。尽管我们已经涵盖了很多内容,但仍然有更多内容需要探索:路由、混合、服务器端渲染等。有如此多令人惊叹的东西可以一起使用。访问 非常优秀的文档 和这个包含 示例和资源 的仓库以深入了解。还有一本书叫做 The Majesty of Vue.js,以及在 Egghead.io 上的课程。
非常感谢 Robin Rendle、Chris Coyier、Blake Newman 和 Evan You 校对本系列文章的各个部分。我希望本系列文章能传达我对 Vue 的兴奋之情,并帮助你开始尝试其中的一些内容!
文章系列
- 渲染、指令和事件
- 组件、Props 和插槽
- Vue-cli
- Vuex
- 动画 (您当前位置!)
非常感谢,Sarah!这些文章非常棒,并且与 Vue 的指南完美地结合在一起。
在最后部分,你提到了使用带有 Vue 的 SASS。你是单独使用 SASS 还是有首选的 Vue SASS 插件?我尝试过搜索,但没有找到。
我喜欢 Vue 如何使用其组件来保持样式的作用域,但作为一名重度 SASS 用户,我发现自己大部分时间都在 Vue 之外管理样式。
谢谢!
-Eric
嘿,如果你使用 Vue-cli 来搭建你的项目,添加 Sass 到你的样式中非常简单。只需运行 npm install sass sass-loader,然后在你的 style 标签中添加 lang=”sass” 即可。
body {
background-color: red
}
我看到了很多过渡的例子都是针对屏幕上出现和消失的元素。
如果我只想移动元素的位置(保持在屏幕上)怎么办?
例如,一个侧边栏菜单,根据数据是 true 还是 false 滑入。
我认为可以使用类绑定来实现.. 我正在使用“有点类似”你问题的效果,但使用按钮点击.. 假设我的侧边栏占整个屏幕的 20%.. 然后我给用户提供了一个使用点击缩小到 5% 的选项.. 你只需要两个类.. 并在用户点击时触发一个类.. 在这里阅读有关类绑定的信息.. https://vuejs.ac.cn/v2/guide/class-and-style.html