什么是 Lottie

Lottie 是一个由 Airbnb 开发的库,它可以在 Web、iOS、Android 和 React Native 上渲染 Adobe After Effects 动画。简单来说,设计师可以在 After Effects 中创建精美的动画,然后通过 Bodymovin 插件导出为 JSON 格式,开发者再通过 Lottie 库将这些 JSON 文件渲染成高质量的动画。

这意味着你可以在你的网站或应用中使用复杂的动画,而不需要使用 GIF 或视频,从而获得更好的性能和质量。

Lottie动画示例Lottie动画示例

为什么使用 Lottie

在传统的 Web 开发中,如果我们想要在网页中展示动画,通常有以下几种方式:

  1. GIF 图片:文件较大,质量较低,无法交互
  2. CSS 动画:适合简单动画,复杂动画难以实现
  3. JavaScript 动画库:如 GSAP,需要手动编写动画代码
  4. 视频:文件大,加载慢,不够灵活

而 Lottie 提供了一种全新的方式,它具有以下优势:

  • 文件小:JSON 格式的动画文件通常比 GIF 或视频小得多
  • 无损缩放:基于矢量的动画可以无限缩放而不失真
  • 可交互:可以控制动画的播放、暂停、速度等
  • 性能好:使用 SVG 或 Canvas 渲染,性能优异
  • 设计开发协作:设计师可以直接输出动画,开发者直接使用

基础知识准备

INFO

在开始使用 Lottie 之前,你需要了解一些基础知识:

1. 安装 lottie-web

首先,我们需要安装 lottie-web 库:

sh
# 使用 npm
npm install lottie-web
sh
# 使用 yarn
yarn add lottie-web
sh
# 使用 pnpm
pnpm add lottie-web
sh
# 使用 bun
bun add lottie-web

2. 获取 Lottie 动画文件

你可以从以下途径获取 Lottie 动画文件(JSON 格式):

  • 设计师使用 After EffectsBodymovin 插件导出
  • 使用在线资源,如 LottieFiles

LottieFiles 网站截图LottieFiles 网站截图

3. 了解 Vue3 的组合式 API

本教程将使用 Vue3 的组合式 API (Composition API) 来封装 Lottie 组件,如果你还不熟悉,可以先了解以下概念:

  • refreactive:用于创建响应式数据
  • computed:计算属性
  • watch:侦听器
  • onMountedonUnmounted:生命周期钩子
  • definePropsdefineEmits:组件属性和事件

最简单的使用方法

提示

在深入封装之前,让我们先看看最基础的使用方法,这有助于理解 Lottie 的工作原理。

1. 创建一个简单的 HTML 结构

HTML
html
<div id="lottie-container" style="width: 300px; height: 300px;"></div>

2. 使用 JS 加载动画

JavaScript
javascript
import lottie from 'lottie-web';

// 假设我们已经导入了动画数据
import animationData from './path/to/animation.json';

// 在组件挂载后
onMounted(() => {
  const animation = lottie.loadAnimation({
    container: document.getElementById('lottie-container'), // 动画容器
    renderer: 'svg', // 渲染器类型:'svg' | 'canvas' | 'html'
    loop: true, // 是否循环播放
    autoplay: true, // 是否自动播放
    animationData: animationData // 动画数据
  });
  
  // 可以通过 animation 对象控制动画
  // animation.play(); // 播放
  // animation.pause(); // 暂停
  // animation.stop(); // 停止
});

3. 在 Vue3 中的简单使用

在 Vue3 中,我们可以结合 ref 来引用 DOM 元素:

Lottie.vue
vue
<template>
  <div ref="lottieContainer" style="width: 300px; height: 300px;"></div>
</template>

<script setup>
import lottie from 'lottie-web';
import { ref, onMounted, onUnmounted } from 'vue';
import animationData from './path/to/animation.json';

const lottieContainer = ref(null);
let animation = null;

onMounted(() => {
  if (lottieContainer.value) {
    animation = lottie.loadAnimation({
      container: lottieContainer.value,
      renderer: 'svg',
      loop: true,
      autoplay: true,
      animationData: animationData
    });
  }
});

onUnmounted(() => {
  // 组件卸载时销毁动画实例,避免内存泄漏
  if (animation) {
    animation.destroy();
  }
});
</script>

NOTE

