前言
大家好, 今天给大家分享如何在h5端拥有app那样的丝滑切换动画. 例如在切换路由的时候, 每次都是很死板, 要么直接出现, 要么直接隐藏, 例如像下面这样.
显然这样很死板, 页面切换没有做任何的动画.
并且我们每次进入一个新的页面, 如果在接口在setup下或者生命周期函数中, 每次切换都是会发起请求. 但是如果是平凡的进入一个有众多数据的页面呢, 例如商品图片列表, 每次请求大量的数据, 会造成无用的请求.
这是我们的理想效果
手机端过渡动效以及组件缓存
明确目标
1.在切换路由时, 需要发生动画并且具有过渡的效果
2.预览当前页面后跳转并返回,需要不发送请求
分析功能
其一, 需要使用vue-router 提供的过渡动效, 能够结果路由跳转进行切换动画
其二, 还需要再返回到当前例如B组件时, 将B组件进行缓存, 从而减少重复的请求
这里需要使用keep-alive 组件缓存来实现, 同样我们看一下文档说明
按照官网描述, 这里是已经将两者结合, 也就是同时包含了动态路由的动画和组件缓存
其三, 虚拟任务栈的概念
思考一下, 这里需要做区分, 我是进入这个页面, 还是返回到这个页面. 也就是 我到底是push 呢 还是back呢 因为两者需要做相反反向的动画效果, 才可以显示的连贯, 并且管理这些对应的页面栈, 例如下图描述(先进后出, 后进先出)
其四, 管理虚拟任务栈
我们需要封装一个这样的栈, 来维护组件进入和退出的流程, 对于这样的虚拟任务栈, 我们可以通过数组的方法维护,从而实现先进后出
最后可以通过keep-alive 中的include把虚拟任务栈数组进行绑定, 从而实现任务栈缓存的概念
封装router-transtion
1.defineProps接收参数
告诉组件, 我的跳转类型[push, back , none]
告诉组件, 页面栈的最后一个组件的名称, 例如 home
js
// h5端 切换动画+缓存
const props = defineProps({
// 路由跳转类型, 是进入组件还是退出组件
routerType: {
type: String,
default: 'none', // none: 不使用动画, push: 进入组件, back: 退出
validator: (val) => {
const arr = ['none', 'push', 'back']
const res = arr.includes(val)
if (!res) {
throw new Error(`你的 routerType 必须是 ${arr.join('、')} 其中之一`)
}
}
},
// 首页的组件名称, 也就是页面栈的最后一个组件名称
mainComponentName: {
type: String,
required: true
}
})
2.参照官网书写缓存组件结构
这个没什么好说的cv官网
:name="transitionName" 代表transition动画组件使用动画指定的样式
:key="$route.fullPath" 保证路由的唯一性
js
<template>
<router-view v-slot="{ Component }">
<transition :name="transitionName">
<keep-alive>
<component :is="Component" :key="$route.fullPath"> </component>
</keep-alive>
</transition>
</router-view>
</template>
3. 告诉transition执行的动画
因为可能是router.push 也可能是router.back 那么这两者的动画应该是相反的,
所以这里需要做区分, 并且是在路由前置守卫中执行, 接下来我们每次进行跳转的时候
css动画样式就是常规的左右移动50% 然后加过渡效果
js
import { useRouter } from 'vue-router'
// 跳转动画
const transitionName = ref('')
/**
* router的前置守卫
*/
router.beforeEach((to, form) => {
// 定义transition跳转动画的名称
transitionName.value = props.routerType
})
<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>
4. 设置路由出口
因为是需要全局来控制跳转的类型, 我们需要将routerType定义在pinia或者vuex中并持久化, 防止刷新丢失
pinia部分代码
js
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { User } from '@/types/user'
export const useUserStore = defineStore(
'cp-counter',
() => {
// 1. 提供用户信息
const user = ref<User>()
// 2.修改用信息的方法
const setUser = (val: User) => {
user.value = val
}
// 3.清空用户,退出后使用
const delUser = () => {
user.value = undefined
}
return { user, setUser, delUser }
},
{
// 开启持久化存储
persist: true
}
)
二级路由出口对应代码
注意, 跳转前需要往仓库设置routerType
:routerType="store.routerType" 代表当前的跳转方式
mianComponentName="home" 代表这是最底层的页面
js
<script setup lang="ts">
import { useRouterStore } from '@/stores'
import { useRouter } from 'vue-router'
const router = useRouter()
const store = useRouterStore()
// 去home页面
const gotoHome = () => {
store.changeRouterType('push')
router.push('/home')
}
// 去user页面
const gotoUser = () => {
store.changeRouterType('push')
router.push('/user')
}
</script>
<template>
<div class="layout-page">
<!-- <router-view></router-view> -->
<router-transition
:routerType="store.routerType"
mianComponentName="home"
></router-transition>
<!-- tabber -->
<van-tabbar route safe-area-inset-bottom fixed>
<van-tabbar-item @click="gotoHome">
<template #icon="{ active }">
<cp-icon
class="icon"
:name="`home-index-${active ? 'active' : 'default'}`"
></cp-icon>
</template>
页面A</van-tabbar-item
>
<van-tabbar-item @click="gotoUser">
<template #icon="{ active }">
<cp-icon
class="icon"
:name="`home-notice-${active ? 'active' : 'default'}`"
></cp-icon>
</template>
页面B</van-tabbar-item
>
</van-tabbar>
</div>
</template>
<style lang="scss" scoped>
::v-deep() {
.van-tabbar {
z-index: 100;
}
}
.icon {
font-size: 26px;
}
</style>
返回的back
js
store.changeRouterType('back')
router.back()
5. 解决切换时动画问题
此时动画是出来了 但是 查看页面会出现一半白屏的现象
想要解决, 其实也很简单, 可以审查下元素, 看一下截图, 其实就是上一层的页面盖住了即将出现的页面, 我们只需要设置定位脱离标准流即可
但是, 如果这样设置, 我们缓存的每一个组件都是固定定位的, 这显然不合理
我们只需要在动画开始前加上 在动画结束后去除,
所以需要设置一个变量来控制是否增加定位的类
这里需要使用transition提供的钩子函数实现, 具体可以看下图
这里需要用到router-transtion组件中我们使用到的路由前置守卫+动态样式
js
// 控制动画
const isAnimation = ref(false)
/**
* 动画开始
*/
const beforEnter = () => {
isAnimation.value = true
}
/**
* 动画结束
*/
const afterLeave = () => {
isAnimation.value = false
}
<transition
:name="transitionName"
@before-enter="beforEnter"
@before-leave="afterLeave"
>
<component
:is="Component"
:key="$route.fullPath"
:class="{ 'fixed top-0 left-0 w-screen z-10': isAnimation }"
></component>
</transition>
此时呢就可以实现动画的效果了, 但是不要忘了, 我们还需要处理组件的缓存, 而且是根据虚拟任务栈
keep-alive缓存
进入页面,需要把当前页面进入栈
退出页面,需要把当前页面从栈中删除
如果是首页,就清空栈,只保留mainComponentName 也就是home
1.将keep-alive 添加include属性
js
<keep-alive :include="virtualTaskStack">
2. 执行以上三步进行进栈处理
js
/**
* 清空栈
*/
const clearStack = () => {
virtualTaskStack.value = [props.mainComponentName]
}
// 跳转动画
const transitionName = ref('')
/**
* router的前置守卫
*/
router.beforeEach((to, form) => {
// 定义transition跳转动画的名称
transitionName.value = props.routerType
if (props.routerType === 'push') {
// push页面时, 将新页面的组件名称添加到虚拟任务栈的最后一个
virtualTaskStack.value.push(to.name)
} else if (props.routerType === 'back') {
// 后退页面时, 将虚拟任务栈的最后一个组件名称删除
virtualTaskStack.value.pop()
}
if (to.name === props.mainComponentName) {
// 如果是首页, 则清空栈
clearStack()
}
})
3. 将缓存的组件设置name
注意 组件的name 需要和路由中的name 保持一致!
此时我们再看一下效果 返回到页面b 的打印不会再打印, 说明缓存成功
完结~手动撒花✿✿ヽ(°▽°)ノ✿