用keepalive如何玩转标签页缓存?

403 阅读5分钟

开发背景

使用管理后台系统过程中,为了优化用户体验,甲方爸爸最喜欢提出要缓存某些操作页面,以下场景相信大家不陌生也希望能实现的

场景一:多个标签页切换,热启动前标签页

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,过滤有父级页面的路由,并对其做相应的routeNamefullPath记录处理跟对应的根父级标签页关联上,同时切换根父级标签页记录的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-routerrouter.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]
  }
}
    
# 以上就是我针对标签页缓存的处理方案,欢迎大家来评价讨论交流!