这是最基础的使用方法,但在实际项目中,我们通常需要更灵活的控制和更好的封装。接下来,我们将一步步学习如何封装一个功能完善的 Lottie 组件。

基础封装

提示

现在我们已经了解了 Lottie 的基本使用方法,接下来让我们开始封装一个简单的 Lottie 组件。封装的目的是让组件更易于复用,并且提供更好的接口供父组件调用。

1. 创建基础组件

首先,让我们创建一个名为 LottieAnimation.vue 的基础组件:

LottieAnimation.vue
vue
<template>
  <div class="lottie-container" ref="container"></div>
</template>

<script setup>
import lottie from 'lottie-web';
import { ref, onMounted, onUnmounted } from 'vue';

// 定义组件属性
const props = defineProps({
  animationData: {
    type: Object,
    required: true
  }
});

const container = ref(null);
let animation = null;

onMounted(() => {
  if (container.value && props.animationData) {
    animation = lottie.loadAnimation({
      container: container.value,
      renderer: 'svg',
      loop: true,
      autoplay: true,
      animationData: props.animationData
    });
  }
});

onUnmounted(() => {
  if (animation) {
    animation.destroy();
  }
});
</script>

<style scoped>
.lottie-container {
  width: 100%;
  height: 100%;
}
</style>

这个组件非常简单,它只接收一个必需的 animationData 属性,并在组件挂载时创建动画,在组件卸载时销毁动画。

2. 使用基础组件

现在我们可以在其他组件中使用这个基础 Lottie 组件:

Lottie.vue
vue
<template>
  <div style="width: 200px; height: 200px;">
    <LottieAnimation :animation-data="animationData" />
  </div>
</template>

<script setup>
import LottieAnimation from './LottieAnimation.vue';
import animationData from './path/to/animation.json';
</script>

不做其他配置,默认为自动播放以及循环播放的SVG动画: 在这里插入图片描述在这里插入图片描述

3. 添加基本配置选项

我们的基础组件还比较简单,让我们添加一些常用的配置选项,如循环播放、自动播放等:

LottieAnimation.vue
vue
<template>
  <div class="lottie-container" ref="container"></div>
</template>

<script setup>
import lottie from 'lottie-web';
import { ref, onMounted, onUnmounted } from 'vue';

// 定义组件属性
const props = defineProps({
  animationData: {
    type: Object,
    required: true
  },
  loop: {                // 新增loop控制是否循环播放
    type: Boolean, 
    default: true
  },
  autoplay: {           // 新增autoplay控制是否自动播放//
    type: Boolean,
    default: true
  },
  width: {
    type: [String, Number],
    default: '100%'
  },
  height: {
    type: [String, Number],
    default: '100%'
  }
});

const container = ref(null);
let animation = null;

onMounted(() => {
  if (container.value && props.animationData) {
    animation = lottie.loadAnimation({
      container: container.value,
      renderer: 'svg',
      loop: props.loop,                 // 通过prop传入的loop//
      autoplay: props.autoplay,        // 通过prop传入的autoplay//
      animationData: props.animationData
    });
  }
});

onUnmounted(() => {
  if (animation) {
    animation.destroy();
  }
});
</script>

<style scoped>
.lottie-container {
  width: v-bind('props.width');
  height: v-bind('props.height');
}
</style>

TIP

现在我们的组件可以接受更多的配置选项,使用起来更加灵活:

Lottie.vue
vue
// 父组件
<template>
  <LottieAnimation 
    :animation-data="animationData"
    :loop="false"
    :autoplay="true"
    width="300px"
    height="300px"
  />
</template>

设置loop为false,动画播放完成便会停止: 在这里插入图片描述在这里插入图片描述

4. 添加基本控制方法

添加基本控制方法

目前我们组件只能在初始化时设置一些配置,但无法在运行时控制动画。让我们添加一些基本的控制方法,如播放暂停停止

LottieAnimation.vue
vue
// LottieAnimation.vue
<template>
  <div class="lottie-container" ref="container"></div>
</template>

<script setup>
import lottie from 'lottie-web';
import { ref, onMounted, onUnmounted } from 'vue';

// 定义组件属性
const props = defineProps({
  animationData: {
    type: Object,
    required: true
  },
  loop: {
    type: Boolean,
    default: true
  },
  autoplay: {
    type: Boolean,
    default: true
  },
  width: {
    type: [String, Number],
    default: '100%'
  },
  height: {
    type: [String, Number],
    default: '100%'
  }
});

