vue动态路由+侧边栏菜单之动态路由

发布于 2019-09-05  856 次阅读


主要思路,从后台取到菜单数据,前端根据数据生成路由配置以及菜单。

后台根据前端的用户信息返回对应权限的菜单数据,省的在前端写好路由再配置权限了。

本功能使用vuex进行统一集中式的管理,这里不过多解释。

主要步骤如下:

1.从后台取对应用户权限的菜单数据

2.遍历数据,生成父路由及其子路由组件,添加到路由表中。

3.同时菜单数据也要保存到sessionStorage中,避免每次都从后台取,而且避免URL刷新空白,后面会提到。

我后台返回的菜单数据格式如下

为了添加路由时导入组件方便,我的vue组件目录结构是和菜单URL完全对应的

首先放一下我的路由配置,里面这个constantRouterMap 是固定的路由表,在没有从接口取数据生成路由时也可访问。


import Vue from 'vue'
import Router from 'vue-router'
import Layout from '../views/layout/Layout'

const _import = require('./_import_' + process.env.NODE_ENV)
Vue.use(Router)

export const constantRouterMap = [
  {path: '/login', component: _import('admin/user/login'), hidden: true},
  {path: '/404', component: _import('404'), hidden: true},
  {
    path: '/',
    component: Layout,
    redirect: '/control',
    name: '首页',
    hidden: true,
    children: [{
      path: '/control', url: '/control', component: _import('admin/control/index'), name: 'control',
      meta: {title: '首页', isTabView: true}
    },
      {
        name: 'viewList',
        path: "/admin/viewlist/index/:id",
        meta: {title: 'title', isTabView: true},
        component: _import("admin/viewlist/index")
      }]
  },
]
export default new Router({
  //mode: 'history', //后端支持可开
  scrollBehavior: () => ({y: 0}),
  routes: constantRouterMap
})

首先定义一个action用于从接口获取菜单数据,前面说过每次获取菜单数据时都要在sessionStorage中保存一下,避免每次都要从接口请求数据


// 获取菜单
GetMenu(state) {
  return new Promise(resolve => {
    let storageMenus = sessionStorage.menus
    if (!utils.isNullOrEmpty(storageMenus)) {
      store.commit("SetMenuAndRouter", JSON.parse(storageMenus))
      resolve()
    } else {
      userApi.getMenu().then(res => {
        let menus = res.data
        store.commit("SetMenuAndRouter", menus)
        resolve()
      })
    }
  })
}

然后还需要一个mutations是用于设置菜单及路由

//设置菜单和路由
SetMenuAndRouter(state, menus) {
  //保存用户菜单
  state.menus = menus
  sessionStorage.menus = JSON.stringify(menus)
  //生成菜单URL与标题键值对
  store.commit('SetMenuTitle', menus)
  //菜单转换为路由
  let accessedRouters = []
  accessedRouters = menuToRouter(menus)
  //执行设置路由的方法
  store.commit('SET_ROUTERS', accessedRouters)
  router.addRoutes(store.getters.addRouters)
}

接下来是上面代码中调用的这个mutations:SetRouters用于生成路由

SET_ROUTERS: (state, routers) => {
  state.addRouters = routers
  state.routers = constantRouterMap.concat(routers) //将固定路由和新增路由进行合并, 成为本用户最终的全部路由信息
}

menuToRouter用于将接口返回的数据转换为路由组件,代码如下

export function menuToRouter(menus) {
  const accessedRouters = []
  for (let i in menus) {
    let parentMenu = menus[i]
    //特殊情况 viewlist
    let parentRoute = {
      //父级路由 去掉末尾的index 默认跳转到子路由index
      path: parentMenu.Url,
      children: [],
      redirect: '',
      component: Layout,
      meta: {title: parentMenu.Name},
      name: parentMenu.Id
    }
    let childRoute = []
    for (let j in parentMenu.Children) {
      let childMenu = parentMenu.Children[j]
      childMenu.Url = childMenu.Url.toLowerCase()
      //去掉开头的斜杠
      let childModulePath = childMenu.Url.substr(1, childMenu.Url.length).toLowerCase()
      let isTabView = utils.isNullOrEmpty(childMenu.IsTab) ? false : childMenu.IsTab.toString() == "1"
      //如果是ViewList的URL 不需要设置对应路由了 已经在固定路由中设置过了
      if (childMenu.Url.indexOf('viewlist') != -1) {
        continue
      } else {
        childRoute.push({
          name: childMenu.Id,
          path: childMenu.Url,
          meta: {title: childMenu.Name, isTabView: isTabView},
          component: _import(childModulePath)
        })
      }

    }
    parentRoute.children = childRoute
    accessedRouters.push(parentRoute)
  }
  accessedRouters.push({path: '*', redirect: '/404', url: '/404', hidden: true})
  return accessedRouters
}

SetMenuTitle是因为我Tab选项卡那里的需求,所以写了一个这

//保存URL与菜单标题键值对
SetMenuTitle(state, menus) {
  var menuTitle = {}
  for (var pKey in menus) {
    var pMenu = menus[pKey]
    menuTitle[pMenu.Url] = pMenu.Name
    for (var cKey in pMenu.Children) {
      var cMenu = pMenu.Children[cKey]
      menuTitle[cMenu.Url.toLowerCase()] = cMenu.Name
    }
  }
  menuTitle['/control'] = '首页'
  state.menuTitle = menuTitle
}

还有是注意添加路由是需要加载组件的,路由中的component属性,我这里是根据菜单URL导入对应的组件,主要代码是

const _import = require('../../router/_import_' + process.env.NODE_ENV)

在router文件夹下有两个文件,分别为_import_development.js以及_import_production.js

