zoukankan      html  css  js  c++  java
  • SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(六):使用 vue-router 进行动态加载菜单

    SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(六):使用 vue-router 进行动态加载菜单


    前提:

    (1) 相关博文地址:

    SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(一):搭建基本环境:https://www.cnblogs.com/l-y-h/p/12930895.html
    SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(二):引入 element-ui 定义基本页面显示:https://www.cnblogs.com/l-y-h/p/12935300.html
    SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(三):引入 js-cookie、axios、mock 封装请求处理以及返回结果:https://www.cnblogs.com/l-y-h/p/12955001.html
    SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(四):引入 vuex 进行状态管理、引入 vue-i18n 进行国际化管理:https://www.cnblogs.com/l-y-h/p/12963576.html
    SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(五):引入 vue-router 进行路由管理、模块化封装 axios 请求、使用 iframe 标签嵌套页面:https://www.cnblogs.com/l-y-h/p/12973364.html

    (2)代码地址:

    https://github.com/lyh-man/admin-vue-template.git

    一、获取动态菜单数据

    1、介绍

    (1)为什么使用动态加载菜单?
      在之前的 router 中,菜单的显示都是写死的。
      而实际业务需求中,可能需要动态的从后台获取菜单数据并显示,这就需要动态加载菜单了。
    比如:
      需要根据用户权限,去展现不同的菜单选项。

    (2)项目中如何使用?
      项目中使用时,各个 vue 组件事先定义好,并存放在固定位置。
      用户登录成功后,根据后台返回的菜单数据,动态拼接路由信息,并显示相应的菜单项(不同权限用户显示不同菜单)。
      点击菜单项,会连接到相应的 vue 组件,跳转路由并显示。

    2、使用 mock 模拟返回 动态菜单数据

    (1)Step1:
      封装一个 菜单请求模块,并引入。
      之前博客中已介绍使用了 模块化封装 axios 请求,这里新建一个 menu.js 用来处理菜单相关的请求。

    复制代码
    import http from '@/http/httpRequest.js'
    
    export function getMenus() {
        return http({
            url: '/menu/getMenus',
            method: 'get'
        })
    }
    复制代码

     引入 封装好的模块。

    (2)Step2:
      使用 mock 模拟菜单数据。
      封装一个 menu.js 并引入。

    复制代码
    import Mock from 'mockjs'
    
    // 登录
    export function getMenus() {
        return {
            // isOpen: false,
            url: 'api/menu/getMenus',
            type: 'get',
            data: {
                'code': 200,
                'msg': 'success',
                'data': menuList
            }
        }
    }
    
    /*
        menuId       表示当前菜单项 ID
        parentId     表示父菜单项 ID
        name_zh      表示菜单名(中文)
        name_en      表示菜单名(英语)
        url          表示路由跳转路径(自身模块 或者 外部 url)
        type         表示当前菜单项的级别(0: 目录,1: 菜单项,2: 按钮)
        icon         表示当前菜单项的图标
        orderNum     表示当前菜单项的顺序
        subMenuList  表示当前菜单项的子菜单
    */
    var menuList = [{
            'menuId': 1,
            'parentId': 0,
            'name_zh': '系统管理',
            'name_en': 'System Control',
            'url': '',
            'type': 0,
            'icon': 'el-icon-setting',
            'orderNum': 0,
            'subMenuList': [{
                    'menuId': 2,
                    'parentId': 1,
                    'name_zh': '管理员列表',
                    'name_en': 'Administrator List',
                    'url': 'sys/UserList',
                    'type': 1,
                    'icon': 'el-icon-user',
                    'orderNum': 1,
                    'subMenuList': []
                },
                {
                    'menuId': 3,
                    'parentId': 1,
                    'name_zh': '角色管理',
                    'name_en': 'Role Control',
                    'url': 'sys/RoleControl',
                    'type': 1,
                    'icon': 'el-icon-price-tag',
                    'orderNum': 2,
                    'subMenuList': []
                },
                {
                    'menuId': 4,
                    'parentId': 1,
                    'name_zh': '菜单管理',
                    'name_en': 'Menu Control',
                    'url': 'sys/MenuControl',
                    'type': 1,
                    'icon': 'el-icon-menu',
                    'orderNum': 3,
                    'subMenuList': []
                }
            ]
        },
        {
            'menuId': 5,
            'parentId': 0,
            'name_zh': '帮助',
            'name_en': 'Help',
            'url': '',
            'type': 0,
            'icon': 'el-icon-info',
            'orderNum': 1,
            'subMenuList': [{
                'menuId': 6,
                'parentId': 5,
                'name_zh': '百度',
                'name_en': 'Baidu',
                'url': 'https://www.baidu.com/',
                'type': 1,
                'icon': 'el-icon-menu',
                'orderNum': 1,
                'subMenuList': []
            }, {
                'menuId': 7,
                'parentId': 5,
                'name_zh': '博客',
                'name_en': 'Blog',
                'url': 'https://www.cnblogs.com/l-y-h/',
                'type': 1,
                'icon': 'el-icon-menu',
                'orderNum': 1,
                'subMenuList': []
            }]
        }
    ]
    复制代码

     在 index.js 中声明,并开启 mock 拦截。

    import * as menu from './modules/menu.js'
    fnCreate(menu, true)

    (3)使用
      使用时,通过如下代码可以访问到 mock。
      this.$http 是封装的 axios 请求(详情可见:https://www.cnblogs.com/l-y-h/p/12973364.html)

    // 获取动态菜单
    this.$http.menu.getMenus().then(response => {
        console.log(response)
    })

    (4)简单测试一下
      在 Login.vue 的登录方法中,简单测试一下。

    复制代码
    dataFormSubmit() {
        this.$http.login.getToken().then(response => {
            // 获取动态菜单
            this.$http.menu.getMenus().then(response => {
                console.log(response)
            })
            this.$message({
                message: this.$t("login.signInSuccess"),
                type: 'success'
            })
            // 保存 token
            setToken(response.data.token)
            this.updateName(this.dataForm.userName)
            console.log(response)
            this.$router.push({
                name: 'Home'
            })
        })
    }
    复制代码

    (5)测试结果

    3、动态菜单数据国际化问题

    (1)问题
      动态菜单数据国际化问题,数据都是保存在数据库中,中文系统下返回英文数据、或者 英文系统下返回中文数据,感觉都有些别扭,所以需要进行国际化处理,某语言系统返回该语言的数据。

    (2)解决
    思路一:
      从数据库下手,给表维护字段,比如:name-zh、name-en,使用时,根据语言选择对应的字段显示即可。修改时修改语言对应的字段即可,简单明了。
      当然这个缺点也很明显,增加了额外的字段,如果还有其他语言,那么会增加很多额外的字段。

    思路二:
      也从数据库下手,额外增加一个语言数据表,里面维护了各种语言下对应的数据。使用时,从这张表查对应的数据即可。修改时也只要修改这张表对应的字段即可。
      缺点也有,毕竟多维护了一张数据表。

    思路三:
      数据库不做国际化处理,由前端或者后端去做,数据库维护的是 国际化的变量名。即从数据库取出数据后,根据该数据返回国际化数据。
      缺点嘛,国际化数据都写死了,无法直接修改。需要修改国际化的配置文件。

    (3)项目中使用
      在这个项目中,我采用第一种方式,此项目就是一个练手的项目,国际化也就 中英文切换,增加一个额外的字段影响不大。所以 上面 mock 数据中,我使用了 name_zh 表示中文、name_en 表示英语。
      当然,有更好的方式,还望大家不吝赐教。

    4、修改 router 获取动态菜单数据

    (1)思路:
    Step1:首先确定获取动态菜单数据的时机。
      一般在登录成功跳转到主页面时,获取动态菜单数据。
      由于涉及到路由的跳转(登录页面 -> 主页面),所以可以使用 beforeEach 进行路由守卫操作。
      但由于每次路由跳转均会触发 beforeEach ,所以为了防止频繁获取动态路由值,可以使用一个全局的布尔型变量去控制,比如 false 表示未获取动态路由,获取动态路由后,将其置为 true,每次路由跳转时,判断该值是否为 true,为 false 就重新获取动态菜单数据并处理。

    Step2:其次,处理动态菜单数据,将其变化成 route 的形式。
      route 格式一般如下:
    其中:
      path:指路由路径(可以根据路径定位路由)。
      name:指路由名(可以根据路由名定位路由)。
      component:指路由所在的组件。
      children:指的是路由组件中嵌套的子路由。
      meta:用于定义路由的一些元信息。

    复制代码
    route = {
        path: '',
        name: '',
        component: null,
        children: [],
        meta: {}
    }
    复制代码

    简单举例:
      如下菜单数据转换成 route:
        url 可以转换为 path、name,用于定位路由。
        url 也用于定位 component 组件。
        subMenuList 可以转换为 children,用于设置子路由(当然子路由也进行同样处理)。
        其余的属性,可以放在 meta 中。

    复制代码
    【动态菜单数据:】
    [{
            'menuId': 1,
            'parentId': 0,
            'name_zh': '系统管理',
            'name_en': 'System Control',
            'url': '',
            'type': 0,
            'icon': 'el-icon-setting',
            'orderNum': 0,
            'subMenuList': [{
                    'menuId': 2,
                    'parentId': 1,
                    'name_zh': '管理员列表',
                    'name_en': 'Administrator List',
                    'url': 'sys/UserList',
                    'type': 1,
                    'icon': 'el-icon-user',
                    'orderNum': 1,
                    'subMenuList': []
                }
            ]
        }
    ]
    
    【转换后:】
    [
        path: '',
        name: '',
        componment: null,
        children: [{
            path: 'sys-UserList',
            name: 'sys-UserList',
            componment: () => import(/sys/UserList.vue),
            meta: {
                'menuId': 2,
                'parentId': 1,
                'type': 1,
                'icon': 'el-icon-user',
                'orderNum': 1,
                'name_zh': '管理员列表',
                'name_en': 'Administrator List',
            }
        ]],
        meta: {
            'menuId': 1,
            'parentId': 0,
            'type': 0,
            'icon': 'el-icon-setting',
            'orderNum': 0,
            'name_zh': '系统管理',
            'name_en': 'System Control',
        }
    ]
    复制代码

    Step3:使用 addRoute 方法,添加动态路由到主路由上。
      对于转换好的路由数据(route),可以使用 addRoute 将其添加到主路由上。
    注(有个小坑,简单说明一下,后续会演示):
      此处若直接使用 addRoute 添加路由,通过 路由实例的 options 方法会无法看到新添加的路由信息,也即无法通过 路由实例去获取动态添加的路由数据。
      若想看到新添加的路由信息,可以指定一个路由实例上的路由,并在其 children 中绑定动态路由,然后通过 addRoute 添加该路由,数据才会被显示。

    (2)代码实现 -- 获取动态菜单数据:
    Step1:确定获取动态菜单数据的时机。
      时机:一般在登录成功并进入主页面时,获取动态菜单并显示。
      由于涉及到路由跳转(登录页面 -> 主页面),所以可以使用 beforeEach 定义一个全局守卫,当从登录页面跳转到主页面时可以触发获取数据的操作。
      当然,有其他方式触发获取数据的操作亦可。

    Step2:定义一个全局变量,用于控制是否获取过动态菜单数据。
      由于在 beforeEach 内部定义路由,每次路由跳转均会触发此操作,为了防止频繁获取动态菜单,可以定义一个全局布尔变量来控制是否已经获取过动态菜单。
      可以在 router 实例中 添加一个变量用于控制(isAddDynamicMenuRoutes)。
      通过 router.options.isAddDynamicMenuRoutes 可以获取、修改该值。

    复制代码
    // 创建一个 router 实例
    const router = new VueRouter({
        // routes 用于定义 路由跳转 规则
        routes,
        // mode 用于去除地址中的 #
        mode: 'history',
        // scrollBehavior 用于定义路由切换时,页面滚动。
        scrollBehavior: () => ({
            y: 0
        }),
        // isAddDynamicMenuRoutes 表示是否已经添加过动态菜单(防止频繁请求动态菜单)
        isAddDynamicMenuRoutes: false
    })
    复制代码

    Step3:获取动态数据。
      由于之前对主页面的路由跳转,已经定义过一个 beforeEach,用于验证 token 是否存在。
      从登录页面跳转到主页面会触发该 beforeEach,且登录后存在 token,所以可以直接复用。

    复制代码
    import http from '@/http/http.js'
    
    router.beforeEach((to, from, next) => {
        // 当开启导航守卫时,验证 token 是否存在。
        // to.meta.isRouter 表示是否开启动态路由
        if (to.meta.isRouter) {
            // console.log(router)
            // 获取 token 值
            let token = getToken()
            // token 不存在时,跳转到 登录页面
            if (!token || !/S/.test(token)) {
                next({
                    name: 'Login'
                })
            }
            // token 存在时,判断是否已经获取过 动态菜单,未获取,即 false 时,需要获取
            if (!router.options.isAddDynamicMenuRoutes) {
                    http.menu.getMenus().then(response => {
                        console.log(response)
                    })
            }
        }
    }
    复制代码

    当然,需要对代码的逻辑进行一些修改(填坑记录 =_= )。

    填坑:
      页面刷新时,动态路由跳转不正确。
      之前为了验证 token,增加了一个 isRouter 属性值,用于给指定路由增加判断 token 的逻辑。
      对于动态路由,页面刷新后,路由会重新创建,即此时动态路由并不存在,也即 isRouter 不存在,从而 if 中的逻辑并不会触发,动态路由并不会被创建,从而页面跳转失败。
      所以在 if 判断中,增加了一个条件 isDynamicRoutes,用于判断是否为动态路由,因为动态路由只存在于主页面中,所以其与 isRouter 的作用(为主页面增加 token 验证逻辑)不矛盾。

      isDynamicRoutes 可以写在公用的 validate.js 中。使用时需要将其引入。
      由于此项目动态路由路径中均包含 DynamicRoutes,所以以此进行正则表达式判断。

    复制代码
    /**
     * 判断是否为 动态路由
     * @param {*} s
     */
    export function isDynamicRoutes(s) {
        return /DynamicRoutes/.test(s)
    }
    复制代码

    复制代码
    import { isDynamicRoutes } from '@/utils/validate.js'
    
    // 添加全局路由导航守卫
    router.beforeEach((to, from, next) => {
        // 当开启导航守卫时,验证 token 是否存在。
        // to.meta.isRouter 表示是否开启动态路由
        // isDynamicRoutes 判断该路由是否为动态路由(页面刷新时,动态路由没有 isRouter 值,此时 to.meta.isRouter 条件不成立,即动态路由拼接逻辑不能执行)
        if (to.meta.isRouter || isDynamicRoutes(to.path)) {
            // 获取 token 值
            let token = getToken()
            // token 不存在时,跳转到 登录页面
            if (!token || !/S/.test(token)) {
                next({
                    name: 'Login'
                })
            }
            // token 存在时,判断是否已经获取过 动态菜单,未获取,即 false 时,需要获取
            if (!router.options.isAddDynamicMenuRoutes) {
                    http.menu.getMenus().then(response => {
                        console.log(response)
                    })
            }
        }
    }
    复制代码

    (3)代码实现 -- 处理动态菜单数据
    Step1:
      设置全局布尔变量为 true,上面已经定义了一个 isAddDynamicMenuRoutes 变量,当获取动态菜单成功后,将其值置为 true(表示获取动态菜单成功),用于防止频繁获取菜单。
      然后,再去处理动态菜单数据,此处将处理逻辑抽成一个方法 fnAddDynamicMenuRoutes。

    复制代码
    import { isDynamicRoutes } from '
    
    // 添加全局路由导航守卫
    router.beforeEach((to, from, next) => {
        // 当开启导航守卫时,验证 token 是否存在。
        // to.meta.isRouter 表示是否开启动态路由
        // isDynamicRoutes 判断该路由是否为动态路由(页面刷新时,动态路由没有 isRouter 值,此时 to.meta.isRouter 条件不成立,即动态路由拼接逻辑不能执行)
        if (to.meta.isRouter || isDynamicRoutes(to.path)) {
            // 获取 token 值
            let token = getToken()
            // token 不存在时,跳转到 登录页面
            if (!token || !/S/.test(token)) {
                next({
                    name: 'Login'
                })
            }
            // token 存在时,判断是否已经获取过 动态菜单,未获取,即 false 时,需要获取
            if (!router.options.isAddDynamicMenuRoutes) {
                    http.menu.getMenus().then(response => {
                        // 数据返回成功时
                    if (response && response.data.code === 200) {
                        // 设置动态菜单为 true,表示不用再次获取
                        router.options.isAddDynamicMenuRoutes = true
                        // 获取动态菜单数据
                        let results = fnAddDynamicMenuRoutes(response.data.data)
                        console.log(results)
                    }
                    })
            }
        }
    }
    复制代码

    Step2:
      fnAddDynamicMenuRoutes 用于递归菜单数据。
      getRoute 用于返回某个数据转换的 路由格式。
    下面注释写的应该算是很详细了,主要讲一下思路:
      对数据进行遍历,
      定义两个数组(temp,route),temp 用于保存没有子路由的路由,route 用于保存存在子路由的路由。
      如果某个数据存在 子路由,则对子路由进行遍历,并将其返回结果作为当前数据的 children。并使用 route 保存路由。
      如果某个数据不存在子路由,则直接使用 temp 保存路由。
      最后,返回两者拼接的结果,即为转换后的数据。

    复制代码
    // 用于处理动态菜单数据,将其转为 route 形式
    function fnAddDynamicMenuRoutes(menuList = [], routes = []) {
        // 用于保存普通路由数据
        let temp = []
        // 用于保存存在子路由的路由数据
        let route = []
        // 遍历数据
        for (let i = 0; i < menuList.length; i++) {
            // 存在子路由,则递归遍历,并返回数据作为 children 保存
            if (menuList[i].subMenuList && menuList[i].subMenuList.length > 0) {
                // 获取路由的基本格式
                route = getRoute(menuList[i])
                // 递归处理子路由数据,并返回,将其作为路由的 children 保存
                route.children = fnAddDynamicMenuRoutes(menuList[i].subMenuList)
                // 保存存在子路由的路由
                routes.push(route)
            } else {
                // 保存普通路由
                temp.push(getRoute(menuList[i]))
            }
        }
        // 返回路由结果
        return routes.concat(temp)
    }
    
    // 返回路由的基本格式
    function getRoute(item) {
        // 路由基本格式
        let route = {
            // 路由的路径
            path: '',
            // 路由名
            name: '',
            // 路由所在组件
            component: null,
            meta: {
                // 开启路由守卫标志
                isRouter: true,
                // 开启标签页显示标志
                isTab: true,
                // iframe 标签指向的地址(数据以 http 或者 https 开头时,使用 iframe 标签显示)
                iframeUrl: '',
                // 开启动态路由标志
                isDynamic: true,
                // 动态菜单名称(nameZH 显示中文, nameEN 显示英文)
                name_zh: item.name_zh,
                name_en: item.name_en,
                // 动态菜单项的图标
                icon: item.icon,
                // 菜单项的 ID
                menuId: item.menuId,
                // 菜单项的父菜单 ID
                parentId: item.parentId,
                // 菜单项排序依据
                orderNum: item.orderNum,
                // 菜单项类型(0: 目录,1: 菜单项,2: 按钮)
                type: item.type
            },
            // 路由的子路由
            children: []
        }
        // 如果存在 url,则根据 url 进行相关处理(判断是 iframe 类型还是 普通类型)
        if (item.url && /S/.test(item.url)) {
            // 若 url 有前缀 /,则将其去除,方便后续操作。
            item.url = item.url.replace(/^//, '')
            // 定义基本路由规则,将 / 使用 - 代替
            route.path = item.url.replace('/', '-')
            route.name = item.url.replace('/', '-')
            // 如果是 外部 url,使用 iframe 标签展示,不用指定 component,重新指定 path、name 以及 iframeUrl。
            if (isURL(item.url)) {
                route['path'] = `iframe-${item.menuId}`
                route['name'] = `iframe-${item.menuId}`
                route['meta']['iframeUrl'] = item.url
            } else {
                // 如果是项目模块,使用 route-view 标签展示,需要指定 component(加载指定目录下的 vue 组件)
                route.component = () => import(`@/views/dynamic/${item.url}.vue`) || null
            }
        }
        // 返回 route
        return route
    }
    复制代码

    可以查看当前输出结果:

    复制代码
    [
        {
            "path": "",
            "name": "",
            "component": null,
            "meta": {
                "isRouter": true,
                "isTab": true,
                "iframeUrl": "",
                "isDynamic": true,
                "name_zh": "系统管理",
                "name_en": "System Control",
                "icon": "el-icon-setting",
                "menuId": 1,
                "parentId": 0,
                "orderNum": 0,
                "type": 0
            },
            "children": [
                {
                    "path": "sys-UserList",
                    "name": "sys-UserList",
                    "meta": {
                        "isRouter": true,
                        "isTab": true,
                        "iframeUrl": "",
                        "isDynamic": true,
                        "name_zh": "管理员列表",
                        "name_en": "Administrator List",
                        "icon": "el-icon-user",
                        "menuId": 2,
                        "parentId": 1,
                        "orderNum": 1,
                        "type": 1
                    },
                    "children": []
                },
                {
                    "path": "sys-RoleControl",
                    "name": "sys-RoleControl",
                    "meta": {
                        "isRouter": true,
                        "isTab": true,
                        "iframeUrl": "",
                        "isDynamic": true,
                        "name_zh": "角色管理",
                        "name_en": "Role Control",
                        "icon": "el-icon-price-tag",
                        "menuId": 3,
                        "parentId": 1,
                        "orderNum": 2,
                        "type": 1
                    },
                    "children": []
                },
                {
                    "path": "sys-MenuControl",
                    "name": "sys-MenuControl",
                    "meta": {
                        "isRouter": true,
                        "isTab": true,
                        "iframeUrl": "",
                        "isDynamic": true,
                        "name_zh": "菜单管理",
                        "name_en": "Menu Control",
                        "icon": "el-icon-menu",
                        "menuId": 4,
                        "parentId": 1,
                        "orderNum": 3,
                        "type": 1
                    },
                    "children": []
                }
            ]
        },
        {
            "path": "",
            "name": "",
            "component": null,
            "meta": {
                "isRouter": true,
                "isTab": true,
                "iframeUrl": "",
                "isDynamic": true,
                "name_zh": "帮助",
                "name_en": "Help",
                "icon": "el-icon-info",
                "menuId": 5,
                "parentId": 0,
                "orderNum": 1,
                "type": 0
            },
            "children": [
                {
                    "path": "iframe-6",
                    "name": "iframe-6",
                    "component": null,
                    "meta": {
                        "isRouter": true,
                        "isTab": true,
                        "iframeUrl": "https://www.baidu.com/",
                        "isDynamic": true,
                        "name_zh": "百度",
                        "name_en": "Baidu",
                        "icon": "el-icon-menu",
                        "menuId": 6,
                        "parentId": 5,
                        "orderNum": 1,
                        "type": 1
                    },
                    "children": []
                },
                {
                    "path": "iframe-7",
                    "name": "iframe-7",
                    "component": null,
                    "meta": {
                        "isRouter": true,
                        "isTab": true,
                        "iframeUrl": "https://www.cnblogs.com/l-y-h/",
                        "isDynamic": true,
                        "name_zh": "博客",
                        "name_en": "Blog",
                        "icon": "el-icon-menu",
                        "menuId": 7,
                        "parentId": 5,
                        "orderNum": 1,
                        "type": 1
                    },
                    "children": []
                }
            ]
        }
    ]
    复制代码

    5、使用 addRoute 添加路由:

      通过上面的步骤,已经获取到动态菜单数据,并将其转为路由的格式。
      现在只需要使用 addRoute 将其添加到路由上,即可使用该动态路由。
    有个小坑,下面有演示以及解决方法。

    (1)添加三个基本组件页面,用于测试。
      基本组件页面的位置,要与上面 component 指定的位置相一致。

    复制代码
    【MenuControl.vue】
    <template>
        <div>
            <h1>Menu Control</h1>
        </div>
    </template>
    
    <script>
    </script>
    
    <style>
    </style>
    
    【RoleControl.vue】
    <template>
        <div>
            <h1>Role Control</h1>
        </div>
    </template>
    
    <script>
    </script>
    
    <style>
    </style>
    
    【UserList.vue】
    <template>
        <div>
            <h1>User List</h1>
        </div>
    </template>
    
    <script>
    </script>
    
    <style>
    </style>
    复制代码

    (2)使用 addRoute 添加路由 -- 基本版
      根据项目需求,自行处理相关显示逻辑(此处只是最简单的版本)。
      由于此处的菜单显示,只是二级菜单。且菜单内容,均显示在 Home.vue 组件中,所以最终路由的格式应该如下所示,所有的路由均显示在 children 中。

    复制代码
    {
        path: '/DynamicRoutes',
        name: 'DynamicHome',
        component: () => import('@/views/Home.vue'),
        children: [{
         }]
    }
    复制代码

    想要实现这个效果,得对转换后的动态数据进行进一步的处理。对于第一层菜单数据,需要指定相应的组件,此处为 Home.vue。

    复制代码
    // 获取动态菜单数据
    let results = fnAddDynamicMenuRoutes(response.data.data)
    // 如果动态菜单数据存在,对其进行处理
    if (results && results.length > 0) {
        // 遍历第一层数据
        results.map(value => {
            // 如果 path 值不存在,则对其赋值,并指定 component 为 Home.vue
            if (!value.path) {
                value.path = `/DynamicRoutes-${value.meta.menuId}`
                value.name = `DynamicHome-${value.meta.menuId}`
                value.component = () => import('@/views/Home.vue')
            }
        })
    }
    console.log(results)
    复制代码

    可以看到路由信息都已完善,此时,即可使用 addRoute 方法添加路由。

    复制代码
    // 使用 router 实例的 addRoutes 方法,给当前 router 实例添加一个动态路由
    router.addRoutes(results)
    // 查看当前路由信息
    console.log(router.options)
    // 测试一下路由是否能正常跳转,能跳转,则显示路由添加成功
    setTimeout(()=> {
        router.push({name : "sys-UserList"})
    }, 3000)
    复制代码

      如上图所示,动态路由添加成功,且能够成功跳转。
      但是有一个问题,如何获取到动态路由的值呢?

      一般通过 router 实例对象的 options 方法可以查看到当前路由的信息。(使用 router.options 或者 this.$router.options 可以查看)。

      但是从上图可以看到,动态添加的数据并没有在 options 中显示出来,但路由确实添加成功了。没有看源代码,所以不太清除里面的实现逻辑(有人知道的话,还望不吝赐教)。谷歌搜索了一下,没有找到满意的答案,基本都是说如何在 options 中显示路由信息(=_=),所以此处省略原理,以后有机会再补充,此处直接上解决办法。

    获取动态路由值一般有两种方式。
      第一种:通过 router 实例对象的 options 方式。
        动态路由使用静态路由的 children 展开,即事先定义好静态路由,添加动态路由时,将该动态路由添加到静态路由 children 上,即可通过 options 显示。 

      第二种:通过 vuex 方式。
        通过 vuex 保存当前动态路由的信息。即单独保存一份动态路由的信息,使用时从 vuex 中获取即可。当然,使用 localStorage、sessionStorage 保存亦可。

    (3)获取动态路由值 -- 方式一
      通过静态路由的 children 的形式,添加动态路由信息。
    Step1:
      定义一个静态路由,当然,router 实例对象中的 routes 也需要修改。

    复制代码
    // 用于保存动态路由
    const dynamicRoutes = [{
        path: '/DynamicRoutes',
        component: () => import('@/views/Home.vue'),
        children: []
    }]
    
    // 创建一个 router 实例
    const router = new VueRouter({
        // routes 用于定义 路由跳转 规则
        routes: dynamicRoutes.concat(routes),
        // routes,
        // mode 用于去除地址中的 #
        mode: 'history',
        // scrollBehavior 用于定义路由切换时,页面滚动。
        scrollBehavior: () => ({
            y: 0
        }),
        // isAddDynamicMenuRoutes 表示是否已经添加过动态菜单(防止频繁请求动态菜单)
        isAddDynamicMenuRoutes: false
    })
    复制代码

    Step2:
      修改 addRoutes 规则。
      首先需要将 动态路由添加到 静态路由的 children 方法中。
      然后使用 addRoutes 添加静态路由。

    复制代码
    // 如果动态菜单数据存在,对其进行处理
    if (results && results.length > 0) {
        // 遍历第一层数据
        results.map(value => {
            // 如果 path 值不存在,则对其赋值,并指定 component 为 Home.vue
            if (!value.path) {
                value.path = `/DynamicRoutes-${value.meta.menuId}`
                value.name = `DynamicHome-${value.meta.menuId}`
                value.component = () => import('@/views/Home.vue')
            }
        })
    }
    // 将动态路由信息添加到静态路由的 children 中
    dynamicRoutes[0].children = results
    dynamicRoutes[0].name = 'DynamicRoutes'
    // 使用 router 实例的 addRoutes 方法,给当前 router 实例添加一个动态路由
    router.addRoutes(dynamicRoutes)
    // 查看当前路由信息
    console.log(router.options)
    // 测试一下路由是否能正常跳转,能跳转,则显示路由添加成功
    router.push({name : "sys-UserList"})
    复制代码

      通过上图,可以看到,使用 addRoutes 添加数据到静态路由的 children 后,可以使用 router 实例对象的 options 查看到相应的动态数据。

      但是还是出现了一些小问题,由于静态路由指定了 Home.vue 组件,所以其 children 不应该再出现 Home.vue 组件,否则出现了会出现上图那样的情况(Home.vue 组件中套 Home.vue 组件)。

      感觉没有什么好的解决办法,只能粗暴的舍弃一些数据。
      比如舍弃第一层的数据,将其余数据拼接成一个数组,然后添加到静态路由的 children 上。

    复制代码
    // 保存动态菜单数据
    let temp = []
    // 如果动态菜单数据存在,对其进行处理
    if (results && results.length > 0) {
        // 舍弃第一层数据
        results.map(value => {
            if (!value.path) {
                temp = temp.concat(value.children)
            }
        })
    }
    // 将动态路由信息添加到静态路由的 children 中
    dynamicRoutes[0].children = temp
    dynamicRoutes[0].name = 'DynamicRoutes'
    // 使用 router 实例的 addRoutes 方法,给当前 router 实例添加一个动态路由
    router.addRoutes(dynamicRoutes)
    // 查看当前路由信息
    console.log(router.options)
    // 测试一下路由是否能正常跳转,能跳转,则显示路由添加成功
    router.push({name : "sys-UserList"})
    复制代码

    当然,不舍弃第一层数据,将其全部拼接成一个数组亦可。

    复制代码
    // 保存动态菜单数据
    let temp = []
    // 如果动态菜单数据存在,对其进行处理
    if (results && results.length > 0) {
        // 将全部数据拼接成一个数组
        results.map(value => {
            if (!value.path) {
                temp = temp.concat(value.children)
                value.children = []
                value.path = `/DynamicRoutes-${value.meta.menuId}`
                value.name = `DynamicHome-${value.meta.menuId}`
                temp = temp.concat(value)
            }
        })
    }
    // 将动态路由信息添加到静态路由的 children 中
    dynamicRoutes[0].children = temp
    dynamicRoutes[0].name = 'DynamicRoutes'
    // 使用 router 实例的 addRoutes 方法,给当前 router 实例添加一个动态路由
    router.addRoutes(dynamicRoutes)
    // 查看当前路由信息
    console.log(router.options)
    // 测试一下路由是否能正常跳转,能跳转,则显示路由添加成功
    router.push({name : "sys-UserList"})
    复制代码

    (4)获取动态路由值 -- 方式二
      使用 vuex 保存动态路由的值,使用时从 vuex 中获取。
      如下,在 common.js 中定义相关路由处理操作。

    复制代码
    【进行相关处理:】
    export default {
        state: {
            dynamicRoutes: []
        },
        // 更改 state(同步)
        mutations: {
            updateDynamicRoutes(state, routes) {
                state.dynamicRoutes = routes
            },
            resetState(state) {
                let stateTemp = {
                    dynamicRoutes: []
                }
                Object.assign(state, stateTemp)
            }
        },
        // 异步触发 mutations
        actions: {
            updateDynamicRoutes({commit, state}, routes) {
                commit("updateDynamicRoutes", routes)
            },
            resetState({commit, state}) {
                commit("resetState")
            }
        }
    }
    
    
    【完整 common.js】
    export default {
        // 开启命名空间(防止各模块间命名冲突),访问时需要使用 模块名 + 方法名
        namespaced: true,
        // 管理数据(状态)
        state: {
            // 用于保存语言设置(国际化),默认为中文
            language: 'zh',
            // 表示侧边栏选中的菜单项的名
            menuActiveName: '',
            // 表示标签页数据,数组
            mainTabs: [],
            // 表示标签页中选中的标签名
            mainTabsActiveName: '',
            // 用于保存动态路由的数据
            dynamicRoutes: []
        },
        // 更改 state(同步)
        mutations: {
            updateLanguage(state, data) {
                state.language = data
            },
            updateMenuActiveName(state, name) {
                state.menuActiveName = name
            },
            updateMainTabs(state, tabs) {
                state.mainTabs = tabs
            },
            updateMainTabsActiveName(state, name) {
                state.mainTabsActiveName = name
            },
            updateDynamicRoutes(state, routes) {
                state.dynamicRoutes = routes
            },
            resetState(state) {
                let stateTemp = {
                    language: 'zh',
                    menuActiveName: '',
                    mainTabs: [],
                    mainTabsActiveName: '',
                    dynamicRoutes: []
                }
                Object.assign(state, stateTemp)
            }
        },
        // 异步触发 mutations
        actions: {
            updateLanguage({commit, state}, data) {
                commit("updateLanguage", data)
            },
            updateMenuActiveName({commit, state}, name) {
                commit("updateMenuActiveName", name)
            },
            updateMainTabs({commit, state}, tabs) {
                commit("updateMainTabs", tabs)
            },
            updateMainTabsActiveName({commit, state}, name) {
                commit("updateMainTabsActiveName", name)
            },
            updateDynamicRoutes({commit, state}, routes) {
                commit("updateDynamicRoutes", routes)
            },
            resetState({commit, state}) {
                commit("resetState")
            }
        }
    }
    复制代码

    如何在 router 中使用 vuex 呢?
      通过 router.app.$options.store 可以获取到 vuex 内容。

    复制代码
    【获取 state 数据:】
        router.app.$options.store.state
        
    【调用 vuex 方法:】
        router.app.$options.store.dispatch()
     
    【使用:】 
    let results = fnAddDynamicMenuRoutes(response.data.data)
    // 如果动态菜单数据存在,对其进行处理
    if (results && results.length > 0) {
        // 遍历第一层数据
        results.map(value => {
            // 如果 path 值不存在,则对其赋值,并指定 component 为 Home.vue
            if (!value.path) {
                value.path = `/DynamicRoutes-${value.meta.menuId}`
                value.name = `DynamicHome-${value.meta.menuId}`
                value.component = () => import('@/views/Home.vue')
            }
        })
    }
    // 使用 vuex 管理动态路由数据
    router.app.$options.store.dispatch('common/updateDynamicRoutes', results)
    console.log(JSON.stringify(router.app.$options.store.state.common.dynamicRoutes))
    
    // 使用 router 实例的 addRoutes 方法,给当前 router 实例添加一个动态路由
    router.addRoutes(results)
    // 查看当前路由信息
    console.log(router.options)
    // 测试一下路由是否能正常跳转,能跳转,则显示路由添加成功
    router.push({name : "sys-UserList"})
    复制代码

    (5)使用 addRoute 添加路由 -- 最终版
      此项目中,使用 vuex 管理动态菜单数据,而不在 options 中显示(当然两种方式结合一起亦可)。
    完整 router 的 index.js 逻辑如下。

    复制代码
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Home from '../views/Home.vue'
    import {
        getToken
    } from '@/http/auth.js'
    import http from '@/http/http.js'
    import {
        isURL,
        isDynamicRoutes
    } from '@/utils/validate.js'
    
    Vue.use(VueRouter)
    
    // 定义路由跳转规则
    // component 采用 路由懒加载形式
    // 此项目中,均采用 name 方式指定路由进行跳转
    /* 
        meta 用于定义路由元信息,
    其中 
        isRouter 用于指示是否开启路由守卫(true 表示开启)。
        isTab 用于表示是否显示为标签页(true 表示显示)
        iframeUrl 用于表示 url,使用 http 或者 https 开头的 url 使用 iframe 标签展示
    */
    const routes = [{
            path: '/',
            redirect: {
                name: "Login"
            }
        },
        {
            path: '/404',
            name: '404',
            component: () => import('@/components/common/404.vue')
        },
        {
            path: '/Login',
            name: 'Login',
            component: () => import('@/components/common/Login.vue')
        },
        {
            path: '/Home',
            name: 'Home',
            component: () => import('@/views/Home.vue'),
            redirect: {
                name: 'HomePage'
            },
            children: [{
                    path: '/Home/Page',
                    name: 'HomePage',
                    component: () => import('@/views/menu/HomePage.vue'),
                    meta: {
                        isRouter: true
                    }
                },
                {
                    path: '/Home/Demo/Echarts',
                    name: 'Echarts',
                    component: () => import('@/views/menu/Echarts.vue'),
                    meta: {
                        isRouter: true,
                        isTab: true
                    }
                },
                {
                    path: '/Home/Demo/Ueditor',
                    name: 'Ueditor',
                    component: () => import('@/views/menu/Ueditor.vue'),
                    meta: {
                        isRouter: true,
                        isTab: true
                    }
                },
                {
                    path: '/Home/Demo/Baidu',
                    name: 'Baidu',
                    meta: {
                        isRouter: true,
                        isTab: true,
                        iframeUrl: '@/test.html'
                    }
                },
                // 路由匹配失败时,跳转到 404 页面
                {
                    path: "*",
                    redirect: {
                        name: '404'
                    }
                }
            ]
        }
    ]
    
    // 创建一个 router 实例
    const router = new VueRouter({
        // routes 用于定义 路由跳转 规则
        routes,
        // mode 用于去除地址中的 #
        mode: 'history',
        // scrollBehavior 用于定义路由切换时,页面滚动。
        scrollBehavior: () => ({
            y: 0
        }),
        // isAddDynamicMenuRoutes 表示是否已经添加过动态菜单(防止频繁请求动态菜单)
        isAddDynamicMenuRoutes: false
    })
    
    // 添加全局路由导航守卫
    router.beforeEach((to, from, next) => {
        // 当开启导航守卫时,验证 token 是否存在。
        // to.meta.isRouter 表示是否开启动态路由
        // isDynamicRoutes 判断该路由是否为动态路由(页面刷新时,动态路由没有 isRouter 值,此时 to.meta.isRouter 条件不成立,即动态路由拼接逻辑不能执行)
        if (to.meta.isRouter || isDynamicRoutes(to.path)) {
            // console.log(router)
            // 获取 token 值
            let token = getToken()
            // token 不存在时,跳转到 登录页面
            if (!token || !/S/.test(token)) {
                next({
                    name: 'Login'
                })
            }
            // token 存在时,判断是否已经获取过 动态菜单,未获取,即 false 时,需要获取
            if (!router.options.isAddDynamicMenuRoutes) {
                http.menu.getMenus().then((response => {
                    // 数据返回成功时
                    if (response && response.data.code === 200) {
                        // 设置动态菜单为 true,表示不用再次获取
                        router.options.isAddDynamicMenuRoutes = true
                        // 获取动态菜单数据
                        let results = fnAddDynamicMenuRoutes(response.data.data)
                        // 如果动态菜单数据存在,对其进行处理
                        if (results && results.length > 0) {
                            // 遍历第一层数据
                            results.map(value => {
                                // 如果 path 值不存在,则对其赋值,并指定 component 为 Home.vue
                                if (!value.path) {
                                    value.path = `/DynamicRoutes-${value.meta.menuId}`
                                    value.name = `DynamicHome-${value.meta.menuId}`
                                    value.component = () => import('@/views/Home.vue')
                                }
                            })
                        }
                        // 使用 vuex 管理动态路由数据
                        router.app.$options.store.dispatch('common/updateDynamicRoutes', results)
                        // 使用 router 实例的 addRoutes 方法,给当前 router 实例添加一个动态路由
                        router.addRoutes(results)
                    }
                }))
            }
        }
        next()
    })
    
    // 用于处理动态菜单数据,将其转为 route 形式
    function fnAddDynamicMenuRoutes(menuList = [], routes = []) {
        // 用于保存普通路由数据
        let temp = []
        // 用于保存存在子路由的路由数据
        let route = []
        // 遍历数据
        for (let i = 0; i < menuList.length; i++) {
            // 存在子路由,则递归遍历,并返回数据作为 children 保存
            if (menuList[i].subMenuList && menuList[i].subMenuList.length > 0) {
                // 获取路由的基本格式
                route = getRoute(menuList[i])
                // 递归处理子路由数据,并返回,将其作为路由的 children 保存
                route.children = fnAddDynamicMenuRoutes(menuList[i].subMenuList)
                // 保存存在子路由的路由
                routes.push(route)
            } else {
                // 保存普通路由
                temp.push(getRoute(menuList[i]))
            }
        }
        // 返回路由结果
        return routes.concat(temp)
    }
    
    // 返回路由的基本格式
    function getRoute(item) {
        // 路由基本格式
        let route = {
            // 路由的路径
            path: '',
            // 路由名
            name: '',
            // 路由所在组件
            component: null,
            meta: {
                // 开启路由守卫标志
                isRouter: true,
                // 开启标签页显示标志
                isTab: true,
                // iframe 标签指向的地址(数据以 http 或者 https 开头时,使用 iframe 标签显示)
                iframeUrl: '',
                // 开启动态路由标志
                isDynamic: true,
                // 动态菜单名称(nameZH 显示中文, nameEN 显示英文)
                name_zh: item.name_zh,
                name_en: item.name_en,
                // 动态菜单项的图标
                icon: item.icon,
                // 菜单项的 ID
                menuId: item.menuId,
                // 菜单项的父菜单 ID
                parentId: item.parentId,
                // 菜单项排序依据
                orderNum: item.orderNum,
                // 菜单项类型(0: 目录,1: 菜单项,2: 按钮)
                type: item.type
            },
            // 路由的子路由
            children: []
        }
        // 如果存在 url,则根据 url 进行相关处理(判断是 iframe 类型还是 普通类型)
        if (item.url && /S/.test(item.url)) {
            // 若 url 有前缀 /,则将其去除,方便后续操作。
            item.url = item.url.replace(/^//, '')
            // 定义基本路由规则,将 / 使用 - 代替
            route.path = item.url.replace('/', '-')
            route.name = item.url.replace('/', '-')
            // 如果是 外部 url,使用 iframe 标签展示,不用指定 component,重新指定 path、name 以及 iframeUrl。
            if (isURL(item.url)) {
                route['path'] = `iframe-${item.menuId}`
                route['name'] = `iframe-${item.menuId}`
                route['meta']['iframeUrl'] = item.url
            } else {
                // 如果是项目模块,使用 route-view 标签展示,需要指定 component(加载指定目录下的 vue 组件)
                route.component = () => import(`@/views/dynamic/${item.url}.vue`) || null
            }
        }
        // 返回 route
        return route
    }
    
    // 解决相同路径跳转报错
    const routerPush = VueRouter.prototype.push;
    VueRouter.prototype.push = function push(location, onResolve, onReject) {
        if (onResolve || onReject)
            return routerPush.call(this, location, onResolve, onReject)
        return routerPush.call(this, location).catch(error => error)
    }
    
    export default router
    复制代码

    二、动态菜单的展示

      前面已经获取到了动态菜单数据,并使用 vuex 对其进行了管理。
      现在只需要将数据进行展示即可。

    1、定义一个 DynamicMenu.vue 组件

      定义一个 DynamicMenu.vue 组件,用于展示动态菜单。
      与之前写 Aside.vue 代码类似,将其 copy 过来直接修改即可。
    其中:
      menu 表示菜单项数据,是由父组件通过 props 传递进来的。
      index 显示菜单选中项,通过 menu.name 绑定。
      icon 显示菜单项图标,通过 menu.meta.icon 绑定。
      菜单数据国际化通过 menu.meta.name_zh、menu.meta.name_en 绑定。
      subMenu 表示当前菜单的子菜单选项,需要绑定路由跳转事件。
      而路由跳转事件的相关处理,在之前的一篇博客中已经做了处理,此处直接使用即可。
    路由跳转、iframe 嵌套参考:https://www.cnblogs.com/l-y-h/p/12973364.html

    复制代码
    <template>
        <el-submenu :index="menu.name">
            <template slot="title">
                <i :class="menu.meta.icon"></i>
                <span>{{$i18n.locale === 'zh' ? menu.meta.name_zh : menu.meta.name_en}}</span>
            </template>
            <el-menu-item v-for="subMenu in menu.children" :key="subMenu.meta.menuId" :index="subMenu.name" @click="$router.push({ name: subMenu.name })">
                <i :class="subMenu.meta.icon"></i>
                <span slot="title">{{$i18n.locale === 'zh' ? subMenu.meta.name_zh : subMenu.meta.name_en}}</span>
            </el-menu-item>
        </el-submenu>
    </template>
    
    <script>
        export default {
            name: 'DynamicMenu',
            props: {
                menu: {
                    type: Object,
                    required: true
                }
            }
        }
    </script>
    
    <style>
    </style>
    复制代码

    2、在 Aside.vue 中引入 DynamicMenu.vue

      组件定义好后,就需要引入了。
    (1)引入动态菜单组件:
    Step1:import 引入相关组件。
    Step2:components 声明相关组件。
    Step3:使用组件。

    复制代码
    <template>
        <DynamicMenu v-for="menu in dynamicRoutes" :key="menu.meta.menuId" :menu="menu"></DynamicMenu>
    </template>        
    <script>
        import DynamicMenu from '@/views/dynamic/DynamicMenu.vue'
        export default {
            name: 'Aside',
            components: {
                DynamicMenu
            }
        }
    </script>
    复制代码

    (2)引入 vuex 管理的动态菜单数据
      由于之前已经引入过 vuex,此处直接引入 动态菜单数据 dynamicRoutes 即可。

    computed: {
        ...mapState('common', ['menuActiveName', 'mainTabs', 'dynamicRoutes'])
    }

    3、完整效果

    别把自己太当回事,也别太把自己不当回事!Life is Fantastic!!!
     
  • 相关阅读:
    pyspark 知识点
    pyspark 读取csv文件创建DataFrame的两种方法
    iOS开发改变字符串中指定字符颜色,大小等等
    iOS开发解决页面滑动返回跟scrollView左右划冲突
    iOS开发自定义试图切换
    iOS开发UIColor,CGColor,CIColor三者的区别和联系
    iOS开发热更新JSPatch
    Swift-创建UIButton(其他UI组件雷同)
    Swift-枚举enum理解
    Swift-闭包理解(二)
  • 原文地址:https://www.cnblogs.com/onesea/p/13054877.html
Copyright © 2011-2022 走看看