# 动态路由 **Repository Path**: wangzhaoyv/dynamic_routing ## Basic Information - **Project Name**: 动态路由 - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-04-05 - **Last Updated**: 2023-04-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ### 说明:此动态路由的实现是借助于 Ant Design Pro 方法,然后自己基于vue-admin-template模板实现的 ### 简述: 动态路由的关键就是router中的router.addRoutes()方法 vue官方文档:https://router.vuejs.org/zh/api/#router-addroutes 本项目源码地址:https://gitee.com/lovePoject/dynamic_routing.git ### 流程概叙: ![输入图片说明](https://images.gitee.com/uploads/images/2020/0627/123949_a32955b0_2168899.png "slf.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 (must set!!!) 该名称由使用(必须设置!!!) * 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,所以后台数据大致可以清楚,这也是为什么放在第一个的原因 注:实际情况根据你的路由情况而定 ### 后台数据设计 ```json "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工厂了 ### 数据拼接规则讲解 ```javascript //第一段 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} */ 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 ```javascript 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暴露出去 ```javascript const getters = { asyncRouters:state => state.async.asyncRouters } export default getters ``` ### 路由判断讲解 ```javascript 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 } ``` 到此动态路由就实现了