概述
在实际项目中,通常需要结合第三方动画库来实现更丰富的动画效果。本章介绍如何在 Vue 2 中集成主流动画库,并提供性能优化建议。
第三方动画库对比
| 动画库 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| Animate.css | CSS 动画库 | 开箱即用、预设丰富、无需 JS | 快速实现常见动画效果 |
| GSAP | JavaScript 动画库 | 功能强大、性能优异、兼容性好 | 复杂动画、时间轴控制 |
| Velocity.js | JavaScript 动画库 | 轻量级、API 类似 jQuery | 简单的 JS 动画需求 |
| Mo.js | JavaScript 动画库 | 图形动画、运动路径 | 创意动画、图形效果 |
集成 Animate.css
Animate.css 是一个预设 CSS 动画库,提供了大量开箱即用的动画效果,非常适合快速集成到 Vue 项目中。
安装与配置
方式一:CDN 引入
<head>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
</head>方式二:NPM 安装
npm install animate.css
# 或
yarn add animate.css// main.js
import 'animate.css';基本用法
Animate.css 4.x 版本使用 animate__ 前缀:
<!-- 直接使用动画类 -->
<div class="animate__animated animate__bounce">弹跳效果</div>
<!-- 循环动画 -->
<div class="animate__animated animate__bounce animate__infinite">
无限弹跳
</div>
<!-- 延迟动画 -->
<div class="animate__animated animate__bounce animate__delay-2s">
延迟2秒弹跳
</div>
<!-- 控制动画速度 -->
<div class="animate__animated animate__bounce animate__faster">快速弹跳</div>
<div class="animate__animated animate__bounce animate__slower">慢速弹跳</div>自定义动画类名
Vue 的 <transition> 组件提供了自定义类名属性,可直接与 Animate.css 配合使用:
| 属性 | 说明 |
|---|---|
enter-active-class | 进入过渡的激活状态类名 |
leave-active-class | 离开过渡的激活状态类名 |
enter-class | 进入过渡的开始状态类名(可选) |
leave-class | 离开过渡的开始状态类名(可选) |
<transition
name="custom-classes-transition"
enter-active-class="animate__animated animate__tada"
leave-active-class="animate__animated animate__bounceOutRight"
>
<p v-if="show">使用 Animate.css 动画</p>
</transition>完整示例
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Animate.css 集成示例</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
<style>
.container {
max-width: 600px;
margin: 50px auto;
padding: 20px;
text-align: center;
}
.demo-box {
width: 200px;
height: 100px;
margin: 20px auto;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
font-weight: bold;
}
.btn {
padding: 10px 20px;
margin: 5px;
font-size: 14px;
border: none;
border-radius: 5px;
cursor: pointer;
background: #667eea;
color: white;
transition: transform 0.2s;
}
.btn:hover {
transform: scale(1.05);
}
.animation-select {
padding: 8px 15px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 5px;
margin: 10px;
}
</style>
</head>
<body>
<div id="app" class="container">
<h2>Animate.css 动画演示</h2>
<div>
<label>选择动画效果:</label>
<select v-model="selectedAnimation" class="animation-select">
<optgroup label="Attention Seekers">
<option value="bounce">bounce</option>
<option value="flash">flash</option>
<option value="pulse">pulse</option>
<option value="shakeX">shakeX</option>
<option value="shakeY">shakeY</option>
<option value="tada">tada</option>
</optgroup>
<optgroup label="Entrances">
<option value="fadeIn">fadeIn</option>
<option value="fadeInDown">fadeInDown</option>
<option value="fadeInLeft">fadeInLeft</option>
<option value="slideInUp">slideInUp</option>
<option value="zoomIn">zoomIn</option>
</optgroup>
<optgroup label="Exits">
<option value="fadeOut">fadeOut</option>
<option value="fadeOutUp">fadeOutUp</option>
<option value="slideOutDown">slideOutDown</option>
<option value="zoomOut">zoomOut</option>
</optgroup>
</select>
</div>
<transition
enter-active-class="animate__animated"
:enter-active-class="'animate__animated animate__' + selectedAnimation"
leave-active-class="animate__animated animate__fadeOut"
>
<div v-if="show" class="demo-box">动画效果演示</div>
</transition>
<div>
<button @click="show = !show" class="btn">切换显示</button>
<button @click="triggerAnimation" class="btn">触发动画</button>
</div>
<!-- 循环动画示例 -->
<h3>循环动画</h3>
<div class="demo-box animate__animated animate__heartBeat animate__infinite">
无限心跳
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
show: true,
selectedAnimation: 'fadeIn',
animationKey: 0,
},
methods: {
triggerAnimation() {
// 强制重新渲染以触发动画
this.show = false;
this.$nextTick(() => {
this.show = true;
});
},
},
});
</script>
</body>
</html>集成 GSAP
GSAP(GreenSock Animation Platform)是业界领先的 JavaScript 动画库,提供了强大的动画控制能力,特别适合复杂动画场景。
安装与配置
方式一:CDN 引入
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>方式二:NPM 安装
npm install gsap
# 或
yarn add gsap// 按需引入
import { gsap } from 'gsap';
// 或引入所有模块
import gsap from 'gsap';JavaScript 钩子函数
Vue 的 <transition> 组件提供了 JavaScript 钩子,可在过渡的特定阶段执行代码:
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter"
v-on:enter-cancelled="enterCancelled"
v-on:before-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
v-on:leave-cancelled="leaveCancelled"
:css="false"
>
<!-- ... -->
</transition>methods: {
// 进入动画
beforeEnter: function(el) {
// 进入前:设置初始状态
gsap.set(el, {
opacity: 0,
scale: 0.5
});
},
enter: function(el, done) {
// 进入动画
gsap.to(el, {
duration: 0.8,
opacity: 1,
scale: 1,
ease: 'elastic.out(1, 0.5)',
onComplete: done // 必须调用 done 回调
});
},
afterEnter: function(el) {
// 进入完成后
console.log('进入动画完成');
},
// 离开动画
leave: function(el, done) {
gsap.to(el, {
duration: 0.5,
opacity: 0,
scale: 0.5,
onComplete: done
});
}
}注意:当使用 JavaScript 过渡时,需要设置
:css="false"以跳过 CSS 检测,避免性能浪费。
状态动画
GSAP 可与 Vue 的侦听器配合,实现数据状态的平滑过渡:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>GSAP 状态动画</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
.counter {
font-size: 48px;
font-weight: bold;
text-align: center;
color: #667eea;
}
.color-box {
width: 200px;
height: 200px;
margin: 20px auto;
border-radius: 20px;
transition: none; /* 禁用 CSS 过渡,使用 GSAP */
}
</style>
</head>
<body>
<div id="app">
<h2>数字动画</h2>
<div class="counter">{{ animatedNumber.toFixed(0) }}</div>
<button @click="addNumber">+10</button>
<button @click="subtractNumber">-10</button>
<h2>颜色动画</h2>
<div class="color-box" :style="{ backgroundColor: animatedColor }"></div>
<input type="color" v-model="targetColor" />
</div>
<script>
new Vue({
el: '#app',
data: {
number: 0,
animatedNumber: 0,
targetColor: '#667eea',
animatedColor: '#667eea',
},
watch: {
number: function (newValue) {
// 数字补间动画
gsap.to(this.$data, {
duration: 0.5,
animatedNumber: newValue,
ease: 'power2.out',
});
},
targetColor: function (newValue) {
// 颜色补间动画
gsap.to(this.$data, {
duration: 0.5,
animatedColor: newValue,
});
},
},
methods: {
addNumber() {
this.number += 10;
},
subtractNumber() {
this.number -= 10;
},
},
});
</script>
</body>
</html>时间轴动画
GSAP 的时间轴(Timeline)功能可创建复杂的动画序列:
methods: {
enterTimeline: function(el, done) {
// 创建时间轴动画
const tl = gsap.timeline({
onComplete: done
});
tl.fromTo(el, {
opacity: 0,
scale: 0.5,
rotation: -180
}, {
duration: 0.5,
opacity: 1,
scale: 1,
rotation: 0,
ease: 'back.out(1.7)'
})
.fromTo(el.querySelectorAll('.item'), {
opacity: 0,
y: 30
}, {
duration: 0.3,
opacity: 1,
y: 0,
stagger: 0.1 // 交错出现
});
}
}完整示例
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>GSAP 集成完整示例</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.demo-section {
margin: 40px 0;
padding: 30px;
background: #f8f9fa;
border-radius: 15px;
}
.card {
width: 300px;
padding: 20px;
margin: 20px auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.card-content {
color: #666;
line-height: 1.6;
}
.btn {
padding: 12px 24px;
margin: 5px;
font-size: 14px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.progress-bar {
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin: 20px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
width: 0%;
}
.stagger-container {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.stagger-item {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}
</style>
</head>
<body>
<div id="app" class="container">
<h1 style="text-align: center; color: #333">GSAP 动画演示</h1>
<!-- 卡片进入/离开动画 -->
<div class="demo-section">
<h3>卡片过渡动画</h3>
<button @click="showCard = !showCard" class="btn btn-primary">
切换卡片
</button>
<transition
:css="false"
@before-enter="cardBeforeEnter"
@enter="cardEnter"
@leave="cardLeave"
>
<div v-if="showCard" class="card">
<div class="card-title">GSAP 动画卡片</div>
<div class="card-content">
这是一个使用 GSAP 实现过渡动画的卡片组件,支持弹性动画效果。
</div>
</div>
</transition>
</div>
<!-- 进度条动画 -->
<div class="demo-section">
<h3>进度条动画</h3>
<div class="progress-bar">
<div ref="progressFill" class="progress-fill"></div>
</div>
<div style="text-align: center">
<button @click="animateProgress" class="btn btn-primary">
开始加载
</button>
<button @click="resetProgress" class="btn btn-primary">重置</button>
</div>
</div>
<!-- 交错动画 -->
<div class="demo-section">
<h3>交错动画(Stagger)</h3>
<div ref="staggerContainer" class="stagger-container">
<div v-for="i in 8" :key="i" class="stagger-item">{{ i }}</div>
</div>
<div style="text-align: center; margin-top: 20px">
<button @click="playStagger" class="btn btn-primary">播放动画</button>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
showCard: true,
},
methods: {
// 卡片动画
cardBeforeEnter(el) {
gsap.set(el, {
opacity: 0,
scale: 0.5,
y: -50,
});
},
cardEnter(el, done) {
gsap.to(el, {
duration: 0.8,
opacity: 1,
scale: 1,
y: 0,
ease: 'elastic.out(1, 0.5)',
onComplete: done,
});
},
cardLeave(el, done) {
gsap.to(el, {
duration: 0.4,
opacity: 0,
scale: 0.5,
y: 50,
ease: 'power2.in',
onComplete: done,
});
},
// 进度条动画
animateProgress() {
const progress = this.$refs.progressFill;
gsap.to(progress, {
duration: 2,
width: '100%',
ease: 'power2.out',
});
},
resetProgress() {
const progress = this.$refs.progressFill;
gsap.to(progress, {
duration: 0.3,
width: '0%',
});
},
// 交错动画
playStagger() {
const items = this.$refs.staggerContainer.querySelectorAll(
'.stagger-item'
);
gsap.fromTo(
items,
{
opacity: 0.3,
scale: 0.5,
rotation: -15,
},
{
duration: 0.5,
opacity: 1,
scale: 1,
rotation: 0,
stagger: 0.1,
ease: 'back.out(1.7)',
}
);
},
},
});
</script>
</body>
</html>性能优化建议
硬件加速优化
使用 CSS 属性触发 GPU 硬件加速,可显著提升动画性能:
/* 推荐:使用 transform 和 opacity */
.animate-element {
/* 触发硬件加速 */
transform: translateZ(0);
/* 或使用 will-change 提示浏览器优化 */
will-change: transform, opacity;
}
/* 进入动画 */
.fade-enter-active {
transition: transform 0.3s ease, opacity 0.3s ease;
will-change: transform, opacity;
}
.fade-enter {
opacity: 0;
transform: translateY(20px);
}/* 不推荐:直接动画会触发重排的属性 */
.bad-animation {
transition: width 0.3s, height 0.3s, left 0.3s, top 0.3s;
}避免重绘与重排
| 属性 | 是否触发重排 | 性能建议 |
|---|---|---|
width / height | 是 | 使用 transform: scale() |
top / left | 是 | 使用 transform: translate() |
margin / padding | 是 | 使用 transform |
opacity | 否(仅重绘) | 推荐 |
transform | 否 | 强烈推荐 |
color / background | 否(仅重绘) | 可接受 |
动画属性选择
/* 高性能动画属性(推荐) */
.good-anim {
transition: transform 0.3s ease, opacity 0.3s ease;
}
/* 低性能动画属性(避免) */
.bad-anim {
transition: width 0.3s, height 0.3s, margin 0.3s;
}过渡帧率优化
/* 限制动画帧率以减少资源消耗 */
.optimized-anim {
transition: transform 0.3s ease;
/* 提升渲染层级 */
backface-visibility: hidden;
perspective: 1000px;
}// 使用 requestAnimationFrame 优化 JS 动画
methods: {
customAnimation(el, done) {
let start = null;
const duration = 300;
function animate(timestamp) {
if (!start) start = timestamp;
const progress = (timestamp - start) / duration;
if (progress < 1) {
el.style.opacity = progress;
requestAnimationFrame(animate);
} else {
el.style.opacity = 1;
done();
}
}
requestAnimationFrame(animate);
}
}动画设计原则
1. 意义性
动画应该有明确的目的,而非单纯的装饰:
- 引导注意力:突出重要操作或变化
- 状态反馈:确认用户操作已被接收
- 空间关系:展示元素间的层级或关联
2. 适度性
- 持续时间:大多数 UI 动画应在 200-500ms 之间
- 缓动函数:使用自然流畅的缓动曲线
- 频率控制:避免过于频繁的动画
/* 推荐的动画持续时间 */
.quick-feedback {
transition-duration: 200ms; /* 快速反馈 */
}
.standard-transition {
transition-duration: 300ms; /* 标准过渡 */
}
.complex-animation {
transition-duration: 500ms; /* 复杂动画 */
}3. 一致性
保持整个应用的动画风格统一:
/* 定义统一的动画变量 */
:root {
--transition-fast: 200ms ease;
--transition-normal: 300ms ease-out;
--transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
}常见问题
Q1: 动画结束后元素闪烁怎么办?
问题原因:动画类名被移除后,样式突然变化。
解决方案:确保元素的最终状态与动画结束状态一致。
/* 错误示例 */
.fade-enter {
opacity: 0;
}
.fade-enter-to {
opacity: 1;
}
/* 正确示例:设置元素的默认样式 */
.fade-element {
opacity: 1; /* 与 enter-to 状态一致 */
}Q2: 列表动画中元素错位?
问题原因:缺少 key 属性或使用了索引作为 key。
解决方案:为每个列表项提供唯一且稳定的 key。
<!-- 错误:使用索引作为 key -->
<div v-for="(item, index) in items" :key="index">
<!-- 正确:使用唯一 ID -->
<div v-for="item in items" :key="item.id">Q3: 如何调试过渡动画?
方案:使用浏览器开发者工具。
- 打开开发者工具(F12)
- 切换到 Elements 面板
- 选中目标元素
- 在 Styles 面板中查看应用的类名
- 使用 Animations 面板录制和分析动画
// Vue Devtools 中查看过渡状态
mounted() {
console.log(this.$refs.transitionElement);
}Q4: 过渡钩子中的 done 回调什么时候调用?
回答:当使用 JavaScript 过渡时,enter 和 leave 钩子必须调用 done 回调,否则过渡永远不会结束。
methods: {
enter: function(el, done) {
// 错误:忘记调用 done
// gsap.to(el, { opacity: 1, duration: 0.5 });
// 正确:在动画完成时调用 done
gsap.to(el, {
opacity: 1,
duration: 0.5,
onComplete: done // 必须调用
});
}
}Q5: Animate.css 动画不生效?
可能原因:
- 类名前缀问题:Animate.css 4.x 需要
animate__前缀
<!-- 错误 -->
<div class="animated bounce">
<!-- 正确 -->
<div class="animate__animated animate__bounce">-
CSS 文件未正确引入:检查网络面板确认 CSS 文件已加载
-
元素被隐藏:确保元素可见且
display不为none
Q6: GSAP 动画如何与 Vue 响应式数据配合?
方案:使用 Vue 的侦听器或计算属性。
data() {
return {
targetValue: 0,
animatedValue: 0 // 用于动画的中间值
}
},
watch: {
targetValue(newVal) {
// 当目标值变化时,平滑过渡 animatedValue
gsap.to(this.$data, {
animatedValue: newVal,
duration: 0.5,
ease: 'power2.out'
});
}
}