目录
01: 前言
04: 前台业务下 H5 的应用场景
05: 通用组件:transition-router-view 构建方案分析 与 虚拟任务栈
过渡动画
组件缓存
小结
06: 通用组件:transition-router-view 构建方案之过渡动效
07: 通用组件:处理过渡动效展示样式错误的问题
08: 通用组件:虚拟任务栈处理
09: 通用组件:记录页面滚动位置
10: 总结
01: 前言
移动端的应用应该以什么样的形式进行展示呢?
它的展示形式如何区分浏览器端,又为什么要进行区分?
虚拟任务栈又是什么?
02: 通用组件:trigger-menu 和 trigger-menu-item 构建方案分析
这块内容充当了移动端中的 TabBar 的作用,我们期望把它封装成一个通用的组件。
接下来我们就需要来分析一下这个“TabBar”,我们把它叫做 trigger-menu 的构建方案。
我们期望将来 trigger-menu 可以以下面的形式进行使用:
<m-trigger-menu
v-if="isMobileTerminal"
class="fixed bottom-6 m-auto left-0 right-0 w-[220px]"
>
<m-trigger-menu-item
icon="home"
iconClass="fill-zinc-900 dark:fill-zinc-200"
>
首页
</m-trigger-menu-item>
……
</m-trigger-menu>
也就是说,它被分成了两个组件:trigger-menu 和 trigger-menu-item。
其中 trigger-menu 表示整个的功能区域,trigger-menu-item 表示其中的一项。
因此我们需要针对这两个组件分别进行分析:
1. trigger-menu:对于它而言,只起到一个 包裹容器 的作用,所以我们只需要提供一个对应的插槽即可。
2. trigger-menu-item:起到了对应的展示作用,展示包括了 icon 和 文字。所以内部应该存在 svg-icon 用来展示图片;存在一个插槽用来展示文字。
到这里,我们基本分析完成了这两个组件的构建方案,整体还是比较简单的。
03: 通用组件:构建 trigger-menu 和 trigger-menu-item
- src/libs
- - trigger-menu
- - - index.vue
- - trigger-menu-item
- - - index.vue
// src/libs/trigger-menu/index.vue
<template>
<div
class="min-w-[180px] bg-white dark:bg-zinc-800 rounded-full shadow
flex items-center justify-between px-2 py-1"
>
<slot />
</div>
</template>
<script setup></script>
// src/libs/trigger-menu-item/index.vue
<template>
<div
class="w-5 flex flex-col items-center justify-center col mx-0.5"
@click="onItemClick"
>
<m-svg-icon
:name="icon"
:fillClass="iconClass"
class="w-2 h-2"
></m-svg-icon>
<p class="text-sm mt-0.5" :class="textClass">
<slot />
</p>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const props = defineProps({
icon: {
type: String,
required: true
},
iconClass: {
type: String
},
textClass: {
type: String,
default: 'text-zinc-900 dark:text-zinc-200'
},
to: {
type: String
}
})
const router = useRouter()
const onItemClick = () => {
if (!props.to) {
return
}
router.push(props.to)
}
</script>
<style lang="scss" scoped></style>
使用组件:
// src/views/main/index.vue
<m-trigger-menu
v-if="isMobileTerminal"
class="fixed bottom-6 m-auto left-0 right-0 w-[220px]"
>
<m-trigger-menu-item
icon="home"
iconClass="fill-zinc-900 dark:fill-zinc-200"
>
首页
</m-trigger-menu-item>
<m-trigger-menu-item
v-if="$store.getters.token"
icon="vip"
iconClass="fill-zinc-400 dark:fill-zinc-500"
textClass="text-zinc-400 dark:text-zinc-500"
@click="onVipClick"
>
VIP
</m-trigger-menu-item>
<m-trigger-menu-item
icon="profile"
iconClass="fill-zinc-400 dark:fill-zinc-500"
textClass="text-zinc-400 dark:text-zinc-500"
@click="onMyClick"
>
{{ $store.getters.token ? '我的' : '登录' }}
</m-trigger-menu-item>
</m-trigger-menu>
04: 前台业务下 H5 的应用场景
通常情况下,我们说起移动端项目指的一般是两种:
1. 原生 APP
2. H5 网页
此时我们所做的这个移动端,指的就是 H5 网页。该内容依然是以网页为主,但是被运行到手机端之中。
H5 网页应用到手机端的时候,通常也是有两种运行的形式:
1. 直接在手机端浏览器中运行。这种使用情况相对较少。在这种情况下,用户明显的知道这就是一个网页。
2. 在原生组件 WebView 中运行(混合开发)。通常会被嵌入到 APP 之中。这种使用情况比较多,以下内容主要针对此种情况进行说明。
这种情况下,用户会认为该内容是 APP 的一部分,不会把它当成网页。而是会把它当做 原生APP。一旦用户把它作为 APP 进行衡量,就会对应用有更高的要求。
路由之间的跳转应该具备对应的动画,并且上一个页面的状态应该被缓存(页面的滚动状态和数据视图)。想要实现这样的功能,我们必须使用到之前提到过的 过渡动效。
我们期望把整个的一套移动端的跳转全部封装为一个 通用组件,期望通过这个通用组件来实现 移动端下 H5 页面的过渡功能。
05: 通用组件:transition-router-view 构建方案分析 与 虚拟任务栈
根据上一小节的分析可知,我们接下来要实现 移动端的过渡动效,以达到一个良好的移动端用户交互体验。 接下来尝试分析一下它的实现方案。
它的实现方案整体分为两种:
1. 过渡动画。
2. 组件缓存。
过渡动画
想要实现这个功能,我们需要使用到 过渡动效 这个功能,它描述了两个路由之间进行过渡时的动画效果。在这个功能的官方描述中,主要包含了三个对应的组件:
<!-- 路由出口 -->
<router-view v-slot="{ Component }">
<!-- 动画 -->
<transition name="fade">
<!-- 动态组件 -->
<component :is="Component" />
</transition>
</router-view>
使用其中的 transition 就可以实现跳转时的动画效果。
大家需要注意过渡动画分为两部分:
1. 进入动画。
2. 退出动画。
这里 transition 的 name 需要是动态的,以此来表示对应的两种动画形式。
组件缓存
因为同时我们要使用到 组件缓存,所以我们还需要依赖 keep-alive
这四个组件想要在一起工作,将要按照以下的方式进行组合:
<!-- 路由出口 -->
<router-view v-slot="{ Component }">
<!-- 动画组件 -->
<transition name="transitionName">
<!-- 缓存组件 -->
<keep-alive>
<!-- 动态组件 -->
<component :is="Component" :key="$route.fullPath" />
// 同域名下的跳转。比如(动态路由 /detail/:id)
</keep-alive>
</transition>
</router-view>
有一点大家需要注意:不是所有的组件都需要缓存。
我们把:组件的进入和退出流程,比作一个栈。
只有进入到栈中的组件才需要被缓存,就像 Android 中的 任务栈 概念一样,如下图所示:
在当前咱们移动端的组件处理中,我们同样期望有一个这样的栈来维护我们的组件进入和退出流程。我们把这样的一套流程,称作:虚拟任务栈。
对于这样的一个虚拟任务栈而言,我们可以通过 数组 进行维护。因为数组与栈的概念有相似之处,即:先进后出 的流程。
我们可以通过 keep-alive 中的 include 概念,把 虚拟任务栈 - 数组 进行绑定,从而实现 任务栈 的缓存概念。
小结
本小节我们分析了接下来要去处理的移动端页面跳转功能。想要实现这样的功能,主要分成了两大步:
1. 过渡动画:使用 过渡动效 实现。
2. 组件缓存:虚拟任务栈 - 数组 配合 keep-alive 中的 include 实现。
06: 通用组件:transition-router-view 构建方案之过渡动效
- src/libs
- - transition-router-view
- - - index.vue
// src/libs/transition-router-view/index.vue
<template>
<!-- 路由出口 -->
<router-view v-slot="{ Component }">
<!-- 动画组件 -->
<transition
:name="transitionName"
@before-enter="beforeEnter"
@after-leave="afterLeave"
>
<!-- 缓存组件 -->
<keep-alive :include="virtualTaskStack">
<component
:is="Component"
:class="{ 'fixed top-0 left-0 w-screen z-50': isAnimation }"
:key="$route.fullPath"
/>
</keep-alive>
</transition>
</router-view>
</template>
<script>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
// 无需监听路由的各种状态(在 PC 端下)
const NONE = 'none'
// 路由进入
const PUSH = 'push'
// 路由退出
const BACK = 'back'
// 路由跳转的 enum
const ROUTER_TYPE_ENUM = [NONE, PUSH, BACK]
</script>
<script setup>
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
}
})
// 任务栈
const virtualTaskStack = ref([props.mainComponentName])
const router = useRouter()
// 跳转动画
const transitionName = ref('')
/**
* 监听路由变化
*/
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 isAnimation = ref(false)
const beforeEnter = () => {
isAnimation.value = true
}
const afterLeave = () => {
isAnimation.value = false
}
/**
* 清空栈
*/
const clearTask = () => {
virtualTaskStack.value = [props.mainComponentName]
}
</script>
<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>
// src/store/modules/app.js
import { ALL_CATEGORY_ITEM } from '@/constants'
export default {
namespaced: true,
state: () => ({
// 路由跳转类型
routerType: 'none'
}),
mutations: {
/**
* 修改 routerType
*/
changeRouterType(state, newType) {
state.routerType = newType
}
}
}
// src/store/getters.js
import { isMobileTerminal } from '@/utils/flexible'
export default {
……
// 路由跳转方式
routerType: (state) => {
// 在 PC 端下,永远为 none
if (!isMobileTerminal.value) {
return 'none'
}
return state.app.routerType
}
}
// src/App.vue 使用组件
<m-transition-router-view
mainComponentName="home"
:routerType="$store.getters.routerType"
></m-transition-router-view>
// 除了 libs 中组件包含的跳转、前往首页的跳转 之外,其他的跳转进行修改
store.commit('app/changeRouterType', 'push')
router.push('/login')
store.commit('app/changeRouterType', 'back')
router.back()
07: 通用组件:处理过渡动效展示样式错误的问题
// src/libs/transition-router-view/index.vue
<template>
<transition
@before-enter="beforeEnter"
@after-leave="afterLeave"
>
<component
:class="{ 'fixed top-0 left-0 w-screen z-50': isAnimation }"
/>
</transition>
</template>
<script setup>
// 处理动画状态变化
const isAnimation = ref(false)
const beforeEnter = () => {
isAnimation.value = true
}
const afterLeave = () => {
isAnimation.value = false
}
</script>
08: 通用组件:虚拟任务栈处理
目前路由的跳转动画已经执行成功,下面来处理对应的组件缓存。
对于组件缓存而言,我们将通过 keep-alive 构建一个虚拟任务栈。
// src/libs/transition-router-view/index.vue
<template>
<keep-alive :include="virtualTaskStack">
</keep-alive>
</template>
<script setup>
// 任务栈
const virtualTaskStack = ref([props.mainComponentName])
/**
* 监听路由变化
*/
router.beforeEach((to, from) => {
……
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>
// 注意 各个单文件组件 的命名
<script>
export default {
name: 'home'
}
</script>
特殊情况处理
强制在复用的视图之间进行过渡
情况:从一个详情页跳转到另一个详情页,"/pins/:id"。 两个页面对应一个组件,缓存可能会出现问题,跳转可能也会有问题。
解决:<component :key="$route.fullPath" />
09: 通用组件:记录页面滚动位置
keep-alive 组件只能够帮助我们缓存组件,但是不能够记录页面的滚动位置。
如果我们想要记录页面滚动位置的话,需要在 通用组件外 单独处理。可以使用 useScroll 进行记录。
目前在当前应用中,我们仅需要保存 home 页面的滚动位置即可。
// src/views/main/index.vue
<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>
10: 总结
到这里咱们的整个移动端路由切换就已经全部完成了,本文章主要涉及到了两个通用组件的构建:
1. trigger-menu && trigger-menu-item
2. transition-router-view:
1. 动画效果
2. 组件缓存
3. 滚动位置缓存