动态路由(基于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
流程概叙:
菜单渲染说明
/**
* 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 动态路由