开发背景
使用管理后台系统过程中,为了优化用户体验,甲方爸爸最喜欢提出要缓存某些操作页面,以下场景相信大家不陌生也希望能实现的
场景一:多个标签页切换,热启动前标签页
sequenceDiagram
Menu->>Tab A: 菜单中点击A,创建Tab A并打开标签页Tab A,并做了A条搜索项修改操作
Menu->>Tab B: 菜单中点击B,创建Tab B并打开标签页Tab B,并做了B条搜索项修改操作
Tab B->>Tab A: 标签页Tab B在点击标签栏,跳转到标签页Tab A,仍可见A条搜索项修改操作
Tab A->>Tab B: 标签页Tab A在点击标签栏,跳转到标签页Tab B,仍可见B条搜索项修改操作
问题描述:
Tab A 与 Tab B相互切换,页面会冷启动,导致无法缓存前tab页面类似搜索项修改等操作内容。
场景二:多个标签页切换,热启动前标签页的子页面
sequenceDiagram
Menu->>Tab A: 菜单中点击A,创建Tab A并打开标签页Tab A
Tab A->>Tab A SubPage: Tab A中点击页面功能,打开子页面Tab A SubPage
Menu->>Tab B: 菜单中点击B,创建Tab B并打开标签页Tab B
Tab B->>Tab B SubPage: Tab B中点击页面功能,打开子页面Tab B SubPage
Tab B SubPage-)Tab A: 子页面Tab B SubPage在点击标签栏,跳转到标签页Tab A
Tab A-->>Tab A SubPage: 标签页Tab A识别到上次离开Tab A前是停留在Tab A SubPage,则默认打开Tab A SubPage
问题描述:
Tab A 与 Tab B相互切换,页面会冷启动,导致无法识别到上次离开Tab A前是停留在Tab A SubPage,二次打开Tab A时,不会默认打开Tab A SubPage。
场景三:多个标签页切换,热启动前子级标签页,子级返回,返回到对应父级标签页【场景二进阶版】
sequenceDiagram
Menu->>Tab A: 菜单中点击A,创建Tab A并打开标签页Tab A
Tab A->>Tab A SubPage: Tab A中点击页面功能,打开子页面Tab A SubPage
Menu->>Tab B: 菜单中点击B,创建Tab B并打开标签页Tab B
Tab B->>Tab B SubPage: Tab B中点击页面功能,打开子页面Tab B SubPage
Tab B SubPage-)Tab A: 子页面Tab B SubPage在点击标签栏,跳转到标签页Tab A
Tab A-->>Tab A SubPage: 标签页Tab A识别到上次离开Tab A前是停留在Tab A SubPage,则默认打开Tab A SubPage
Tab A SubPage-)Tab B: 子页面Tab A SubPage在点击标签栏,跳转到标签页Tab B
Tab B-->>Tab B SubPage: 标签页Tab B识别到上次离开Tab B前是停留在Tab B SubPage,则默认打开Tab B SubPage
Tab B SubPage-)Tab B: 子页面Tab B SubPage点击返回按钮,返回到标签页Tab B
Tab B-)Tab A: 标签页Tab B在点击标签栏,跳转到标签页Tab A
Tab A-->>Tab A SubPage: 标签页Tab A识别到上次离开Tab A前是停留在Tab A SubPage,则默认打开Tab A SubPage
Tab A SubPage-)Tab A: 子页面Tab A SubPage点击返回按钮,返回到标签页Tab A
问题描述:
Tab A 与 Tab B相互切换,子页面Tab B SubPage点击返回按钮,由于无法识别到Tab B SubPage的父级页面是Tab B,则返回操作后打开的页面可能会串跳到其他已打开的标签页Tab A。
解决方案
1、配置组件name属性
所有页面组件name属性,必须要与router文件定义的name对应上
页面vue文件:
export default {
name: 'MANAGEMENT',
data () {
return {}
}
}
页面对应的router文件:
[{
path: 'management',
name: 'MANAGEMENT',
component: () => import('@/views/apps/management'),
meta: {
requiresAuth: true
}
}]
2、缓存一级页面,即通过菜单直接进入的页面
<keep-alive>缓存页面的方式,早期是用v-if=$route.meta.keepAlive,但是这方法可实现缓存但有弊端,不灵活,无法动态切换目标缓存页面。后期新出了include属性,根据实际情况以字符串、正则表达式或数组方式,把目标需缓存页面的name值动态赋值给include属性,代码如下:
<template>
<keep-alive :include="aliveTabsName">
<router-view/>
</keep-alive>
</template>
<script>
export default {
name: 'APPS',
computed: {
aliveTabsName () {
return this.$store.getters['Menu/aliveTabsName']
}
},
}
</script>
补充说明:aliveTabsName是把标签栏对应页面的name值数组返回,如
aliveTabsName () {
return this.$store.getters['Menu/aliveTabsName']
}
// aliveTabsName = ['MANAGEMENT']
# 完成以上两步骤,已经能满足场景一的需求!接下来操作开始升级!
3、缓存二级或三级页面
我采取的方式是通过每个子级页面router文件的meta属性设置其父级路由相关属性
二级页面:
- 需要添加
fatherRoute,记录其父级标签页,以便二级页面与对应的tab页面关联上
三级页面:
- 需要添加
fatherRoute,记录其父级标签页,以便与对应的tab页面关联上 - 需要添加
subfatherRoute,记录其上一级页面,以便与对应的二级页面关联上
[
{
path: 'management', // tab页面
name: 'MANAGEMENT',
component: () => import('@/views/apps/management'),
meta: {
requiresAuth: true
}
},
{
path: 'record', // 二级页面
name: 'RECORD',
component: () => import('@/views/apps/record'),
meta: {
fatherRoute: 'MANAGEMENT'
}
},
{
path: 'detail/:id', // 三级页面
name: 'DETAIL',
component: () => import('@/views/apps/record/detail'),
meta: {
subfatherRoute: 'RECORD',
fatherRoute: 'MANAGEMENT'
}
}
]
# 完成以上两步骤,让子级页面与对应的上级页面关联上!
由于到目前为止,只能每个子级页面跟对应的上级页面关联上,在tab标签页切换时依然无法指向对应子级页面!所以要利用vue-router的路由守卫器afterEach,过滤有父级页面的路由,并对其做相应的routeName和fullPath记录处理跟对应的根父级标签页关联上,同时切换根父级标签页记录的currentRoute,便于标签页热启动时指向对应的子级页面路由。代码如下:
// 在main.js定义路由守卫器
// main.js
...
router.afterEach((to, from) => {
store.dispatch('Menu/addSecAlive')
})
// 通过store对菜单做相关处理
// store/menu.js
import * as types from '../../mutation-types'
import router from '../../../router'
...
const mutations = {
...
// 记录二级页面name
[types.MENUSECPAGESAVE] (state) {
let route = router.app._route
if (route.meta && route.meta.fatherRoute) {
state.aliveTabs = state.aliveTabs.map(item => {
if (item.routeName === route.meta.fatherRoute) {
item.currentRoute = route.fullPath
item.child.add(route.name + '|' + route.fullPath)
}
return item
})
}
}
}
const actions = {
...
addSecAlive ({commit}) {
commit(types.MENUSECPAGESAVE)
}
}
export default {
namespaced: true,
...
mutations,
actions
}
# 完成以上步骤,已经能满足场景二的需求!接下来操作继续升级!
3、标签页的二级或三级页面返回操作时的缓存机制
原返回方式是使用vue-router的router.go(-1),但若从A标签页的二级页面切换到B标签页的二级页面,在B标签页的二级页面使用router.go(-1)操作页面返回上一级,就会跳转到A标签页的二级页面,而非B标签页,操作流程如图:【前提:已打开Tab A中Tab A SubPage和Tab B中Tab B SubPage,问题是第5、6步描述】
sequenceDiagram
Tab A->>Tab A SubPage: 1、Tab A中点击页面功能,打开子页面Tab A SubPage
Tab B->>Tab B SubPage: 2、Tab B中点击页面功能,打开子页面Tab B SubPage
Tab B SubPage-)Tab A SubPage: 4、子页面Tab B SubPage在点击标签栏,根据缓存记录,则默认打开Tab A SubPage
Tab A SubPage-->>Tab B SubPage: 5、Tab A SubPage页面`router.go(-1)`操作返回,则会返回到Tab B SubPage,而非Tab A
Tab A SubPage-->>Tab A: 6、Tab A SubPage页面操作返回,目标会返回到Tab A
针对第5步的问题,实现第6步的目标,对缓存的标签记录做了以下处理: 在每次返回操作的事件中增加拦截处理,先处理缓存记录,再跳转到对应父级页面,代码如下:
import * as types from '../../mutation-types'
import router from '../../../router'
const state = {...}
const getters = {...}
const mutations = {
// 删除子级页面返回前tab缓存记录
[types.MENUSECPAGEREMOVE] (state, route) {
let child = new Set()
state.aliveTabs = state.aliveTabs.map(item => {
if (item.routeName === route.meta.fatherRoute) {
item.child.delete(route.name + '|' + route.fullPath)
if (!item.child.size) {
item.currentRoute = ''
} else {
child = item.child
}
}
return item
})
// 检查子级页面缓存记录里是否有对应的路由path记录
let hasPrePath = Array.from(child).find(key => key.indexOf(route.meta.subfatherRoute || route.meta.fatherRoute) > -1)
// 检查子级页面的上级路由name
let preRouteName = route.meta.subfatherRoute || route.meta.fatherRoute
// 优先上级路由path,其次上级路由name
let routePush = hasPrePath
? {
path: hasPrePath.split('|')[1]
}
: {
name: preRouteName
}
router.push(
routePush
)
}
}
const actions = {
deleteSecAlive ({commit}, route) {
commit(types.MENUSECPAGEREMOVE, route)
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
# 完成以上步骤,已经能满足场景三的需求!
4、针对tab缓存记录处理,我有话说
解决方案第一点提到的aliveTabsName,是通过map处理store/aliveTabs返回的,让标签页name与子级页面处于同级记录,而aliveTabs以对象数组的方式记录里,标签页记录和其下的子级页面的缓存记录,代码如下:
let aliveTabs = [
{
"key":"3-3",
"name":"标签页A",
"uri":"/main/tabA", // 记录标签页uri信息
"routeName":"TABA", // 记录标签页routeName信息
"child":{ // 记录标签页下子级页面信息
"_custom":{
"type":"set",
"display":"Set[0]",
"value":[],
"readOnly":true
}
}
},{
"key":"3-5",
"name":"智标签页B",
"uri":"/main/tabB",
"routeName":"TABB",
"child":{
"_custom":{
"type":"set",
"display":"Set[1]",
"value":["TABBSUB|/main/tabASub?id=1588375261592797185"],
"readOnly":true
}
},
"currentRoute":"/main/tabASub?id=1588375261592797185"
}]
// 标签页name与子级页面处于同级记录
const getters = {
aliveTabsName: state => {
let subTabs = []
let fatherTabs = state.aliveTabs.map(item => {
if (item.child && Array.from(item.child).length > 0) {
subTabs = [...subTabs, ...Array.from(item.child).map(sub => sub.split('|')[0])]
}
return item.routeName
})
return [...fatherTabs, ...subTabs]
}
}
# 以上就是我针对标签页缓存的处理方案,欢迎大家来评价讨论交流!