{T}

概述

在实际项目中,通常需要结合第三方动画库来实现更丰富的动画效果。本章介绍如何在 Vue 2 中集成主流动画库,并提供性能优化建议。

第三方动画库对比

动画库类型特点适用场景
Animate.cssCSS 动画库开箱即用、预设丰富、无需 JS快速实现常见动画效果
GSAPJavaScript 动画库功能强大、性能优异、兼容性好复杂动画、时间轴控制
Velocity.jsJavaScript 动画库轻量级、API 类似 jQuery简单的 JS 动画需求
Mo.jsJavaScript 动画库图形动画、运动路径创意动画、图形效果

集成 Animate.css

Animate.css 是一个预设 CSS 动画库,提供了大量开箱即用的动画效果,非常适合快速集成到 Vue 项目中。

安装与配置

方式一:CDN 引入

html
<head>
  <link
    rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
  />
</head>

方式二:NPM 安装

bash
npm install animate.css
# 或
yarn add animate.css
javascript
// main.js
import 'animate.css';

基本用法

Animate.css 4.x 版本使用 animate__ 前缀:

html
<!-- 直接使用动画类 -->
<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离开过渡的开始状态类名(可选)
html
<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>

完整示例

html
<!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 引入

html
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>

方式二:NPM 安装

bash
npm install gsap
# 或
yarn add gsap
javascript
// 按需引入
import { gsap } from 'gsap';
// 或引入所有模块
import gsap from 'gsap';

JavaScript 钩子函数

Vue 的 <transition> 组件提供了 JavaScript 钩子,可在过渡的特定阶段执行代码:

html
<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>
javascript
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 的侦听器配合,实现数据状态的平滑过渡:

html
<!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)功能可创建复杂的动画序列:

javascript
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 // 交错出现
    });
  }
}

完整示例

html
<!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 硬件加速,可显著提升动画性能:

css
/* 推荐:使用 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);
}
css
/* 不推荐:直接动画会触发重排的属性 */
.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否(仅重绘)可接受

动画属性选择

css
/* 高性能动画属性(推荐) */
.good-anim {
  transition: transform 0.3s ease, opacity 0.3s ease;
}

/* 低性能动画属性(避免) */
.bad-anim {
  transition: width 0.3s, height 0.3s, margin 0.3s;
}

过渡帧率优化

css
/* 限制动画帧率以减少资源消耗 */
.optimized-anim {
  transition: transform 0.3s ease;
  /* 提升渲染层级 */
  backface-visibility: hidden;
  perspective: 1000px;
}
javascript
// 使用 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 之间
  • 缓动函数:使用自然流畅的缓动曲线
  • 频率控制:避免过于频繁的动画
css
/* 推荐的动画持续时间 */
.quick-feedback {
  transition-duration: 200ms; /* 快速反馈 */
}

.standard-transition {
  transition-duration: 300ms; /* 标准过渡 */
}

.complex-animation {
  transition-duration: 500ms; /* 复杂动画 */
}

3. 一致性

保持整个应用的动画风格统一:

css
/* 定义统一的动画变量 */
:root {
  --transition-fast: 200ms ease;
  --transition-normal: 300ms ease-out;
  --transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
}

常见问题

Q1: 动画结束后元素闪烁怎么办?

问题原因:动画类名被移除后,样式突然变化。

解决方案:确保元素的最终状态与动画结束状态一致。

css
/* 错误示例 */
.fade-enter {
  opacity: 0;
}
.fade-enter-to {
  opacity: 1;
}

/* 正确示例:设置元素的默认样式 */
.fade-element {
  opacity: 1; /* 与 enter-to 状态一致 */
}

Q2: 列表动画中元素错位?

问题原因:缺少 key 属性或使用了索引作为 key。

解决方案:为每个列表项提供唯一且稳定的 key

html
<!-- 错误:使用索引作为 key -->
<div v-for="(item, index) in items" :key="index">

<!-- 正确:使用唯一 ID -->
<div v-for="item in items" :key="item.id">

Q3: 如何调试过渡动画?

方案:使用浏览器开发者工具。

  1. 打开开发者工具(F12)
  2. 切换到 Elements 面板
  3. 选中目标元素
  4. 在 Styles 面板中查看应用的类名
  5. 使用 Animations 面板录制和分析动画
javascript
// Vue Devtools 中查看过渡状态
mounted() {
  console.log(this.$refs.transitionElement);
}

Q4: 过渡钩子中的 done 回调什么时候调用?

回答:当使用 JavaScript 过渡时,enterleave 钩子必须调用 done 回调,否则过渡永远不会结束。

javascript
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 动画不生效?

可能原因

  1. 类名前缀问题:Animate.css 4.x 需要 animate__ 前缀
html
<!-- 错误 -->
<div class="animated bounce">

<!-- 正确 -->
<div class="animate__animated animate__bounce">
  1. CSS 文件未正确引入:检查网络面板确认 CSS 文件已加载

  2. 元素被隐藏:确保元素可见且 display 不为 none

Q6: GSAP 动画如何与 Vue 响应式数据配合?

方案:使用 Vue 的侦听器或计算属性。

javascript
data() {
  return {
    targetValue: 0,
    animatedValue: 0  // 用于动画的中间值
  }
},
watch: {
  targetValue(newVal) {
    // 当目标值变化时,平滑过渡 animatedValue
    gsap.to(this.$data, {
      animatedValue: newVal,
      duration: 0.5,
      ease: 'power2.out'
    });
  }
}