Skip to content

VueTransitionGroup 无依赖渐进延迟列表动画

如题,列出遇到的问题及解决办法,亦作一小结

目标效果

  • 在进入的时候有渐进延迟的过渡动画,在路由跳转的时候自也要渐进的退出
  • 且为了保证使用起来足够灵活不能使用全局路由守卫
  • 此次封装后续的为数据可视化的大量页面动画提供顺序及进度的可控能力,且需要一定程度上保证页面性能。

初步思路

VueTransitionGroup 的官方文档列出了渐进延迟列表动画的示例,引入额外的库。

我的思路:

  • 动画使用 VueTransitionGroup 绑定到元素上,进入和退出动画的播放通过控制 v-show 决定播放时间
  • 使用 onBeforeRouteLeave 组件内路由守卫控制退出动画,实现离开当前路由,自动播放
  • 将方法进行封装,传入组件列表路径数组,动画执行时使用定时器按顺序和固定间隔控制组件状态

实现过程

实现单个组件随路由自动播放退出动画

效果:

代码:

html
<template>
  <RouterLink to="/demo">to demo</RouterLink>
  <Transition appear>
    <Assets v-show="show" />
  </Transition>
</template>

<script setup>
  import { ref } from "vue";
  import { onBeforeRouteLeave } from "vue-router";
  import Assets from "./assets.vue";

  const show = ref(true);
  onBeforeRouteLeave(() => {
    return new Promise((r) => {
      show.value = false;
      setTimeout(r, 500);
    });
  });
</script>

<style lang="scss">
  .v-enter-active,
  .v-leave-active {
    position: relative;
    top: 0;
    left: 0;
    transition: all 0.5s ease;
  }

  .v-enter-from,
  .v-leave-to {
    opacity: 0;
    left: -600px;
  }
</style>

实现多组件渐进延迟动画

效果:

代码(样式部分不变):

html
<template>
  <RouterLink to="/demo">to demo</RouterLink>
  <TransitionGroup>
    <component
      v-for="({ com, show }, k) in coms"
      :key="k"
      :is="com"
      v-show="show"
    />
  </TransitionGroup>
</template>

<script setup>
  import { defineAsyncComponent, onMounted, shallowReactive } from "vue";
  import { onBeforeRouteLeave } from "vue-router";

  const R = (s) =>
    shallowReactive({
      com: defineAsyncComponent(s),
      show: false,
    });

  const coms = [
    R(() => import("./assets.vue")),
    R(() => import("./assets.vue")),
    R(() => import("./assets.vue")),
  ];

  function handle(state) {
    let num = 0;
    const len = coms.length;
    const timer = setInterval(() => {
      if (num < len) {
        coms[num].show = state;
        num += 1;
      } else {
        clearInterval(timer);
      }
    }, 200);
  }

  onMounted(() => handle(true));

  onBeforeRouteLeave(
    // 此处的延迟需注意,因为动画的时长是500毫秒,加上三次定时器各200毫秒的间隔共600毫秒,所以这里一共等待1100毫秒,少于这个数值会出现路由跳转时动画未全部完成的情况
    () => new Promise((r) => (handle(false), setTimeout(r, 1100))),
  );
</script>

问题及解决方案

打包:vite 对 import 的解析

实现相关的功能之后,对代码进行优化封装,使之后续方便调用,一开始我设想的时对 R 方法的传入参数为一个字符串数组,元素为组件的路径。

js
const list = ["./assets.vue", "./assets.vue", "./assets.vue"];

const R = (s) =>
  shallowReactive({
    com: defineAsyncComponent(() => import(s)),
    show: false,
  });

const coms = list.map(R);

开发环境下是没有问题的,但打包之后发现无法正常显示,这应该是 vite 的编译宏,只能将 list 的元素改为() => import("");

Vue 响应式进阶 API

在存储组件对应的显示状态字段时,我前前后后使用了多种方式。

我想将字段直接存储在 component 对象中,这样的话 list 转化完之后的结构就会很简单。状态字段需要响应式,这样就需要把整个 component 对象转换成响应式,是一定会出现性能问题,控制台也报了警告。

建议使用 Vue 响应式进阶 API 来实现,我多次尝试总是无法正常达到响应式渲染,此处留坑,后续再看,最后的方案我改成了最终代码中的封装方式,单独使用一个响应式对象来存放所有的状态字段。

妥协的退出动画

代码写到这里我已经实现我预期的效果的目标,但是当我尝试将多一些的组件去展示动画的时候,此处组件增加到了 5 个,对应的离开当前路由的等待时间也增加到 1500 毫秒,发现了下面的情况

当组件的数量比较多的时候,先播放完退出动画的组件会将 dom 移除,这导致后面正在播放退出动画的组件的位置的改变。

Vue 官方文档提供的解释:

<Transition> 会在一个元素或组件进入和离开 DOM 时应用动画

我表示无能为力了,遂将推出动画改为同时退出,见下方最终效果动图。

最终代码及效果

js
// sin.js
import { defineAsyncComponent, reactive } from "vue";

export default function (arr) {
  const data = {
    coms: arr.map((e) => defineAsyncComponent(e)),
    store: reactive(arr.map(() => false)),

    // 此处入参为定时器间隔
    show(time) {
      const { store } = data;
      const len = store.length;
      if (!len) return;

      // 此处的写法是为了可以在生命周期和路由守卫中直接将方法传入
      if (typeof time !== "number") time = 200;

      let num = 0;

      // 使用 Promise 可更灵活的控制动画时机和顺序
      return new Promise((r) => {
        const timer = setInterval(() => {
          if (num < len) {
            store[num] = true;
            num += 1;
          } else {
            r(clearInterval(timer));
          }
        }, time);
      });
    },

    // 此处入参为路由跳转前等待的时间
    hide(time) {
      const { store } = data;
      if (!store.length) return;

      // 此处的写法是为了可以在生命周期和路由守卫中直接将方法传入
      if (typeof time !== "number") time = 500;

      // 使用 Promise 可更灵活的控制动画时机和顺序
      return new Promise((r) => {
        for (let i in store) store[i] = false;
        const timer = setTimeout(() => r(clearTimeout(timer)), time);
      });
    },
  };
  return data;
}
html
<!-- template.vue -->
<template>
  <RouterLink to="/demo">to demo</RouterLink>
  <TransitionGroup>
    <!-- 如若需要使用 v-if 可用 template 标签多加一层包裹 -->
    <component
      v-for="(v, i) in areas.coms"
      :key="i"
      :is="v"
      v-show="areas.store[i]"
    />
  </TransitionGroup>
</template>

<script setup>
  import sin from "./sin.js";
  import { onMounted } from "vue";
  import { onBeforeRouteLeave } from "vue-router";

  const areas = sin([
    // 此处为最简有效传入方式
    () => import("./assets.vue"),
    () => import("./assets.vue"),
    () => import("./assets.vue"),
    () => import("./assets.vue"),
  ]);

  onMounted(areas.show);
  onBeforeRouteLeave(areas.hide);
</script>

<style lang="scss">
  .v-enter-active,
  .v-leave-active {
    position: relative;
    top: 0;
    left: 0;
    transition: all 0.5s ease;
  }

  .v-enter-from,
  .v-leave-to {
    opacity: 0;
    left: -600px;
  }
</style>
Release time: 11/24/2022, 3:46:00

Last updated:

⟣ Growing, with you. ⟢