const container = ref(null);
let animation = null;

// 定义控制方法
const play = () => {
  animation?.play();        // play-----从当前帧开始播放//
};

const pause = () => {
  animation?.pause();        // pause-----暂停并保持当前帧//
};

const stop = () => {
  animation?.stop();        // stop-----停止播放并重置到第0帧//
};

// 暴露方法给父组件
defineExpose({
  play,
  pause,
  stop
});

onMounted(() => {
  if (container.value && props.animationData) {
    animation = lottie.loadAnimation({
      container: container.value,
      renderer: 'svg',
      loop: props.loop,
      autoplay: props.autoplay,
      animationData: props.animationData
    });
  }
});

onUnmounted(() => {
  if (animation) {
    animation.destroy();
  }
});
</script>

<style scoped>
.lottie-container {
  width: v-bind('props.width');
  height: v-bind('props.height');
}
</style>

NOTE

现在父组件可以通过 ref 来调用这些方法:

Lottie.vue
vue
// 父组件
<template>
  <div>
    <LottieAnimation 
      ref="lottieRef"
      :animation-data="animationData"
      :autoplay="false"
    />
    <button @click="playAnimation">播放</button>
    <button @click="pauseAnimation">暂停</button>
    <button @click="stopAnimation">停止</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import LottieAnimation from './LottieAnimation.vue';
import animationData from './path/to/animation.json';

const lottieRef = ref(null);

const playAnimation = () => {
  lottieRef.value?.play();
};

const pauseAnimation = () => {
  lottieRef.value?.pause();
};

const stopAnimation = () => {
  lottieRef.value?.stop();
};
</script>

设置自动播放为false,点击播放按钮从当前帧开始播放动画,暂停按钮会保持当前帧,再次点击播放继续从当前帧开始播放,停止按钮会终止动画重置当前帧为0在这里插入图片描述在这里插入图片描述

进阶封装

进阶封装

现在我们已经有了一个基础的 Lottie 组件,但它还缺少一些高级功能,如事件处理、动画速度控制、方向控制等。接下来,我们将逐步添加这些功能。

1. 添加事件处理

Lottie 提供了多种事件,如动画完成、循环完成等。我们可以将这些事件暴露给父组件:

LottieAnimation.vue
vue
// LottieAnimation.vue
<template>
  <div class="lottie-container" ref="container"></div>
</template>

<script setup>
import lottie from 'lottie-web';
import { ref, onMounted, onUnmounted } from 'vue';

// 定义组件属性
const props = defineProps({
  animationData: {
    type: Object,
    required: true
  },
  loop: {
    type: Boolean,
    default: true
  },
  autoplay: {
    type: Boolean,
    default: true
  },
  width: {
    type: [String, Number],
    default: '100%'
  },
  height: {
    type: [String, Number],
    default: '100%'
  }
});

// 定义事件
const emit = defineEmits([
  'complete',
  'loopComplete',
  'enterFrame',
  'segmentStart',
  'destroy'
]);

const container = ref(null);
let animation = null;

// 定义控制方法
const play = () => {
  animation?.play();
};

const pause = () => {
  animation?.pause();
};

const stop = () => {
  animation?.stop();
};

// 暴露方法给父组件
defineExpose({
  play,
  pause,
  stop
});

onMounted(() => {
  if (container.value && props.animationData) {
    animation = lottie.loadAnimation({
      container: container.value,
      renderer: 'svg',
      loop: props.loop,
      autoplay: props.autoplay,
      animationData: props.animationData
    });
    
    // 注册事件监听
    animation.addEventListener('complete', () => emit('complete'));
    animation.addEventListener('loopComplete', () => emit('loopComplete'));
    animation.addEventListener('enterFrame', (e) => emit('enterFrame', e));
    animation.addEventListener('segmentStart', (e) => emit('segmentStart', e));
    animation.addEventListener('destroy', () => emit('destroy'));
  }
});

onUnmounted(() => {
  if (animation) {
    // 移除事件监听//
    animation.removeEventListener('complete');
    animation.removeEventListener('loopComplete');
    animation.removeEventListener('enterFrame');
    animation.removeEventListener('segmentStart');
    animation.removeEventListener('destroy');
    
    // 销毁动画实例//
    animation.destroy();
    animation = null;
  }
});
</script>

