第四章:CSS动画与<transition>:从基础到复杂动效
说实话,Vue 的 <transition> 组件是个好东西。但很多人用着用着就卡住了——只会用那几个预置的 class 名,稍微复杂点的动效就不知道怎么搞了。我个人习惯是把 <transition> 和 CSS 动画结合起来用,这样既能享受 Vue 的自动管理,又能做出很炫的效果。
4.1 用 @keyframes 定义动画
先说说最基础的。Vue 的 <transition> 默认只支持过渡(transition),但你想用动画(animation)怎么办?其实很简单,直接在 CSS 里写 @keyframes,然后在对应的 class 里引用就行。
/* 定义一个弹入动画 */
@keyframes bounceIn {
0% {
transform: scale(0.3);
opacity: 0;
}
50% {
transform: scale(1.1);
}
70% {
transform: scale(0.9);
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* 应用到 Vue 的过渡 class 上 */
.bounce-enter-active {
animation: bounceIn 0.6s ease-out;
}
.bounce-leave-active {
animation: bounceIn 0.4s ease-in reverse;
}
嗯,这里要注意一点:leave 动画我用了 reverse 关键字,这样离开动画就是进入动画的倒放。我在项目中遇到过一个问题——如果动画不对称,直接 reverse 可能会有点违和感。这时候我建议你单独写一个离开的 @keyframes。
animation-fill-mode: forwards;。不然动画结束后元素会跳回初始状态,那画面...你懂的。
4.2 结合 animate.css 库
自己手写 @keyframes 当然可以,但有时候项目赶进度,或者你懒得想那些贝塞尔曲线参数,这时候用现成的动画库就舒服多了。我最常用的是 animate.css,它提供了几十种现成的动画效果。
怎么用?先安装:
npm install animate.css --save
然后在 main.js 里引入:
import 'animate.css';
接着在 Vue 组件里,直接把 animate.css 的 class 名拼到 enter-active-class 和 leave-active-class 上:
<transition
enter-active-class="animate__animated animate__bounceIn"
leave-active-class="animate__animated animate__bounceOut"
>
<div v-if="show" class="box">我弹进来了</div>
</transition>
你看,就这么简单。但有个坑——animate.css 默认动画时长是 1 秒,如果你觉得太快或太慢,可以用 CSS 变量覆盖:
<transition
enter-active-class="animate__animated animate__bounceIn"
leave-active-class="animate__animated animate__bounceOut"
:duration="800"
>
<div v-if="show" class="box">我弹进来了</div>
</transition>
或者直接在 CSS 里改:
.my-custom-box {
--animate-duration: 0.8s;
}
flash 动画,结果在低端安卓机上卡成 PPT。后来发现是动画里用了 visibility 属性切换,触发了重排。建议在移动端慎用那些频繁改变布局的动画。
4.3 动画钩子函数:精细控制每一帧
有时候光靠 CSS 搞不定。比如你想在动画开始前先加载一张图片,或者在动画结束后执行某个逻辑。这时候就需要 JavaScript 钩子函数 了。
Vue 的 <transition> 提供了 8 个钩子,但常用的就这几个:
| 钩子名 | 触发时机 | 常用场景 |
|---|---|---|
| @before-enter | 元素插入前 | 设置初始状态 |
| @enter | 元素插入后 | 启动动画,调用 done() |
| @after-enter | 动画结束后 | 清理状态,触发回调 |
| @before-leave | 离开动画前 | 记录当前位置 |
| @leave | 离开动画开始 | 启动离开动画 |
| @after-leave | 离开动画结束 | 移除元素 |
举个例子。假设你想做一个「先放大再缩小」的进入效果,用 CSS 写 @keyframes 当然可以,但用 JS 钩子会更灵活:
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
>
<div v-if="show" ref="box" class="box">看我表演</div>
</transition>
methods: {
beforeEnter(el) {
// 设置初始状态
el.style.transform = 'scale(0)';
el.style.opacity = '0';
},
enter(el, done) {
// 强制回流,让浏览器知道初始状态
el.offsetHeight;
// 用 Web Animations API 做动画
el.animate([
{ transform: 'scale(0)', opacity: 0 },
{ transform: 'scale(1.2)', opacity: 0.8 },
{ transform: 'scale(1)', opacity: 1 }
], {
duration: 600,
easing: 'ease-out'
}).onfinish = done; // 动画结束后调用 done()
},
afterEnter(el) {
// 动画完成后的清理工作
console.log('进入动画完成');
}
}
这里有个关键点:enter 和 leave 钩子必须调用 done() 回调,否则 Vue 不知道动画什么时候结束。我刚开始用的时候经常忘掉这个,结果动画卡在那里不动,排查了半天才发现是没调 done()。
enter 和 leave 里的 done() 必须被调用。如果你用了 el.animate(),记得在 onfinish 里调;如果你用了 setTimeout,就在回调里调。总之,一定要让 Vue 知道「动画结束了」。
4.4 实战:做一个带进度条的加载动画
光说不练假把式。我结合上面讲的知识点,做一个实际点的例子——一个带进度条的加载动画。当数据加载时,进度条从 0 到 100%,加载完成后内容淡入显示。
<template>
<div>
<transition
@before-enter="beforeProgress"
@enter="startProgress"
@after-enter="afterProgress"
>
<div v-if="loading" class="progress-bar">
<div class="progress-fill" ref="fill"></div>
<span class="progress-text">{{ progress }}%</span>
</div>
</transition>
<transition
enter-active-class="animate__animated animate__fadeInUp"
>
<div v-if="!loading" class="content">
加载完成!这里是内容。
</div>
</transition>
</div>
</template>
data() {
return {
loading: true,
progress: 0
};
},
methods: {
beforeProgress(el) {
this.progress = 0;
el.style.opacity = '1';
},
startProgress(el, done) {
// 模拟进度增长
const interval = setInterval(() => {
this.progress += Math.random() * 10;
if (this.progress >= 100) {
this.progress = 100;
clearInterval(interval);
// 进度条走完后,延迟一下再切换
setTimeout(() => {
this.loading = false;
done();
}, 500);
}
// 更新进度条宽度
this.$refs.fill.style.width = this.progress + '%';
}, 200);
},
afterProgress(el) {
console.log('加载流程结束');
}
}
这个例子把 CSS 动画(animate.css 的 fadeInUp)和 JS 钩子结合起来了。进度条部分用 JS 控制,内容出现部分用 CSS 动画。说白了,就是各取所长——JS 负责逻辑控制,CSS 负责视觉表现。
4.5 避坑指南
最后分享几个我踩过的坑:
- 动画不触发? 检查一下
v-if或v-show是否写对了。我有一回把v-if写成了v-show,结果<transition>根本不工作。 - 动画闪烁? 大概率是初始状态没设置好。在
before-enter里把元素的初始样式写清楚,比如opacity: 0、transform: scale(0)。 - 动画卡顿? 别用那些会触发重排的属性,比如
width、height、margin。尽量用transform和opacity,这两个属性 GPU 会帮你加速。 - animate.css 和自定义动画冲突? 记得给自定义动画的 class 加上
!important,或者提高选择器优先级。不然 animate.css 的样式会覆盖你的。
嗯,差不多就这些。CSS 动画加 <transition> 的组合,说白了就是「Vue 帮你管生命周期,你只管写动画」。用熟了之后,你会发现做动效其实没那么难。