引言
还在为单调乏味的页面发愁吗?是否羡慕那些充满生命力的网页动效?作为前端开发者,如何让用户在访问网站的第一刻就被吸引?答案就是 - 动画!优雅的动画效果不仅能提升用户体验,还能让你的应用脱颖而出。但在实际开发中,你是否遇到过这些问题:
- 动画代码重复写,维护困难
- 不同页面动画风格不统一
- 列表动画实现复杂
- 动画性能优化难
别担心,本文将带你探索Vue3动画系统,教你如何使用vue3的transition和transition-group!
从Vue3 transition开始
基础认知
Vue3为开发者提供的transition组件,它为我们提供了丰富的过渡效果。来看看它的核心特性:
| 特性 | 说明 | 示例值 | 使用场景 |
|---|---|---|---|
| name | 过渡类名前缀 | fade | 定义过渡效果的样式名 |
| mode | 过渡模式 | in-out/out-in | 控制过渡时序 |
| appear | 首次渲染动画 | true/false | 控制初始渲染时是否执行动画 |
| duration | 持续时间 | { enter: 500, leave: 800 } | 精确控制动画时长 |
| css | 是否使用CSS过渡 | true/false | 禁用CSS动画转而使用JS动画 |
实战示例:卡片切换效果
让我们来实现一个简单但实用的卡片切换效果:
<template>
<div class="card-container">
<button @click="toggleCard" class="btn">切换卡片</button>
<transition name="card">
<div v-if="showCard" class="card">
<h3>{{ currentCard.title }}</h3>
<p>{{ currentCard.content }}</p>
</div>
</transition>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showCard = ref(true)
const currentCard = ref({
title: '卡片标题',
content: '这是一段示例内容'
})
const toggleCard = () => {
showCard.value = !showCard.value
}
</script>
<style scoped>
.card-container {
padding: 20px;
}
.card {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
.card-enter-active,
.card-leave-active {
transition: all 0.3s ease;
}
.card-enter-from,
.card-leave-to {
opacity: 0;
transform: translateY(20px);
}
</style>
遇到的问题
看起来不错,但我们很快就会发现一些问题:
- 每个需要动画的地方都要重复编写类似的CSS代码
- 动画效果不易统一管理
- 代码复用性差
- 无法快速切换动画方向和效果
优化方案:封装通用过渡组件
为了解决上述问题,我们可以自己去封装一个功能强大的过渡组件:
<template>
<transition
:name="transitionName"
mode="out-in"
:duration="durationConfig"
appear
@before-leave="beforeLeave"
@leave="leave"
>
<slot />
</transition>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
direction: {
type: String,
default: 'y', // 'x' 或 'y'
},
distance: {
type: Number,
default: 20
},
enterDuration: {
type: Number,
default: 500
},
leaveDuration: {
type: Number,
default: 800
},
})
const transitionName = computed(() => {
return `switch-transition-${props.direction}`
})
const durationConfig = computed(() => ({
enter: props.enterDuration,
leave: props.leaveDuration
}))
const beforeLeave = (el) => {
const { height } = el.getBoundingClientRect()
el.style.height = height + 'px'
}
const leave = (el) => {
el.offsetHeight
el.style.height = '0'
el.style.paddingTop = '0'
el.style.paddingBottom = '0'
}
</script>
<style scoped>
/* Y 轴方向的过渡 */
.switch-transition-y-enter-active,
.switch-transition-y-leave-active {
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.switch-transition-y-enter-from {
opacity: 0;
transform: translateY(v-bind('props.distance * -1 + "px"'));
}
.switch-transition-y-leave-to {
opacity: 0;
transform: translateY(v-bind('props.distance + "px"'));
height: 0;
padding: 0;
}
/* X 轴方向的过渡 */
.switch-transition-x-enter-active,
.switch-transition-x-leave-active {
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.switch-transition-x-enter-from {
opacity: 0;
transform: translateX(v-bind('props.distance * -1 + "px"'));
}
.switch-transition-x-leave-to {
opacity: 0;
transform: translateX(v-bind('props.distance + "px"'));
height: 0;
padding: 0;
}
</style>
使用封装后的组件
<template>
<div class="demo-container">
<button @click="()=>showContent=!showContent" class="btn">切换内容</button>
<switchTransition
>
<div v-if="showContent" class="content-card">
Hello transition!!!
</div>
</switchTransition>
</div>
</template>
<script setup>
import { ref } from 'vue';
import switchTransition from './switch-transition.vue';
const showContent =ref(true)
</script>
以下是使用上述组件实现的效果:
下方是示例链接
https://play.vuejs.org/#eNrFWHtz28YR/yoXtjOUUwF8yHJkVPK4djNTd1onE/uPZELPBCSOJCwQwADgQ9Foxg/Zkl2pktP4KSeO3PgxmZp2WteWzMj6MgRIfYvuPQACIKhYTWdqSyPc7d7u3m/3dvduPvU70xQbdZySUtMOrpma7OBjBR2haUVtoJIm2/ZMISWbplAydEdWdWwVUpQBWN4TBNR9+6T/dtv7bg0Jgj8fXmqrCi7Kg0XJZKGKZSUkmvJVc8fc14+6O/fdtRfTGRgFEjIgIhgFs8Sc/tp9d/Xm3ub2wByg6PJAYw3r9ageYs9giFBDKBsWcI4R1nGk6gpuHYI/iIztQirCLM3iOeClTIVUmBJWKKgAbpgc0u8DudVJNN43MTxG6HhJU0uzIN0xKhUNn6kXiZoxZmvUjpgljupoOMoR0QXabFPWj83P0/2KlB8tLExn6HSMVYWwEEpV3LAMPYILxcZXPJ+2DAciKy0xmar9kYn1hbidA0u5xDhDJooKD4NhKHd2e18/7e8ueVubvc5it/PKXbnS//u2+2ydQTyE75mm6pSqZy1Zt1VHha0oqoVL5AuMmQOXS4pqO7JewjDOTUbiJ4ihhqCWOcp8h7AwCHTmoaGVSc6NBCFfOI7g41QkFMVSVdUUi6gZXs/j0l+UxBIzbShGh2KtKuuKhv8MvCfJjG/aULwNRRT5B/HE+YOQGkIicq5HzU1n4t4KH+YI+3QGTr6flSLR4h859+oVt73trrxxHzwYkcFqkPVo9sO6E05jB4qZiSyZ0LDcwIJSt2TOls/SeZCMrfD8BJmP5yju0lLdAp87J5lFDMxQoHFLhZJsKbFom67mybFOEkAPOJAj7GYCNxdP+c19gB/howGb/zWdCdUdGNolSzUdZGOnTuSrNdOwHDSPLFxGC6hsGTWUhoKVDkhDfmA8YsamBMEhFEIgdS79W6Ikk+GFwrv5wlttF3TYle2w/I5miKqxz4mN88xoipCE0ozbe3yp/2Q5Pc5o7KBLyLHqmE/5x1JCVAgN/kCG2/7Wu/2qt3HD++uT9DjicAKhv3t3b2nF/fFm7/s3YSam1P1qxV1f7D1ZdTfe9jefiqKYRgtcX1g8516+6n27niDeu/M8zOFdfwTCQHb/+eNe52qv86Df3uzubnoXn4/UsPdwsf/sMWOKaPDuvAXTvWsXvPvXev/qgDDGSkz/6ZK7tRUWzOSeI3+YkhjUUAR7G+1ee7O3fjUOdVnW7HfCmglxl2/DJiOmMpSZqf3djf7mSnfrWe/eIl8ABrcvdzv/gJne9VfehYsjweh2Ou71TQBz78pqRAOz3N14uHfnQZiJYL274b553H/9oL/1w2iUl1Z6T//Se3gjIpXZt7e02n98MeAAkRAse7df9q9f6l3aZnt7N5SZm/rtt72d9n+Ncnfrh+6bNwkB0d1te19vM3L/34verbsE2Ytr7qMdpnJ0FH9zee/uuu/9IVihuHvLrxkTAfTJV/1r/2T4jhTJI/LKKt1q+GC8uOzdfMnI7tolOB7EyitP3R8vMO5hKAv6uUM8jbg7f3OvrbLQh3Bh5cTPJ9HUyRMLdUFyNiCUX5AQCvpCYNbykrf6MOh3uH00ln3jIo0j2MZ7RzRzjEUJTYZiQ9bq+HNKOsebGuB9byQRbPBN+H4RXMX0k7Bc6viaY20E0e13EoH2WNWhmoDR76KIFmhIaakIlw34BtWnQM9J3pOy/s9XHabMME1+9ZHQF7xE2Y0KatU0nVTTquOYUibTbDbF5oRoWJUMlOxsBlig5DZVxamShvAIDKpYrVQdf9RQcfOE0YJhFmVR/jD8wGxZ1TSY0g2dVGzbsYxZ0hwEm9UMuALxecGXnh9MaXD/KskmTFpGXYfyHiacN1TSOnDKoBSbhjZH6MgEBods6gg6inJ5lJtEuSl0FHihlHOmoEzDDuk3gDKMtTMHHYNdMkyswIwYuR4yVKH1AVTnII1ouAU1F+IJuiiGkYRy2WyjSmeLcmm2Qk0GCbB9Cf2qPFE+XD4CVBpJIr8hMrEUEwnlJ4+YTGrC+lw5f3TiA0rlU80qtLZxgfzKyeSasqKoegUss3BtFGc1x5jLsFXBVr+EkMmJ+Um2gk83+Q6LhqYEcugJY4dKtiqAg2OYMVVBAx6zJysS+Qm8rGsbgbWsqRWdirMlVKLdJZ0/X4dGqDznd7MSgitdCQtF7DQx1hlkdcsmmNFg4cti1jDgDQswESxZUeugJBsAEbdRqhoNH+YEb018cDg3mQvW+VdJdjSDno70V/ANV6IaqJqw4/wiu1+GlhFWCbHpsdxUVsGVQwPH8qwX9omGyyQy/f2FPRVsO7J6tLv2BygJ42HBB4MtfEfhQQqxANuJ+i8f3kb4ojBKj39ywidkny2GKC3BrsqK0YRplDNbaAJ+s8iqFOWx7DjiP2Ju4JOIOdV8wlHb96QFHisajmOA68NHJvM+ctv3utvXvM499/pT75tN77vX7k9r6P1MQZckkFWcVUFRyTI0LZ5sWKqhchJYyfUCalgMP5LGcuT/viur9VoxYeXU1FQSvIDgz0pLjhqQOTk5yRZDJifZG9J2ajzlQA7Xy2pFPG8bOjwB0nXkClkzVQ1bH5nk7MGLl+Q3jvAUqGlG8490LnTloS82pdmE+fM2vD1I8PGxhW1sNaDwBTQHPIbhTk3IH545jVvwHRBrhlInF9t9iJ9g29DqxEbGdgL2CmaH+Ki1p+g9EYL3rP1hC2LM9jdFDB30xoUU3A9P7rP1gbkT4mHeCC4Aikn3zOH31EEy4+9kki7XSPUfEE7DRPC8CFskVKPuCCp5SOKLQo8E/ic0SeBD/xEGijGG91b6fbyIIQ9Ce0DeHWAFG/6JjHz24z6N/mWzfhNga4bD39zglh6+yr/rpZ1EUt3BSuzm7rdjpmWY5MKt4DK0Dh+TEeuPg6eUAH5nzoQkcMaxwI/cvbBKrmukVZ6Djh76vnQrjbzlW2Q8cKv/CBOTdBpOC7bikvLZwUJaNn/PMX6n1ZPwahMsp3geaPnUYDlr5HmjHokPQMsHdWxs0C9bgLyloy/CsUjXCL+epyiLAaQLpKUD+Ux6NIiGpTN/UCygYFFJEWAGe/XJkY0TVaG9hAKQtP1YC3X8lGGe99EQMDMIayIcuBMkhYHX4bIAmj+BTYyBRLBJE2kqE/mKGX/pb1DabJHb0EAvtSmuEQQY5TLE7B/oumSZ6SyNpYDA6+BZwxxNPEFLkE//ufYZitNnqL/z0ru17a7fgEske7imtUkc9uecwF4LZXBmg+TZRB720sh4hpspSIukWtvQiRTVEvR/X6rYGsuKh3lZzo8jWpkRIuWkDDlUQlVVUWiPyHuVkYbRg05VGtBdqg40plkqK9SX0U+SPj4bawhFuEGOpf0oZacVvY+EHHiykDKhfqQPhXq3kbt1jF+uNqoR8ZDyRQ36vFBn8ekBnNd6B+e1/h/O8w07kPM+/R84z9/tAZw3Uu3BnOd3QqmF/wCfTI/U
升级:使用transition-group处理列表动画
<TransitionGroup> 是一个内置组件,用于对 v-for 列表中的元素或组件的插入、移除和顺序改变添加动画效果。
和 <Transition> 的区别
<TransitionGroup> 支持和 <Transition> 基本相同的 props、CSS 过渡 class 和 JavaScript 钩子监听器,但有以下几点区别:
- 默认情况下,它不会渲染一个容器元素。但你可以通过传入
tagprop 来指定一个元素作为容器元素来渲染。 - 过渡模式在这里不可用,因为我们不再是在互斥的元素之间进行切换。
- 列表中的每个元素都必须有一个独一无二的
keyattribute。 - CSS 过渡 class 会被应用在列表内的元素上,而不是容器元素上。
(以上介绍来自vue的官方网站)(TransitionGroup | Vue.js)
transition-group的特点与优势
- 支持列表项的移动动画
- 自动处理列表项的定位
- 支持列表项的进入/离开动画
- 可以与CSS动画配合使用
封装TransitionGroup组件
<template>
<TransitionGroup
tag="ul"
:name="transitionName"
class="container"
:duration="{ enter: enterDelay, leave: leaveDelay }"
>
<slot />
</TransitionGroup>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
direction: {
type: String,
default: 'x',
validator: (value) => ['x', 'y', 'both'].includes(value)
},
distance: {
type: Number,
default: 30
},
enterDelay: {
type: Number,
default: 500
},
leaveDelay: {
type: Number,
default: 500
}
})
const transitionName = computed(() => `fade-${props.direction}`)
</script>
<style scoped>
/* X方向动画 */
.fade-x-move,
.fade-x-enter-active,
.fade-x-leave-active {
transition: all cubic-bezier(0.55, 0, 0.1, 1);
}
.fade-x-enter-from,
.fade-x-leave-to {
opacity: 0;
transform: scaleY(0.01) translateX(v-bind('props.distance + "px"'));
}
/* Y方向动画 */
.fade-y-move,
.fade-y-enter-active,
.fade-y-leave-active {
transition: all cubic-bezier(0.55, 0, 0.1, 1);
}
.fade-y-enter-from,
.fade-y-leave-to {
opacity: 0;
transform: scaleX(0.01) translateY(v-bind('props.distance + "px"'));
}
/* 双向动画 */
.fade-both-move,
.fade-both-enter-active,
.fade-both-leave-active {
transition: all cubic-bezier(0.55, 0, 0.1, 1);
}
.fade-both-enter-from,
.fade-both-leave-to {
opacity: 0;
transform: scale(0.01) translate(v-bind('props.distance + "px"'), v-bind('props.distance + "px"'));
}
/* 通用离开动画样式 */
.fade-x-leave-active,
.fade-y-leave-active,
.fade-both-leave-active {
position: absolute;
}
/* 容器样式 */
.container {
position: relative;
list-style: none;
padding: 0;
margin: 0;
}
</style>
实战示例:动态列表
这是使用示例
<ListTransition
direction="x"
:distance="50"
:enter-delay="500"
:leave-delay="500"
>
<li
v-for="item in items"
:key="item.id"
class="item"
@click="removeItem(item.id)"
>
{{ item.text }}
<span class="delete-hint">(点击删除)</span>
</li>
</ListTransition>
高级技巧与性能优化
- 使用will-change提升性能
这个属性会提前告诉浏览器元素将要进行变化,浏览器可以提前做优化准备(比如创建新的图层)。但要注意不要过度使用,只在真正需要的元素上添加。
.list-move,
.list-enter-active,
.list-leave-active {
will-change: transform, opacity;
transition: all 0.5s ease;
}
- 避免一次性动画过多元素
const addItems = (count) => {
const batch = 5
const addBatch = (remaining) => {
const currentBatch = Math.min(remaining, batch)
for (let i = 0; i < currentBatch; i++) {
addItem()
}
if (remaining > batch) {
requestAnimationFrame(() => addBatch(remaining - batch))
}
}
addBatch(count)
}
- 使用CSS transform代替位置属性
使用 translate3d 可以触发 GPU 加速,性能会更好。
.item-move {
transform: translate3d(0, 0, 0);
}
总结
通过上面的示例,我们实践了:
- Vue3 transition-group、transition 的基础应用
- 如何封装一个简单的列表过渡组件
这里仅仅展示了 Vue 动画系统的一小部分功能。Vue 的动画系统还有很多强大的特性等待我们探索,比如:
- 自定义过渡类名
- JavaScript 钩子函数
- 初始渲染动画
- 状态过渡动画
- 动画性能优化
等等
记住,合理的动画可以:
- 帮助用户理解界面变化
- 提供更好的交互反馈
- 让应用体验更加流畅
这个示例仅作为入门参考,建议深入阅读 Vue 官方文档来学习更多动画相关的高级特性。