<style scoped>
.lottie-container {
  width: v-bind('props.width');
  height: v-bind('props.height');
}
</style>

现在父组件可以监听这些事件:

Lottie.vue
vue
// 父组件 
<template> 
  <LottieAnimation 
    :animation-data="animationData"
    @complete="onComplete"
    @loop-complete="onLoopComplete"
  />
</template>

<script setup>
import LottieAnimation from './LottieAnimation.vue';
import animationData from './path/to/animation.json';
const onComplete = () => {
  console.log('动画播放完成');
};

const onLoopComplete = () => {
  console.log('动画循环播放完成');
};
</script>

在这里插入图片描述在这里插入图片描述

事件相关文档可翻到本文底部查看 API 参考

2. 添加速度和方向控制

添加速度和方向控制

Lottie 允许我们控制动画的播放速度和方向,让我们将这些功能添加到组件中:

LottieAnimation.vue
vue
<template>
  <div class="lottie-container" ref="container"></div>
</template>

<script setup>
import lottie from 'lottie-web';
import { ref, onMounted, onUnmounted, watch } from 'vue';

// 定义组件属性
const props = defineProps({
  animationData: {
    type: Object,
    required: true
  },
  loop: {
    type: Boolean,
    default: true
  },
  autoplay: {
    type: Boolean,
    default: true
  },
  width: {
    type: [String, Number],
    default: '100%'
  },
  height: {
    type: [String, Number],
    default: '100%'
  },
  speed: {                          // 设置动画速度//
    type: Number,
    default: 1
  },
  direction: {                      // 设置动画方向//
    type: Number,
    default: 1,
    validator: (value) => value === 1 || value === -1
  }
});

// 定义事件
const emit = defineEmits([
  'complete',
  'loopComplete',
  'enterFrame',
  'segmentStart',
  'destroy'
]);

const container = ref(null);
let animation = null;

// 定义控制方法
const play = () => {
  animation?.play();
};

const pause = () => {
  animation?.pause();
};

const stop = () => {
  animation?.stop();
};

const setSpeed = (speed) => {
  if (animation) {
    animation.setSpeed(speed);
  }
};

const setDirection = (direction) => {
  if (animation) {
    animation.setDirection(direction);
  }
};

// 暴露方法给父组件
defineExpose({
  play,
  pause,
  stop,
  setSpeed,
  setDirection
});

// 监听属性变化
watch(() => props.speed, (newSpeed) => {
  if (animation) {
    // 属性变化重新设置动画速度
    animation.setSpeed(newSpeed);
  }
});

watch(() => props.direction, (newDirection) => {
  if (animation) {
    // 属性变化重新设置动画方向
    animation.setDirection(newDirection);
  }
});

onMounted(() => {
  if (container.value && props.animationData) {
    animation = lottie.loadAnimation({
      container: container.value,
      renderer: 'svg',
      loop: props.loop,
      autoplay: props.autoplay,
      animationData: props.animationData
    });
    
    // 设置初始速度和方向
    animation.setSpeed(props.speed);
    animation.setDirection(props.direction);
    
    // 注册事件监听
    animation.addEventListener('complete', () => emit('complete'));
    animation.addEventListener('loopComplete', () => emit('loopComplete'));
    animation.addEventListener('enterFrame', (e) => emit('enterFrame', e));
    animation.addEventListener('segmentStart', (e) => emit('segmentStart', e));
    animation.addEventListener('destroy', () => emit('destroy'));
  }
});

onUnmounted(() => {
  if (animation) {
    // 移除事件监听
    animation.removeEventListener('complete');
    animation.removeEventListener('loopComplete');
    animation.removeEventListener('enterFrame');
    animation.removeEventListener('segmentStart');
    animation.removeEventListener('destroy');
    
    // 销毁动画实例
    animation.destroy();
    animation = null;
  }
});
</script>

<style scoped>
.lottie-container {
  width: v-bind('props.width');
  height: v-bind('props.height');
}
</style>

NOTE

现在通过父组件可以控制动画的速度和方向:

