第 130 章:动画与过渡
学习目标
- 掌握 Vue 3 内置的 transition 组件
- 学会 CSS 过渡、动画、JavaScript 钩子
- 理解 TransitionGroup 列表动画
- 集成第三方动画库(GSAP、Animate.css)
一、Vue 3 动画体系
二、基础过渡
2.1 transition 组件
vue
<template>
<button @click="show = !show">Toggle</button>
<transition name="fade">
<p v-if="show">Hello Vue</p>
</transition>
</template>2.2 CSS 过渡类
css
/* 进入 */
.fade-enter-active {
transition: opacity 0.3s;
}
.fade-enter-from {
opacity: 0;
}
.fade-enter-to {
opacity: 1;
}
/* 离开 */
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-leave-from {
opacity: 1;
}
.fade-leave-to {
opacity: 0;
}2.3 简化写法
css
/* 简写:状态类 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}三、CSS 动画
3.1 keyframes
vue
<template>
<transition name="bounce">
<p v-if="show">蹦跶!</p>
</transition>
</template>
<style>
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(1);
}
}
</style>3.2 自定义过渡类名
vue
<transition
enter-active-class="animated fadeIn"
leave-active-class="animated fadeOut"
>
<p v-if="show">Animate.css</p>
</transition>html
<!-- 引入 Animate.css -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/animate.css" />四、JavaScript 钩子
4.1 钩子函数
vue
<template>
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
>
<p v-if="show">JS 动画</p>
</transition>
</template>
<script setup>
const beforeEnter = (el: Element) => {
console.log('before enter');
};
const enter = (el: Element, done: () => void) => {
// 用 GSAP 等库
done();
};
const afterEnter = (el: Element) => {
console.log('after enter');
};
</script>4.2 velocity.js
bash
pnpm add velocity-animatetypescript
import Velocity from 'velocity-animate';
const enter = (el: Element, done: () => void) => {
Velocity(el, { opacity: 1, translateY: 0 }, { duration: 300, complete: done });
};
const leave = (el: Element, done: () => void) => {
Velocity(el, { opacity: 0, translateY: 20 }, { duration: 300, complete: done });
};4.3 GSAP
bash
pnpm add gsaptypescript
import gsap from 'gsap';
const enter = (el: Element, done: () => void) => {
gsap.fromTo(el,
{ opacity: 0, y: -20 },
{ opacity: 1, y: 0, duration: 0.5, onComplete: done }
);
};五、过渡模式
5.1 模式
vue
<!-- in-out:先进入后离开 -->
<transition mode="in-out">
<p v-if="show">A</p>
</transition>
<!-- out-in:先离开后进入 -->
<transition mode="out-in">
<p v-if="show">A</p>
</transition>5.2 初始渲染
vue
<!-- appear:首次渲染就有动画 -->
<transition appear>
<p>初始</p>
</transition>六、列表过渡
6.1 TransitionGroup
vue
<template>
<button @click="add">添加</button>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</TransitionGroup>
</template>
<style>
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from {
opacity: 0;
transform: translateX(30px);
}
.list-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.list-move {
transition: transform 0.5s ease;
}
</style>6.2 移动动画
css
/* 其他元素平滑移动 */
.list-move {
transition: transform 0.5s;
}
/* 离开元素脱离布局流 */
.list-leave-active {
position: absolute;
}七、状态过渡
7.1 数字动画
vue
<script setup lang="ts">
import { ref, watch } from 'vue';
import gsap from 'gsap';
const count = ref(0);
watch(count, (newVal) => {
gsap.to(animValue, {
duration: 1,
currentValue: newVal,
onUpdate: () => {
// 更新显示
}
});
});
</script>7.2 颜色过渡
vue
<template>
<input type="number" v-model.number="count" />
<p :style="{ color: color }">{{ count }}</p>
</template>
<script setup>
import { ref, computed } from 'vue';
const count = ref(0);
const color = computed(() => {
if (count.value > 0) return 'green';
if (count.value < 0) return 'red';
return 'black';
});
</script>7.3 SVG 动画
vue
<template>
<svg width="100" height="100">
<circle
:cx="50"
:cy="50"
:r="radius"
fill="green"
/>
</svg>
</template>
<script setup>
import { ref, watch } from 'vue';
const radius = ref(10);
const target = ref(40);
watch(target, (val) => {
// 动画改变 radius
});
</script>八、Vue Router 过渡
8.1 路由切换
vue
<template>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>8.2 滚动行为
typescript
const router = createRouter({
routes: [...],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition;
return { top: 0, behavior: 'smooth' };
}
});九、第三方动画库
9.1 @vueuse/motion
bash
pnpm add @vueuse/motiontypescript
import { Motion } from '@vueuse/motion';
// 注册
app.use(Motion);vue
<template>
<div
v-motion
:initial="{ opacity: 0, y: 100 }"
:enter="{ opacity: 1, y: 0, transition: { duration: 500 } }"
>
内容
</div>
</template>9.2 auto-animate
bash
pnpm add @formkit/auto-animatevue
<script setup>
import { useAutoAnimate } from '@formkit/auto-animate/vue';
const [parent] = useAutoAnimate();
const items = ref([1, 2, 3]);
</script>
<template>
<ul ref="parent">
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</template>9.3 Anime.js
bash
pnpm add animejstypescript
import anime from 'animejs';
anime({
targets: '.box',
translateX: 250,
duration: 800,
easing: 'easeInOutQuad'
});十、性能优化
10.1 transform / opacity
css
/* ✅ GPU 加速 */
.fade-enter-active {
transition: opacity 0.3s, transform 0.3s;
}
/* ❌ 触发 layout */
.fade-enter-active {
transition: width 0.3s, height 0.3s;
}10.2 will-change
css
.animated {
will-change: transform, opacity;
}10.3 减少动画
vue
<!-- 跨设备检测 -->
<template>
<transition :name="reducedMotion ? '' : 'fade'">
<p v-if="show">内容</p>
</transition>
</template>
<script setup>
import { ref } from 'vue';
const reducedMotion = ref(window.matchMedia('(prefers-reduced-motion: reduce)').matches);
</script>十一、复用动画
11.1 封装 Hook
typescript
// composables/useFadeIn.ts
import { ref } from 'vue';
export function useFadeIn(duration = 300) {
const visible = ref(false);
const opacity = ref(0);
function show() {
visible.value = true;
requestAnimationFrame(() => {
opacity.value = 1;
});
}
function hide() {
opacity.value = 0;
setTimeout(() => {
visible.value = false;
}, duration);
}
return { visible, opacity, show, hide };
}11.2 动画组件
vue
<!-- FadeIn.vue -->
<script setup lang="ts">
interface Props {
duration?: number;
delay?: number;
}
const props = withDefaults(defineProps<Props>(), {
duration: 300,
delay: 0
});
const visible = ref(false);
onMounted(() => {
setTimeout(() => {
visible.value = true;
}, props.delay);
});
</script>
<template>
<transition name="fade" :duration="duration">
<div v-if="visible" class="fade-content">
<slot />
</div>
</transition>
</template>
<style scoped>
.fade-enter-active {
transition: opacity 0.3s;
}
.fade-enter-from {
opacity: 0;
}
</style>十二、组件库中的动画
12.1 Modal 淡入淡出
vue
<template>
<transition name="modal">
<div v-if="modelValue" class="modal-mask">
<div class="modal-wrapper">
<slot />
</div>
</div>
</transition>
</template>
<style scoped>
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
</style>12.2 Collapse 折叠
vue
<template>
<button @click="open = !open">Toggle</button>
<transition name="collapse">
<div v-if="open" class="collapse-content">
<slot />
</div>
</transition>
</template>
<style scoped>
.collapse-enter-active,
.collapse-leave-active {
transition: max-height 0.3s ease;
overflow: hidden;
}
.collapse-enter-from,
.collapse-leave-to {
max-height: 0;
}
.collapse-enter-to,
.collapse-leave-from {
max-height: 500px;
}
</style>十三、本章小结
| 概念 | 关键 |
|---|---|
| transition | 单元素过渡 |
| TransitionGroup | 列表过渡 |
| CSS 类 | enter/leave + from/to/active |
| JS 钩子 | before/after + enter/leave |
| 模式 | in-out / out-in |
| 性能 | transform / opacity |
| 第三方 | GSAP / VueUse Motion |
动手练习
- 淡入淡出:实现一个 v-model 控制的 Modal
- 列表动画:实现 todo 列表的添加/删除/移动动画
- 数字滚动:用 GSAP 实现数字滚动效果
- 折叠面板:用 transition 实现高度过渡
- 路由切换:为路由变化添加过渡动画
推荐阅读
- 📖 Vue 3 Transition — 官方文档
- 📖 Animate.css — CSS 动画库
- 📖 GSAP — 强大的 JS 动画库
- 🌐 VueUse Motion — Vue 专属动画
下一章:第 131 章:Vite 进阶配置 →