Vue动态路由

557 阅读4分钟

动态路由(基于vue-admin-template模板)

说明:此动态路由的实现是借助于 Ant Design Pro 方法,然后自己基于vue-admin-template模板实现的

简述:

动态路由的关键就是router中的 router.addRoutes()方法

vue官方文档:https://router.vuejs.org/zh/api/#router-addroutes

本项目源码地址:https://gitee.com/wangzhaoyv/dynamic_routing

流程概叙:

流程图.png

菜单渲染说明

/**
 * Note: sub-menu only appear when route children.length >= 1
 * 子菜单仅在路由children.length> = 1时出现
 * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
 * 详情请参考 https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
 * hidden: true                   if set true, item will not show in the sidebar(default is false)
 * 如果设置为true,则项目不会显示在边栏中(默认为false)
 * alwaysShow: true               if set true, will always show the root menu
                                 * 如果设置为true,将始终显示根菜单
 *                                if not set alwaysShow, when item has more than one children route,
                                 *如果未设置alwaysShow,则当项具有多个子路线时,
 *                                it will becomes nested mode, otherwise not show the root menu
                                 *它将变为嵌套模式,否则不显示根菜单
 * redirect: noRedirect           if set noRedirect will no redirect in the breadcrumb
                                 * 如果设置noRedirect,则不会在面包屑中重定向
 * name:'router-name'             the name is used by <keep-alive> (must set!!!)
                                  该名称由<keep-alive>使用(必须设置!!!)
 * meta : {
    roles: ['admin','editor']    control the page roles (you can set multiple roles)
                                 控制页面角色(您可以设置多个角色)
    title: 'title'               the name show in sidebar and breadcrumb (recommend set)
                                 名称显示在侧边栏和面包屑中(推荐设置)
    icon: 'svg-name'             the icon show in the sidebar
                                 侧栏中的图标显示
    breadcrumb: false            if set false, the item will hidden in breadcrumb(default is true)
                                 如果设置为false,则该项将隐藏在面包屑中(默认为true)
    activeMenu: '/example/list'  if set path, the sidebar will highlight the path you set
                                 如果设置了路径,则侧边栏将突出显示您设置的路径
  }
 */

以上一段来自vue-admin-template的路由说明,再加上基础router属性path和component,所以后台数据大致可以清楚,这也是为什么放在第一个的原因

注:实际情况根据你的路由情况而定

后台数据设计

"data": [
  {
    "id": 1,
    "parentId": 0,
    "title": "个人中心",
    "icon": "user",
    "name": "PersonalCenter",
    "path": "/",
    "type": 1,
    "permission": "",
    "sort": 0,
    "hidden": 0,
    "alwaysShow": 0,
    "redirect": "/personal",
    "component": "Layout"
  },
  {
    "id": 2,
    "parentId": 1,
    "title": "工作台",
    "icon": "",
    "name": "Personal",
    "path": "/personal",
    "type": 1,
    "permission": "",
    "sort": 1,
    "hidden": 0,
    "alwaysShow": 0,
    "redirect": "",
    "component": "PersonalComponent"
  },
  {
    "id": 3,
    "parentId": 1,
    "title": "技能点",
    "icon": "",
    "name": "Skill",
    "path": "/skill",
    "type": 1,
    "permission": "",
    "sort": 2,
    "hidden": 1,
    "alwaysShow": 0,
    "redirect": "",
    "component": "SkillListComponent"
  }
]

这是一个没有拼接前的树形结构数据,有以下属性

​ "id": 1, 这条数据的id

​ "parentId": 0, 这条数据的父级id

​ "title": "个人中心", 左侧菜单title属性

​ "icon": "user", 左侧菜单的图标

​ "name": "PersonalCenter", 原路由跳转的路由名

​ "path": "/", 原路由跳转的路由地址

​ "type": 1, 菜单类型:数据库定义1=>菜单 2=>按钮 前端可忽略

​ "permission": "", 路由权限 对应meta中的roles属性

​ "sort": 0, 路由排序,对于菜单顺序很重要

​ "hidden": 0, 同上的路由是否隐藏 0 => false 1 => true

​ "alwaysShow": 0, 同上的路由是否隐藏 0 => false 1 => true

​ "redirect": "/personal", 重定向地址

​ "component": "Layout" 组件名称/组件的地址

说明:这里有些属性是可以不要的

type : 后端属性

alwaysShow : 这个属性可以忽略,要也不影响就是了