Lottie.vue
vue
// 父组件
<template>
  <div>
    <LottieAnimation 
      ref="lottieRef"
      :animation-data="animationData"
      :speed="speed"
      :direction="direction"
    />
    <div>
      <label>速度:</label>
      <input type="range" min="0.1" max="3" step="0.1" v-model="speed">
      <span>{{ speed }}x</span>
    </div>
    <div>
      <label>方向:</label>
      <button @click="toggleDirection">切换方向</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import LottieAnimation from './LottieAnimation.vue';
import animationData from './path/to/animation.json';

const lottieRef = ref(null);
const speed = ref(1);
const direction = ref(1);

const toggleDirection = () => {
  direction.value = direction.value === 1 ? -1 : 1;
};
</script>

在这里插入图片描述在这里插入图片描述

3. 添加帧控制和分段播放

添加帧控制和分段播放

Lottie 还允许我们控制动画的帧和分段播放,这对于复杂动画的精确控制非常有用:

LottieAnimation.vue
vue
<template>
  <div class="lottie-container" ref="container"></div>
</template>

<script setup>
import lottie from 'lottie-web';
import { ref, onMounted, onUnmounted, watch } from 'vue';

// 定义组件属性
const props = defineProps({
  animationData: {
    type: Object,
    required: true
  },
  loop: {
    type: Boolean,
    default: true
  },
  autoplay: {
    type: Boolean,
    default: true
  },
  width: {
    type: [String, Number],
    default: '100%'
  },
  height: {
    type: [String, Number],
    default: '100%'
  },
  speed: {
    type: Number,
    default: 1
  },
  direction: {
    type: Number,
    default: 1,
    validator: (value) => value === 1 || value === -1
  },
  segments: {
    type: Array,
    default: null
  }
});

// 定义事件
const emit = defineEmits([
  'complete',
  'loopComplete',
  'enterFrame',
  'segmentStart',
  'destroy'
]);

const container = ref(null);
let animation = null;

// 定义控制方法
const play = () => {
  animation?.play();
};

const pause = () => {
  animation?.pause();
};

const stop = () => {
  animation?.stop();
};

const setSpeed = (speed) => {
  if (animation) {
    animation.setSpeed(speed);
  }
};

const setDirection = (direction) => {
  if (animation) {
    animation.setDirection(direction);
  }
};

const goToAndPlay = (value, isFrame = true) => {
  if (animation) {
    animation.goToAndPlay(value, isFrame);
  }
};

const goToAndStop = (value, isFrame = true) => {
  if (animation) {
    animation.goToAndStop(value, isFrame);
  }
};

const setSegment = (startFrame, endFrame) => {
  if (animation) {
    animation.setSegment(startFrame, endFrame);
  }
};

const playSegments = (segments, forceFlag) => {
  if (animation) {
    animation.playSegments(segments, forceFlag);
  }
};

// 暴露方法给父组件
defineExpose({
  play,
  pause,
  stop,
  setSpeed,
  setDirection,
  goToAndPlay,
  goToAndStop,
  setSegment,
  playSegments
});

// 监听属性变化
watch(() => props.speed, (newSpeed) => {
  if (animation) {
    animation.setSpeed(newSpeed);
  }
});

watch(() => props.direction, (newDirection) => {
  if (animation) {
    animation.setDirection(newDirection);
  }
});

watch(() => props.segments, (newSegments) => {
  if (animation && newSegments) {
    animation.playSegments(newSegments, true);
  }
}, { deep: true });

onMounted(() => {
  if (container.value && props.animationData) {
    animation = lottie.loadAnimation({
      container: container.value,
      renderer: 'svg',
      loop: props.loop,
      autoplay: props.autoplay,
      animationData: props.animationData
    });
    
    // 设置初始速度和方向
    animation.setSpeed(props.speed);
    animation.setDirection(props.direction);
    
    // 设置初始分段
    if (props.segments) {
      animation.playSegments(props.segments, true);
    }
    
    // 注册事件监听
    animation.addEventListener('complete', () => emit('complete'));
    animation.addEventListener('loopComplete', () => emit('loopComplete'));
    animation.addEventListener('enterFrame', (e) => emit('enterFrame', e));
    animation.addEventListener('segmentStart', (e) => emit('segmentStart', e));
    animation.addEventListener('destroy', () => emit('destroy'));
  }
});

