移动端的过渡特效 --虚拟任务栈

1. 业务需求

当一个APP的页面切换,没有动画效果时就会显得很突兀,对用户来说交互性欠佳;

如上图所示,而且在每次返回首页之后,都会重复请求商品数据,同时上一次浏览到的位置没有记录,如此即浪费网络资源,也导致用户体验不佳。

2. 明确目标

整体实现方面分为两种:

  1. 过渡动画:利用 过渡动效 实现
  2. 组件缓存:虚拟任务栈 - 数组 配合 keep-alive 中的 include 实现

2.1. 过渡动画

需要使用到 过渡动效 这个功能,它描述了两个路由之间进行过渡时的动画效果。

通过利用内置组件<Transition>,制作基于状态变化的过渡和动画。

JS 复制代码
<!-- 路由出口 -->
<router-view v-slot="{ Component }">
  <!-- 动画 -->
  <transition :name="transitionName">
    <!-- 动态组件 -->
    <component :is="Component" />
  </transition>
</router-view>

2.2. 组件缓存 + 虚拟任务栈

组件缓存: 通过内置组件<KeepAlive>实现。但是,不是所有的组件都需要被缓存。 当组件进入时,我们要缓存,当组件退出时,我们就不需要缓存了。

虚拟任务栈: 通过用 数组 来实现这一流程,当进入组件router.push()时,把组件名push,当退出组件router.back()时,把组件名pop,在通过 keep-alive 中的 include 有条件性的缓存组件名构成的数组。

JS 复制代码
<!-- 路由出口 -->
<router-view v-slot="{ Component }">
  <!-- 动画组件 -->
  <transition
    :name="transitionName"
  >
    <!-- 缓存组件-->
    <keep-alive :include="virtualTaskStack">
      <!-- 动态组件 -->
      <component
        :is="Component"
        :key="$route.fullPath"    // 同域名下的跳转。比如(动态路由 /detail/:id)      
      />
    </keep-alive>
  </transition>
</router-view>

3. 代码实现

3.1. 封装组件 transition-router-view

  1. 定义组件接收参数:
    1. 路由跳转类型:[ push, back, none ]
    2. 页面栈中必须存在至少一个页面,作为起始页。
JS 复制代码
<script>
  // 无需监听路由的各种状态(在 PC 端下)
  const NONE = 'none'
  // 路由进入
  const PUSH = 'push'
  // 路由退出
  const BACK = 'back'
  // 路由跳转的 enum
  const ROUTER_TYPE_ENUM = [NONE, PUSH, BACK]
</script>

<script setup>
  import { ref } from 'vue'
  import { useRouter } from 'vue-router'

  const router = useRouter()

  const props = defineProps({
    // 路由跳转的类型,对应 ROUTER_TYPE_ENUM
    routerType: {
      type: String,
      default: NONE,
      validator(val) {
        const result = ROUTER_TYPE_ENUM.includes(val)
        if (!result) {
          throw new Error(`你的 routerType 必须是 ${ROUTER_TYPE_ENUM.join('、')} 中的一个`)
        }
        return result
      }
    },
    // 首页的组件名称,对应任务栈中的第一个组件
    mainComponentName: {
      type: String,
      required: true
    }
  })
</script>
  1. 创建如下 template
    1. name代表 transition 组件使用的动画样式,根据路由跳转类型指定
    2. key 路由只有参数变化时,会复用组件。让组件实例不复用,强制销毁重建
js 复制代码
<template>
  <!-- 路由出口 -->
  <router-view v-slot="{ Component }">
    <!-- 动画组件 -->
    <transition :name="transitionName">
      <!-- 缓存组件 -->
      <keep-alive>
        <component
          :is="Component"
          :key="$route.fullPath"
        />
      </keep-alive>
    </transition>
  </router-view>
</template>
  1. 路由过渡动画样式
css 复制代码
 <style lang="scss" scoped>
// push页面时:新页面的进入动画
.push-enter-active {
  animation-name: push-in;
  animation-duration: 0.4s;
}
// push页面时:老页面的退出动画
.push-leave-active {
  animation-name: push-out;
  animation-duration: 0.4s;
}
// push页面时:新页面的进入动画
@keyframes push-in {
  0% {
    transform: translate(100%, 0);
  }
  100% {
    transform: translate(0, 0);
  }
}
// push页面时:老页面的退出动画
@keyframes push-out {
  0% {
    transform: translate(0, 0);
  }
  100% {
    transform: translate(-50%, 0);
  }
}

// 后退页面时:即将展示的页面动画
.back-enter-active {
  animation-name: back-in;
  animation-duration: 0.4s;
}
// 后退页面时:后退的页面执行的动画
.back-leave-active {
  animation-name: back-out;
  animation-duration: 0.4s;
}
// 后退页面时:即将展示的页面动画
@keyframes back-in {
  0% {
    width: 100%;
    transform: translate(-100%, 0);
  }
  100% {
    width: 100%;
    transform: translate(0, 0);
  }
}
// 后退页面时:后退的页面执行的动画
@keyframes back-out {
  0% {
    width: 100%;
    transform: translate(0, 0);
  }
  100% {
    width: 100%;
    transform: translate(50%, 0);
  }
}
</style>
  1. 因为 routerType 不同状态下值不同,所有我们通过全局的状态来管理:
    1. PC端 时:routerType 始终为 NONE
    2. 在移动端时:routerType 根据进入或退出的状态指定为 push 或者 back
