vue-element-admin三级路由导致页面缓存失效问题

2,404 阅读4分钟

系统的路由都是由后端保存,在用户登录时返回给前端,前端根据拿到的路由数据进行动态渲染。

添加一个三级路由菜单

先看一下二级菜单的结构如下

// 拿到后端的路由数据后最终转换成如下前端路由所需的格式

import Layout from '@/views/layout/index'

const routes = {
  path: '/data-board',
  name: 'DataBoard',
  component: Layout,
  redirect: 'sale',
  meta: {
    title: '数据看板',
    icon: 'board'
  },
  children: [
    {
      path: 'sale',
      name: 'DataBoardSale',
      component: () => import('@/views/data-board/sale'),
      meta: {
        title: '销售看板'
      }
    }
  ]
}

export default routes

AppMain

<template>
  <section class="app-main">
    <keep-alive v-if="enableCache" :include="cachedViews">
      <router-view :key="key" />
    </keep-alive>
    <router-view v-else />
  </section>
</template>

根据需求,首先先配置一个三级路由,结构如下

import Layout from '@/views/layout/index'

const routes = {
  path: '/backstage',
  name: 'Backstage',
  component: Layout,
  redirect: '/backstage/sale-board/department',
  meta: {
    title: '后台管理',
    icon: 'backstage'
  },
  children: [
    {
      path: 'sale-board',
      name: 'BackstageSaleBorad',
      redirect: '/backstage/sale-board/department',
      component: () => import('@/views/backstage'),
      meta: {
        title: '销售看板'
      },
      children: [
        {
          path: 'department-sale',
          name: 'DepartmentSale',
          component: () => import('@/views/backstage/department-sale'),
          meta: {
            title: '事业部与销售部门关系'
          }
        },
        {
          path: 'department-sale-target',
          name: 'DepartmentSaleTarget',
          component: () => import('@/views/backstage/department-sale-target'),
          meta: {
            title: '事业部销售销售目标'
          }
        },
        ...
      ]
    }
  ]
}

export default routes

views/backstage下需要新建一个空白路由用作二级路由,用来承载三级路由

// views/backstage/index.vue
<template>
  <div>
    <router-view :key="key" />
  </div>
</template>

完整的目录结构

通过以上操作,三级路由就实现了,但问题也来了。。。

三级路由的页面缓存失效了,每次切换回来页面都会重新加载?

vue-element-admin里是通过vuex里的cachedViews来缓存的,每次打开一个新页面便将其push进cachedViews,配合keep-alive达到页面缓存的目的

<keep-alive v-if="enableCache" :include="cachedViews">
  <router-view :key="key" />
</keep-alive>
<router-view v-else />

但为什么三级路由的页面缓存失效了呢?查看cachedViews发现三级路由的name也没什么问题。

此时想到,我只缓存了三级路由的页面如‘事业部与销售部门关系’页面DepartmentSale,他的爸爸二级路由‘销售看板’页面BackstageSaleBorad并没有缓存,于是手动将BackstageSaleBorad添加进cachedViews,果然,这时候缓存生效了。

解决方案一:缓存三级路由的同时将其二级父路由添加到cachedViews

由上面可知,三级路由的情况下,只需在打开三级路由时把其对应的二级父路由也加进去cachedViews即可。

但此方案会同时把二级和三级路由缓存,看起来未免有些浪费,而且对代码的改动也较多,需要改动tagsView和vuex里相关的动作,所以我考虑下面的方案👇

解决方案二:将路由与菜单分隔开,菜单显示依旧使用三级结构,路由使用二级

我们知道缓存失效是因为三级结构导致的,那我们将后端返回给我们的带有三级结构的路由copy一份,将其转换为二级路由使用,而界面显示的菜单仍然用原本的三级结构渲染。

采用此方案还可以减少额外的空白路由文件。

路由转换思路:

将三级路由提升到二级路由的位置,原本的二级路由则删除。