onUnmounted(() => {
  if (animation) {
    // 移除事件监听
    animation.removeEventListener('complete');
    animation.removeEventListener('loopComplete');
    animation.removeEventListener('enterFrame');
    animation.removeEventListener('segmentStart');
    animation.removeEventListener('destroy');
    
    // 销毁动画实例
    animation.destroy();
    animation = null;
  }
});
</script>

<style scoped>
.lottie-container {
  width: v-bind('props.width');
  height: v-bind('props.height');
}
</style>

INFO

现在父组件可以更精确地控制动画:

Lottie.vue
vue
// 父组件
<template>
  <div>
    <LottieAnimation 
      ref="lottieRef"
      :animation-data="animationData"
    />
    <div>
      <button @click="goToFrame(30)">跳转到第30帧开始</button>
      <button @click="playFrames(60, 80)">播放60-80帧</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import LottieAnimation from './LottieAnimation.vue';
import animationData from './path/to/animation.json';

const lottieRef = ref(null);

const goToFrame = (frame) => {
  lottieRef.value?.goToAndPlay(frame, true);
};

const playFrames = (start, end) => {
  lottieRef.value?.playSegments([start, end], true);
};
</script>

在这里插入图片描述在这里插入图片描述

INFO

到目前为止,我们已经封装了一个功能相对完善的 Lottie 组件,它可以满足大多数基本需求。在下一部分,我们将进一步完善这个组件,添加更多高级功能,并提供一个完整的 API 参考。

完整组件封装

完整组件封装

在前面的章节中,我们已经逐步构建了一个功能相对完善的 Lottie 组件。现在,让我们将所有功能整合起来,创建一个完整的 Lottie 组件,并添加一些额外的优化。

1. 添加渲染器选项

INFO

Lottie 支持多种渲染器,包括 SVGCanvasHTML。默认情况下,我们使用 SVG 渲染器,但在某些情况下,其他渲染器可能更适合。对于复杂动画,Canvas 渲染器会比 SVG 渲染器性能更好,让我们添加渲染器选项:

LottieAnimation.vue
vue
<template>
  <div class="lottie-container" ref="container" :style="containerStyle"></div>
</template>

<script setup>
import lottie from 'lottie-web';
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';

// 定义组件属性
const props = defineProps({
  animationData: {
    type: Object,
    required: true
  },
  loop: {
    type: Boolean,
    default: true
  },
  autoplay: {
    type: Boolean,
    default: true
  },
  width: {
    type: [String, Number],
    default: '100%'
  },
  height: {
    type: [String, Number],
    default: '100%'
  },
  speed: {
    type: Number,
    default: 1
  },
  direction: {
    type: Number,
    default: 1,
    validator: (value) => value === 1 || value === -1
  },
  segments: {
    type: Array,
    default: null
  },
  // 通过父组件传入renderer决定渲染器
  renderer: {
    type: String,
    default: 'svg',
    validator: (value) => ['svg', 'canvas', 'html'].includes(value)
  },
  rendererSettings: {
    type: Object,
    default: () => ({})
  },
  initialSegment: {
    type: Array,
    default: null
  }
});

// 计算样式
const containerStyle = computed(() => {
  const width = typeof props.width === 'number' ? `${props.width}px` : props.width;
  const height = typeof props.height === 'number' ? `${props.height}px` : props.height;
  
  return {
    width,
    height,
    overflow: 'hidden'
  };
});

// 定义事件
const emit = defineEmits([
  'complete',
  'loopComplete',
  'enterFrame',
  'segmentStart',
  'config_ready',
  'data_ready',
  'data_failed',
  'loaded_images',
  'DOMLoaded',
  'destroy',
  'error'
]);

const container = ref(null);
let animation = ref(null);

// 定义控制方法
const play = () => {
  animation.value?.play();
};

const pause = () => {
  animation.value?.pause();
};

const stop = () => {
  animation.value?.stop();
};

const setSpeed = (speed) => {
  if (animation.value) {
    animation.value.setSpeed(speed);
  }
};

const setDirection = (direction) => {
  if (animation.value) {
    animation.value.setDirection(direction);
  }
};

const goToAndPlay = (value, isFrame = true) => {
  if (animation.value) {
    animation.value.goToAndPlay(value, isFrame);
  }
};

const goToAndStop = (value, isFrame = true) => {
  if (animation.value) {
    animation.value.goToAndStop(value, isFrame);
  }
};