component : 这个属性可以共用name属性,当然灵活性更高的话,就是提供"文件地址"

path : 此属性也可以省略,直接以 '/' + 父name + '/' + 子name

注:当然后台如果传过来的为直接的路由数据更好,那就不需要generator-routers.js工厂了

数据拼接规则讲解

//第一段
import {getMenuPermissionList} from '@/api/profile'
import Layout from '@/layout'

//第二段 前端组件地图
const constantRouterComponents = {
  // 基础页面 layout 必须引入
  'Layout': Layout,
  // 你需要动态引入的页面组件
  //个人中心的组件
  'PersonalComponent': () => import('@/views/personal'),
  'PersonalInfoComponent': () => import('@/views/info'),
  'PersonalUpdateComponent': () => import('@/views/update'),
  'SkillListComponent': () => import('@/views/skillIndex'),
  // 角色管理的
  'RoleListComponent': () => import('@/views/roleList'),
  // 用户管理
  'UserListComponent': () => import('@/views/userList'),
  // 文章管理组件
  'ArticleListComponent': () => import('@/views/articleList'),
  'WriteArticleComponent': () => import('@/views/writeArticle')
}

// 前端未找到页面路由(固定不用改)
const notFoundRouter = {
  path: '*', redirect: '/404', hidden: true
}


/**
 * 第三段
 * 动态生成菜单
 * @param token
 * @returns {Promise<Router>}
 */
export const generatorDynamicRouter = () => {
  return new Promise((resolve, reject) => {
    getMenuPermissionList().then(({data}) => {
      const childrenNav = []
      // 后端数据, 根级树数组,  根级 PID
      listToTree(data, childrenNav, 0)
      const routers = generator(childrenNav)
      routers.push(notFoundRouter)
      resolve(routers)
    }).catch(err => {
      reject(err)
    })
  })
}

/**
 * 第五段
 * 格式化树形结构数据 生成 vue-router 层级路由表
 * @param routerMap
 * @param parent
 * @returns {*}
 */
export const generator = (routerMap, parent) => {
  return routerMap.map(item => {
    const {title,name,path, hidden, alwaysShow, redirect, component,icon, permission} = item || {};
    const currentRouter = {
      // 如果路由设置了 path,则作为默认 path,否则 路由地址 动态拼接生成如 /dashboard/workplace
      path: path || `${parent && parent.path || ''}/${name}`,
      // 路由名称,建议唯一
      name: name,
      // 该路由对应页面的 组件 :方案1
      // component: constantRouterComponents[item.component],
      // 该路由对应页面的 组件 :方案2 (动态加载)
      component: constantRouterComponents[component || name] || (() => import(`@/views/${component}`)),
      // meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
      meta: {
        title: title,
        icon: icon || undefined,
        permission: permission
      }
    }
    // 是否设置了隐藏菜单
    if (hidden) {
      currentRouter.hidden = true
    }
    // 是否设置了隐藏子菜单
    if (alwaysShow) {
      currentRouter.alwaysShow = true
    }
    // 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠
    if (!currentRouter.path.startsWith('http')) {
      currentRouter.path = currentRouter.path.replace('//', '/')
    }
    // 重定向
    item.redirect && (currentRouter.redirect = redirect)
    // 是否有子菜单,并递归处理
    if (item.children && item.children.length > 0) {
      // Recursion
      currentRouter.children = generator(item.children, currentRouter)
    }
    return currentRouter
  })
}

/**
 * 第四段
 * 数组转树形结构
 * @param list 源数组
 * @param tree 树
 * @param parentId 父ID
 */
const listToTree = (list, tree, parentId) => {
  list.forEach(item => {
    // 判断是否为父级菜单
    if (item.parentId === parentId) {
      const child = {
        ...item,
        key: item.key || item.name,
        children: []
      }
      // 迭代 list, 找到当前菜单相符合的所有子菜单
      listToTree(list, child.children, item.id)
      // 删掉不存在 children 值的属性
      if (child.children.length <= 0) {
        delete child.children
      }
      // 加入到树中
      tree.push(child)
    }
  })
}

这里完全使用的是 Ant Design Pro的方法,只是需要按照自己的路由规则来修改,接下来我分段简单的说明一下

第一段:引入,这里引入有两个

​ 1: 基础的框架layout组件

​ 2:这个是接口获取数据的方法

第二段:组件地图

