前端必备技能:Vue3动画进阶指南,让你的页面活起来!

824 阅读4分钟

引言

还在为单调乏味的页面发愁吗?是否羡慕那些充满生命力的网页动效?作为前端开发者,如何让用户在访问网站的第一刻就被吸引?答案就是 - 动画!优雅的动画效果不仅能提升用户体验,还能让你的应用脱颖而出。但在实际开发中,你是否遇到过这些问题:

  • 动画代码重复写,维护困难
  • 不同页面动画风格不统一
  • 列表动画实现复杂
  • 动画性能优化难

别担心,本文将带你探索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>

遇到的问题

看起来不错,但我们很快就会发现一些问题:

  1. 每个需要动画的地方都要重复编写类似的CSS代码
  2. 动画效果不易统一管理
  3. 代码复用性差
  4. 无法快速切换动画方向和效果

优化方案:封装通用过渡组件

为了解决上述问题,我们可以自己去封装一个功能强大的过渡组件:

<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>

以下是使用上述组件实现的效果:

20250208204414_rec_.gif

下方是示例链接

https://play.vuejs.org/#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

升级:使用transition-group处理列表动画

<TransitionGroup> 是一个内置组件,用于对 v-for 列表中的元素或组件的插入、移除和顺序改变添加动画效果。

和 <Transition> 的区别

<TransitionGroup> 支持和 <Transition> 基本相同的 props、CSS 过渡 class 和 JavaScript 钩子监听器,但有以下几点区别:

  • 默认情况下,它不会渲染一个容器元素。但你可以通过传入 tag prop 来指定一个元素作为容器元素来渲染。
  • 过渡模式在这里不可用,因为我们不再是在互斥的元素之间进行切换。
  • 列表中的每个元素都必须有一个独一无二的 key attribute。
  • CSS 过渡 class 会被应用在列表内的元素上,而不是容器元素上。

(以上介绍来自vue的官方网站)(TransitionGroup | Vue.js)

transition-group的特点与优势

  1. 支持列表项的移动动画
  2. 自动处理列表项的定位
  3. 支持列表项的进入/离开动画
  4. 可以与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>

高级技巧与性能优化

  1. 使用will-change提升性能

这个属性会提前告诉浏览器元素将要进行变化,浏览器可以提前做优化准备(比如创建新的图层)。但要注意不要过度使用,只在真正需要的元素上添加。

.list-move,
.list-enter-active,
.list-leave-active {
  will-change: transform, opacity;
  transition: all 0.5s ease;
}

  1. 避免一次性动画过多元素

    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)
    }
  1. 使用CSS transform代替位置属性

使用 translate3d 可以触发 GPU 加速,性能会更好。


    .item-move {
      transform: translate3d(0, 0, 0);
    }

总结

通过上面的示例,我们实践了:

  1. Vue3 transition-group、transition 的基础应用
  2. 如何封装一个简单的列表过渡组件

这里仅仅展示了 Vue 动画系统的一小部分功能。Vue 的动画系统还有很多强大的特性等待我们探索,比如:

  • 自定义过渡类名
  • JavaScript 钩子函数
  • 初始渲染动画
  • 状态过渡动画
  • 动画性能优化
    等等

记住,合理的动画可以:

  • 帮助用户理解界面变化
  • 提供更好的交互反馈
  • 让应用体验更加流畅

这个示例仅作为入门参考,建议深入阅读 Vue 官方文档来学习更多动画相关的高级特性。