const setSegment = (startFrame, endFrame) => {
  if (animation.value) {
    animation.value.setSegment(startFrame, endFrame);
  }
};

const playSegments = (segments, forceFlag) => {
  if (animation.value) {
    animation.value.playSegments(segments, forceFlag);
  }
};

const getDuration = (inFrames = true) => {
  if (animation.value) {
    return animation.value.getDuration(inFrames);
  }
  return 0;
};

// 暴露方法给父组件
defineExpose({
  play,
  pause,
  stop,
  setSpeed,
  setDirection,
  goToAndPlay,
  goToAndStop,
  setSegment,
  playSegments,
  getDuration,
  lottieInstance: animation
});

// 初始化 Lottie 动画
const initLottie = () => {
  if (!container.value || !props.animationData) return;
  
  animation.value = lottie.loadAnimation({
    container: container.value,
    renderer: props.renderer,
    loop: props.loop,
    autoplay: props.autoplay,
    animationData: props.animationData,
    rendererSettings: props.rendererSettings,
    initialSegment: props.initialSegment
  });
  
  // 设置初始速度和方向
  animation.value.setSpeed(props.speed);
  animation.value.setDirection(props.direction);
  
  // 设置初始分段
  if (props.segments) {
    animation.value.playSegments(props.segments, true);
  }
  
  // 注册事件监听
  animation.value.addEventListener('complete', () => emit('complete'));
  animation.value.addEventListener('loopComplete', () => emit('loopComplete'));
  animation.value.addEventListener('enterFrame', (e) => emit('enterFrame', e));
  animation.value.addEventListener('segmentStart', (e) => emit('segmentStart', e));
  animation.value.addEventListener('config_ready', () => emit('config_ready'));
  animation.value.addEventListener('data_ready', () => emit('data_ready'));
  animation.value.addEventListener('data_failed', (e) => emit('data_failed', e));
  animation.value.addEventListener('loaded_images', () => emit('loaded_images'));
  animation.value.addEventListener('DOMLoaded', () => emit('DOMLoaded'));
  animation.value.addEventListener('destroy', () => emit('destroy'));
  animation.value.addEventListener('error', (e) => emit('error', e));
};

// 监听属性变化
watch(() => props.speed, (newSpeed) => {
  if (animation.value) {
    animation.value.setSpeed(newSpeed);
  }
}, { immediate: false });

watch(() => props.direction, (newDirection) => {
  if (animation.value) {
    animation.value.setDirection(newDirection);
  }
}, { immediate: false });

watch(() => props.segments, (newSegments) => {
  if (animation.value && newSegments) {
    animation.value.playSegments(newSegments, true);
  }
}, { immediate: false, deep: true });

// 监听动画数据变化,重新加载动画
watch(() => props.animationData, (newData) => {
  if (animation.value) {
    animation.value.destroy();
  }
  
  if (newData && container.value) {
    initLottie();
  }
}, { deep: true });

onMounted(() => {
  initLottie();
});

onUnmounted(() => {
  if (animation.value) {
    // 移除所有事件监听
    animation.value.removeEventListener('complete');
    animation.value.removeEventListener('loopComplete');
    animation.value.removeEventListener('enterFrame');
    animation.value.removeEventListener('segmentStart');
    animation.value.removeEventListener('config_ready');
    animation.value.removeEventListener('data_ready');
    animation.value.removeEventListener('data_failed');
    animation.value.removeEventListener('loaded_images');
    animation.value.removeEventListener('DOMLoaded');
    animation.value.removeEventListener('destroy');
    animation.value.removeEventListener('error');
    
    // 销毁动画实例
    animation.value.destroy();
    animation.value = null;
  }
});
</script>

<style scoped>
.lottie-container {
  display: inline-block;
}
</style>

至此,我们已经封装好了一个完整的,并且功能完善的Lottie组件。

3. 优化组件性能

为了进一步优化组件性能,我们可以考虑以下几点:

  1. 懒加载动画数据:对于较大的动画文件,可以考虑使用懒加载。
  2. 使用 Canvas 渲染器:对于复杂动画,Canvas 渲染器可能比 SVG 渲染器性能更好。
  3. 动态导入动画数据:使用 import() 动态导入动画数据。

NOTE

下面是一个优化示例:

LottieAnimation.vue
vue
<template>
  <div class="lottie-container" ref="container" :style="containerStyle"></div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';