_import_development.js

module.exports = file => require('@/views/' + file + '.vue').default // vue-loader at least v13.0.0+

_import_production.js

module.exports = file => () => import('@/views/' + file + '.vue')

还有一个最重要的,使用动态路由时,当你在首页(加载路由数据的页面)以外,刷新一下会直接变空白,这是由于子路由是动态添加的,界面刷新的时候,其实我们路由里面并没有该子路由的配置,在页面刷新时候,在router.beforeEach里面去判断,如果是页面刷新且无子路由配置,就调用获取菜单并生成路由的方法,判断条件==3是因为我的固定路由表里默认是3个

import router, {constantRouterMap} from './router'
import store from './store'
import NProgress from 'nprogress' // Progress 进度条
import 'nprogress/nprogress.css' // Progress 进度条样式
import * as utils from "@/utils"

router.beforeEach((to, from, next) => {
  NProgress.start()
  if (from.name === null || to.name === null) { //页面刷新
    if ((utils.isNullOrEmpty(store.getters.permission_routers) || store.getters.permission_routers.length == 3)) {
      store.dispatch("GetMenu").then(() => {
        next({...to, replace: true})
      })
    } else {
      next()
    }
  } else {
    if (to.matched.length === 0) {  //如果未匹配到路由
      from.path ? next({path: from.path}) : next('/')   //如果上级也未匹配到路由则跳转主页面,如果上级能匹配到则转上级路由
    } else {
      next()    //如果匹配到正确跳转
    }
  }
})

最后放下完整代码

import {constantRouterMap} from '@/router/index'
import Layout from '@/views/layout/Layout'
import * as utils from '@/utils'
import store from "../index"
import router from "../../router"
import * as userApi from '@/api/user'

const _import = require('../../router/_import_' + process.env.NODE_ENV)

export function menuToRouter(menus) {
  const accessedRouters = []
  for (let i in menus) {
    let parentMenu = menus[i]
    //特殊情况 viewlist
    let parentRoute = {
      //父级路由 去掉末尾的index 默认跳转到子路由index
      path: parentMenu.Url,
      children: [],
      redirect: '',
      component: Layout,
      meta: {title: parentMenu.Name},
      name: parentMenu.Id
    }
    let childRoute = []
    for (let j in parentMenu.Children) {
      let childMenu = parentMenu.Children[j]
      childMenu.Url = childMenu.Url.toLowerCase()
      //去掉开头的斜杠
      let childModulePath = childMenu.Url.substr(1, childMenu.Url.length).toLowerCase()
      let isTabView = utils.isNullOrEmpty(childMenu.IsTab) ? false : childMenu.IsTab.toString() == "1"
      //如果是ViewList的URL 不需要设置对应路由了 已经在固定路由中设置过了
      if (childMenu.Url.indexOf('viewlist') != -1) {
        continue
      } else {
        childRoute.push({
          name: childMenu.Id,
          path: childMenu.Url,
          meta: {title: childMenu.Name, isTabView: isTabView},
          component: _import(childModulePath)
        })
      }

    }
    parentRoute.children = childRoute
    accessedRouters.push(parentRoute)
  }
  accessedRouters.push({path: '*', redirect: '/404', url: '/404', hidden: true})
  return accessedRouters
}

const permission = {
  state: {
    routers: constantRouterMap, //本用户所有的路由,包括了固定的路由和下面的addRouters
    addRouters: [], //本用户的角色赋予的新增的动态路由
    menus: [],
    menuTitle: {},//菜单URL与标题键值对 主要为Tab标题所使用
  },
  mutations: {
    SetRouters: (state, routers) => {
      state.addRouters = routers
      state.routers = constantRouterMap.concat(routers) //将固定路由和新增路由进行合并, 成为本用户最终的全部路由信息
    },
    //设置菜单和路由
    SetMenuAndRouter(state, menus) {
      //保存用户菜单
      state.menus = menus
      sessionStorage.menus = JSON.stringify(menus)
      //生成菜单URL与标题键值对
      store.commit('SetMenuTitle', menus)
      //菜单转换为路由
      let accessedRouters = []
      accessedRouters = menuToRouter(menus)
      //执行设置路由的方法
      store.commit('SetRouters', accessedRouters)
      router.addRoutes(store.getters.addRouters)
    },
    //保存URL与菜单标题键值对
    SetMenuTitle(state, menus) {
      var menuTitle = {}
      for (var pKey in menus) {
        var pMenu = menus[pKey]
        menuTitle[pMenu.Url] = pMenu.Name
        for (var cKey in pMenu.Children) {
          var cMenu = pMenu.Children[cKey]
          menuTitle[cMenu.Url.toLowerCase()] = cMenu.Name
        }
      }
      menuTitle['/control'] = '首页'
      state.menuTitle = menuTitle
    },
  },
  actions: {
    // 获取菜单
    GetMenu(state) {
      return new Promise(resolve => {
        let storageMenus = sessionStorage.menus
        if (!utils.isNullOrEmpty(storageMenus)) {
          store.commit("SetMenuAndRouter", JSON.parse(storageMenus))
          resolve()
        } else {
          userApi.getMenu().then(res => {
            let menus = res.data
            store.commit("SetMenuAndRouter", menus)
            resolve()
          })
        }
      })
    }
  }
}
export default permission

还有getter.js中代码

const getters = {
  menus: state => state.permission.menus,
  menuTitle: state => state.permission.menuTitle,
  permission_routers: state => state.permission.routers,
  addRouters: state => state.permission.addRouters,
}
export default getters

相关代码打包

[reply2down]http://down.lkcloud.top/BlogFile/dynamicRouter.zip[/reply2down]

LoneKing