注意转换过程中path的变化,这里我把删除的二级父路由的path拼接到其子路由的path

核心代码实现:

// 处理后端component字段
function filterAsyncRouter(asyncRouterMap) {
  const accessedRouters = asyncRouterMap.filter(item => {
    if (!item.component) return false
    // Layout组件特殊处理
    if (['layout', 'Layout'].includes(item.component)) {
      item.component = Layout
    } else {
      // component为'null'表示为二级路由,不用转换
      if (item.component !== 'null') {
        item.component = _import(item.component)
      }
    }
    if (item.children && item.children.length) {
      item.children = filterAsyncRouter(item.children)
    }
    return true
  })
  return accessedRouters
}

// 转为二级路由
function convertRoutes(accessRoutes) {
  const castRoute = (routes) => {
    let flatRoutes = []
    routes.forEach(item => {
      if (item.children && item.children.length) {
        item.children.forEach(child => {
          flatRoutes.push({
            ...child,
            path: `${item.path}/${child.path}`,
          })
        })
      } else {
        flatRoutes.push({
          ...item
        })
      }
    })
    return flatRoutes
  }

  let result = []
  accessRoutes.forEach(accessRouter => {
    let childrenRoutes = []
    if (accessRouter.children && accessRouter.children.length) {
      childrenRoutes = castRoute(accessRouter.children)
    }
    result.push({
      ...accessRouter,
      children: childrenRoutes
    })
  })
  return result
}


state: {
  permissinRoutes: [], // 路由
  appMenuList: [] // 菜单
},

mutations: {
  SET_PERMISSION_ROUTES: (state, routes) => {
    state.permissinRoutes = routes
  },

  SET_APP_MENU_LIST: (state, list) => {
    state.appMenuList = list
  },
}


actions: {
  generateRoutes({ commit, state }) {
    return new Promise((resolve, reject) => {
      let appMenuList = []
      let permissionRoutes = []
      // 服务端返回的菜单
      const serverMenuList = state.userInfo.menuList
      // 替换component字段为真实的前端组件
      const afterReplaceComponent = filterAsyncRouter(serverMenuList)
      // 三级路由=>二级路由
      permissionRoutes = convertRoutes(afterReplaceComponent)
      // 全部菜单(包含三级)
      appMenuList = [...constantRoutes, ...afterReplaceComponent]
      commit('SET_PERMISSION_ROUTES', permissionRoutes)
      commit('SET_APP_MENU_LIST', appMenuList)
      resolve(permissionRoutes)
    })
  }	
}

路由拦截里

router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken()) {
    // has token
    if (to.path === LOGIN_PATH) {
      // 如果是登录页,直接进入首页
      next({ path: '/' })
      NProgress.done() // 如果当前页是首页时不会触发afterEach hook,需要在这里结束进度条
    } else {
      if (store.getters.appMenuList.length === 0) {
        store.dispatch('generateRoutes').then(routes => {
          let arr = [...routes]
          arr.push({
            path: '*',
            redirect: '/404',
            hidden: true
          })
          // console.log('路由', arr)
          resetRouter()
          router.addRoutes(arr)
          next({ ...to, replace: true })
        }).catch(err => {
          console.error('err', err)
          Message.error('路由初始化失败')
          NProgress.done() // 如果当前页是首页时不会触发afterEach hook,需要在这里结束进度条
        })
      } else {
        next()
      }
    }
  } else {
    // has no token
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      // next(`${LOGIN_PATH}?redirect=${to.path}`) // 否则全部重定向到登录页
      next(`${LOGIN_PATH}`) // 否则全部重定向到登录页
      NProgress.done() // 如果当前页是登录页时不会触发afterEach hook,需要在这里结束进度条
    }
  }
})

router.afterEach((to) => {
  NProgress.done() // finish progress bar
  if (to.meta.title) {
    document.title = `${to.meta.title}-${projectName}`
  }
})

happy ending~~~