// 定义组件属性
const props = defineProps({
   //--------------------------------
  // 与之前相同的属性......
  src: {
    type: String,
    default: ''
  }
});

//--------------------------------
// 与之前相同的代码......

// 动态加载动画数据
const loadAnimationData = async () => {
  if (props.src) {
    try {
      const response = await fetch(props.src);
      const data = await response.json();
      if (animation.value) {
        animation.value.destroy();
      }
      props.animationData = data;
      initLottie();
    } catch (error) {
      console.error('Failed to load animation data:', error);
      emit('error', error);
    }
  } else if (props.animationData) {
    initLottie();
  }
};

onMounted(() => {
  if (props.src) {
    loadAnimationData();
  } else {
    initLottie();
  }
});

//--------------------------------
// 与之前相同的代码......
</script>

API 参考

下面是我们封装的 Lottie 组件的完整 API 参考。

属性 (Props)

属性名类型默认值说明
animationDataObject必填Lottie 动画数据,JSON 格式
loopBooleantrue是否循环播放动画
autoplayBooleantrue是否自动播放动画
widthString/Number'100%'动画容器宽度
heightString/Number'100%'动画容器高度
speedNumber1动画播放速度,1 表示正常速度
directionNumber1动画播放方向,1 表示正向,-1 表示反向
segmentsArraynull播放特定片段,格式为 [开始帧, 结束帧]
initialSegmentArraynull初始播放片段,格式为 [开始帧, 结束帧]
rendererString'svg'渲染器类型,可选值:'svg''canvas''html'
rendererSettingsObject{}渲染器设置,详见 lottie-web 文档
srcString''动画数据的 URL,如果提供,将从该 URL 加载动画数据

事件 (Events)

事件名参数说明
complete动画播放完成时触发
loopComplete动画循环播放完成时触发
enterFrame{ currentTime, totalTime, ... }进入新帧时触发
segmentStart{ firstFrame, totalFrames, ... }片段开始播放时触发
config_ready配置就绪时触发
data_ready数据就绪时触发
data_failederror数据加载失败时触发
loaded_images图像加载完成时触发
DOMLoadedDOM 加载完成时触发
destroy动画实例销毁时触发
errorerror发生错误时触发

方法 (Methods)

方法名参数返回值说明
play播放动画
pause暂停动画
stop停止动画并回到第一帧
setSpeedspeed: Number设置动画播放速度,speed为1表示正常速度
setDirectiondirection: Number设置动画播放方向,1表示正向播放,-1表示反向播放
goToAndPlayvalue: Number, isFrame: Boolean跳转到指定帧或时间并播放,isFrame为true表示,为false表示时间,默认为false
goToAndStopvalue: Number, isFrame: Boolean跳转到指定帧或时间并停止,isFrame为true表示,为false表示时间,默认为false
setSegmentstartFrame: Number, endFrame: Number设置播放片段
playSegmentssegments: Array, forceFlag: Boolean播放指定片段,segments可以包含两个数字或者两个数字组成的数组,forceFlag表示是否立即强制播放该片段,例:
anim.playSegments([[0,10],[30,45]], true) // 立即播放0-10帧和30-45帧
getDurationinFrames: Boolean = trueNumber获取动画持续时间,单位为帧或秒

访问 Lottie 实例

如果你需要访问原始的 Lottie 实例来使用更多高级功能,可以通过 lottieInstance 属性获取:

javascript
const lottieRef = ref(null);

// 在组件挂载后
onMounted(() => {
  const lottieInstance = lottieRef.value?.lottieInstance.value;
  if (lottieInstance) {
    // 使用 lottieInstance 调用更多 lottie-web API
  }
});

在本教程中,我们从零开始,一步步构建了一个功能完善的 Vue3 Lottie 组件。我们学习了:

  1. Lottie 的基本概念和工作原理
  2. 如何在 Vue3 中使用 lottie-web
  3. 如何封装一个基础的 Lottie 组件
  4. 如何添加事件处理、控制方法和配置选项
  5. 如何优化组件性能
  6. 组件的完整 API 和常见问题解决方案

通过这个组件,你可以在 Vue3 项目中轻松地使用 Lottie 动画,为你的应用添加生动、高质量的动画效果。

希望这个教程对你有所帮助!