js 复制代码
export default {
  namespaced: true,
  state: () => ({
    // 路由跳转类型
    routerType: 'none'
  }),
  mutations: {
    /**
     * 修改 routerType
     */
    changeRouterType(state, newType) {
      state.routerType = newType
    }
  }
}
js 复制代码
// 路由跳转方式
routerType: (state) => {
  // 在 PC 端下,永远为 none
  if (!isMobileTerminal.value) {
    return 'none'
  }
  return state.app.routerType
}
  1. 在每次进行 router.push() 操作前,修改 routerTypepush
  • store.commit('changeRouterType', 'push')
  1. 在执行 router.back() 操作前,修改 routerTypeback
  • store.commit('changeRouterType', 'back')

但是需要注意两点:

    1. 不要再 libs 中修改,因为 libs 为通用组件
    2. 不要再跳转到 首页 时添加该操作,因为首页为任务栈的

3.2. 处理过渡动效展示样式错误问题

  1. 使用transition提供的钩子函数,动态添加样式
js 复制代码
<script setup>
  ...
  // 处理动画状态变化
  const isAnimation = ref(false)
  const beforeEnter = () => {
    isAnimation.value = true
  }
  const afterLeave = () => {
    isAnimation.value = false
  }
</script>

<template>
  <transition :name="transitionName" 
    @before-enter="beforeEnter" 
    @after-leave="afterLeave"
    >
    <!-- 缓存组件 -->
    <keep-alive >
      <component
        :is="Component"
        :class="{ 'fixed top-0 left-0 w-screen z-50': isAnimation }"
        :key="$route.fullPath"
        />
    </keep-alive>
  </transition>
</template>

3.3. 虚拟任务栈处理

  1. 监听路由变化,根据路由跳转类型,维护跳转动画和虚拟栈数组。
  2. 如果是首页,就清空栈,只保留主页面。
  3. 为所有的路由表和组件,指定对应的 name,并保持相同。
js 复制代码
<script setup>
  // 跳转动画
  const transitionName = ref('')
  // 任务栈
  const virtualTaskStack = ref([props.mainComponentName])
  /**
    * 监听路由变化
    */
  router.beforeEach((to, from) => {
    transitionName.value = props.routerType
    if (props.routerType === PUSH) {
      // 入栈
      virtualTaskStack.value.push(to.name)
    } else if (props.routerType === BACK) {
      // 出栈
      virtualTaskStack.value.pop()
    }
    // 进入首页默认清空栈
    if (to.name === props.mainComponentName) {
      clearTask()
    }
  })
  /**
   * 清空栈
   */
  const clearTask = () => {
    virtualTaskStack.value = [props.mainComponentName]
  }

</script>

<template>
  <keep-alive :include="virtualTaskStack">
    ...
  </keep-alive>
</template>
  1. 调用组件
js 复制代码
<transiton-router-view
  :routerType="$store.getters.routerType"
   mainComponentName="home"
></transiton-router-view>

3.4. 记录页面滚动位置

  1. 使用 useuse 提供的 useScroll API 获取当前容器滚动拒绝
  2. 当被缓存组件重新激活是,利用 onActivated 周期函数实现位置滚动
js 复制代码
<script setup>
  import { useScroll } from '@vueuse/core'

  /**
 * 记录页面滚动位置
 */
  const containerTarget = ref(null)
  const { y: containerTargetScrollY } = useScroll(containerTarget)
  // 被缓存的组件再次可见,会回调 onActivated 方法
  onActivated(() => {
    if (!containerTarget.value) {
      return
    }
    containerTarget.value.scrollTop = containerTargetScrollY.value
  })
</script>

<template>
  <div ref="containerTarget" />
</template>

踩雷:给元素设置scrollTop不生效时

  • 确保在获取该元素时,元素一定要有height
  • 确保该元素的overflowauto或者scroll
相关推荐
天天进步201526 分钟前
Vue项目重构实践:如何构建可维护的企业级应用
前端·vue.js·重构
2402_8575834926 分钟前
“协同过滤技术实战”:网上书城系统的设计与实现
java·开发语言·vue.js·科技·mfc
小华同学ai29 分钟前
vue-office:Star 4.2k,款支持多种Office文件预览的Vue组件库,一站式Office文件预览方案,真心不错
前端·javascript·vue.js·开源·github·office
k093343 分钟前
vue中proxy代理配置(测试一)
前端·javascript·vue.js
IT女孩儿2 小时前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
请叫我飞哥@5 小时前
HTML5适配手机
前端·html·html5
@解忧杂货铺6 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
苹果酱05678 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
WebDeveloper20018 小时前
如何使用美国域名中心US Domain Center和WordPress创建商业网站
运维·服务器·css·网络·html
真的很上进11 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html