​ 1.这里的组件地图的意思,通过前面的键可以引入对应的组件(对应后台传入数据的

component属性这样就是以后添加路由就要在这里加上一个组件值)

​ \2. 这样的方法略显复杂 .如果规则订好,我们是完全可以通过 基础路径 + name 引入

第三段:获取数据,生成动态菜单

​ 1: 通过listToTree方法将获取到的后端数据转化为树形数据

​ 2: 通过generator方法格式化为路由数据

第四段:这里的第四段是listToTree这个方法,作用是将后端获取到的数据格式为路由数据

​ 1: 此处用到了递归的算法,传入参数分别为

  "list":后台获取的数据
  
  "tree": 将数据放入该对象
  
  "parentId" : 父级id

​ 2:离开的条件"再也没有该父id的数据"

第五段:这里的第五段是generator这个方法,作用是将树型数据格式化为路由数据

​ 1: 此处用到了递归的算法,传入参数分别为

​ "routerMap":树形路由数据

  "parent" : 父级的数据

​ 2: 这个数据还有没有子级数据,或者子级数据的长度为0

​ 3: 这里的path就存在我上面"后台数据设计"提到的两个可以不需要的属性方法

vuex

import {generatorDynamicRouter} from "@/router/generator-routers";

export default {
  state: {
    //动态路由地址  
    asyncRouters: []
  },
  mutations: {
    SET_ASYNC_ROUTER(state, routers) {
      state.asyncRouters = routers;
    }
  },
  actions: {
    asyncRouterList({commit}) {
      return new Promise((resolve, reject) => {
        //获取路由动态路由数据
        generatorDynamicRouter().then(routers => {
          //保存路由地址到state仓库中  
          commit("SET_ASYNC_ROUTER", routers);
          resolve(routers);
        }).catch((err) => {
          reject(err);
        })
      })
    }
  }
}

这个其实没有什么可以说的,提一句的是

actions是做异步操作:调用使用的是 store.dispatch

mutations做的是同步操作 store.commit

state数据不可直接修改state数据

然后asyncRouters数据通过getters暴露出去

const getters = { 
  asyncRouters:state => state.async.asyncRouters
}
export default getters

路由判断讲解

import router from './router'
import store from './store'
import {Notification} from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import {getToken} from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({showSpinner: false}) // NProgress Configuration

const whiteList = ['/login'] // no redirect whitelist

router.beforeEach(async (to, from, next) => {
  // start progress bar
  NProgress.start()

  // set page title
  document.title = getPageTitle(to.meta.title)

  // determine whether the user has logged in
  const hasToken = getToken()
  //判断是否有token
  if (hasToken) {
    //判断前往路径是否为login,如果有了token还是去登录就去主页
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({path: '/'})
      NProgress.done()
    } else {
      //不是去登录页  也有token 判断是否已经获取到了用户数据  
      const hasGetUserInfo = store.getters.nickname
      //有用户数据,直接过
      if (hasGetUserInfo) {
        next()
      } else {
        //没有用户数据就要去获取用户数据还有路由数据
        try {
          // 获取用户数据
          await store.dispatch('user/getInfo')
          // 获取路由数据
          await store.dispatch("asyncRouterList");
          //添加到路由中去
          router.addRoutes(store.getters.asyncRouters);
          // 请求带有 redirect 重定向时,登录自动重定向到该地址
          const redirect = decodeURIComponent(from.query.redirect || to.path)
          if (to.path === redirect) {
            // hack方法 确保addRoutes已完成 ,设置replace:true,这样导航将不会留下历史记录
            next({...to, replace: true})
          } else {
            // 跳转到目的路由
            next({path: redirect})
          }
        } catch (error) {
          // 移除token并跳转到登录页
          await store.dispatch('user/resetToken')
          Notification.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    //没有token,看看前往的是否在白名单中,因为注册也是不需要登录
    if (whiteList.indexOf(to.path) !== -1) {
      // 在白名单里就直接放行
      next()
    } else {
      // 否则重定向到登录页面
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

router.onError((error) => {
  NProgress.done()
  //路由失败时显示下失败信息
  Notification.error(error.message)
})


router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

src/layout/components/Sidebar/index.vue

routes() {
  return this.$store.getters.asyncRouters
},

到此动态路由就实现了

最后

Vue3已经发布很久了,已经提供了新版本的动态路由实现步骤与思路,并在动态上进一步完善,可以查看Vue3 动态路由