跳至主要內容

动态路由

杨长志大约 7 分钟

动态路由

路由鉴权

在访问路由的时候要通过路由守卫进行拦截,判断当前所访问的路由有没有访问权限,这个过程称之为 路由鉴权

路由鉴权主要有两种模式,前端路由鉴权动态路由鉴权

前端路由鉴权

前端路由鉴权有两种模式:

    1. 完全由前端控制,给每个菜单绑定一个权限组,登录人员会有一个权限组名,一般跟角色绑定,例如:管理员(admin)、普通人员等。所有菜单和路由在前端配置,路由切换的时候进行路由拦截,判断当前要访问的路由所属权限组与当前登录人所属权限组是否一致,一致则通过,否则就不通过。
    1. 逻辑大同小异,在方式1的基础上更加细化,当前登录人能访问什么菜单直接由后端接口返回,在人员管理的时候会给当前人员分配菜单,然后登录的时候通过接口查询当前登录人能访问的所有菜单。对于路由还是在前端配置,路由切换的时候进行路由拦截,判断当前要访问的路由所对应的菜单是不是在登录后接口查询返回的菜单列表中,如果在就通过,不在就不通过。

前端路由鉴权的缺点:

    1. 需要写大量的逻辑判断代码,用于判断哪些能通过,哪些不能通过。
    1. 每次访问模块都需要鉴定权限,模块数量过多时会影响系统性能。
    1. 前端需要把所有的路由都提前考虑好,需要写全量路由表,不方便路由调整。

动态路由鉴权

通过前面的前端路由鉴权以及其缺点,于是我们产生了另外一种思路,动态路由,后端返回了哪些菜单,就自动根据这些菜单生成对应的路由,这样路由拦截的时候也不用考虑权限,不用去做大量的逻辑判断,也做到了真正的权限隔离,登录返回了几个菜单,你的系统里面就只有这么几个路由,其他任何的访问都是404。

具体的做法就是:

动态路由
动态路由

动态路由

想要使用动态路由鉴权的方式,就需要能够自动根据接口查询回来的菜单动态的生成vue-router路由,这样我们在点击菜单进行菜单访问的时候才能跳转到正确的页面。具体步骤如下:

动态路由对应菜单结构示例

// 获取的菜单结构示例
[{
    path: '/',
    name: '_home',
    component: 'BasicLayout',
    title: '首页',
    meta: {

    },
    children: [{
        path: '/home',
        name: 'home',
        component: 'dashboard/console',
        title: '首页',
        meta: {
            icon: 'dashboard'
        }
    }]
},
{
    path: '/example',
    component: 'BasicLayout',
    name: 'fsfse',
    title: '案例',
    meta: {
        icon: 'example'
    },
    children: [{
        path: 'table',
        name: '23dd',
        title: '案例二级菜单1',
        component: '',
        meta: {
            icon: 'table'
        },
        children: [{
            path: 'third',
            name: 'd334',
            title: '表格示例页面',
            component: 'example/table/third',
            meta: {
            }
        }]
    }
    ]
},
{
    path: '/_form',
    component: 'BasicLayout',
    name: 'ijjj',
    title: '表单',
    meta: {
        icon: ''
    },
    children: [{
        path: '/form',
        name: 'vvddd',
        title: '表单',
        component: 'form',
        meta: {
            icon: 'form'
        }
    }]
},
{
    path: '/blank',
    component: 'blank',
    name: 'ijadwadwajj',
    title: '新页签打开路由',
    meta: {
    icon: '',
    target: 'blank'
    }
},
{
    path: '/_frame',
    component: 'BasicLayout',
    name: 'ddfffafefe',
    title: '外部链接',
    meta: {
        icon: '',
        title: ''
    },
    children: [{
        path: '/iview',
        name: 'dwdjdjdjd',
        title: '内嵌IView',
        component: 'https://www.iviewui.com?tf=99',
        meta: {
            icon: '',
            target: 'self'
        }
    }, {
        path: 'http://www.baidu.com',
        name: 'gdhjahgddwyw',
        title: '外部打开百度',
        component: '',
        type: 'blankHref',
        meta: {
        href: '',
        icon: '',
        target: 'blank'
        }
    }]
}]

获取路由

  • 获取路由存入localStorage
  • 调用动态路由自动生成方法-filterAsyncRouter来自动生成路由
  • 将处理好的路由作为菜单数据存储在Vuex store中
// 获取用户菜单
GetUserMenu().then(async usermenu => {
    const menus = usermenu || []
    // 顶部菜单
    const headerMenuList = []
    menus.map((item) => {
        const meunitem = cloneDeep(item)
        if (meunitem.children) {
            delete meunitem.children
        }
        item.meta.header = item.name
        headerMenuList.push(meunitem)
    })
    // 动态创菜单
    if (Setting.dynamicSiderMenu) {
        // 动态创建菜单
        let menuSider = filterAsyncRouter(cloneDeep(menus), '', router)
        // 处理路由,添加fullPath
        menuSider = transferAllmenus(menuSider)
        // 设置顶部菜单
        await commit('admin/menu/setHeader', headerMenuList, { root: true })
        // 所有菜单
        await commit('admin/menu/setMenuSider', menuSider, { root: true })
        // 侧边栏菜单
        // 侧边栏菜单
        await commit('admin/menu/setSider', menuSider, { root: true });
        await commit('admin/page/init', [
            ...frameInRoutes,
            ...menuSider
        ], { root: true });
    }

    resolve();
}).catch(async menuerror => {
    console.error(menuerror)
    // 设置 vuex 用户信息
    await dispatch('admin/user/set', userinfo, {
        root: true
    });
    // 用户登录后从持久化数据加载一系列的设置
    await dispatch('load');
    // 结束
    resolve();
})

动态路由自动生成 filterAsyncRouter

import _import from '@/router/_router_import.js';
import BasicLayout from '@/layouts/basic-layout/layout.vue';
import EmptyLayout from '@/layouts/empty-layout';
import ToolFrame from '@/pages/tool/tool-frame.vue'

export const filterAsyncRouter = (menus, parentName, router) => {
    const accessedRouters = menus.map(route => {
         // 用名字作为组件路径和组件名称
         route.meta = route.meta || {}
         route.meta.title = route.title
         if (route.component === 'BasicLayout') { // Layout组件特殊处理
             route.component = BasicLayout
         } else {
             // 需要新开窗
             if (route.path && route.component && (route.component.indexOf('http://') >= 0 || route.component.indexOf('https://') >= 0)) {
                 route.meta.href = route.component
                 route.component = ToolFrame
                 route.meta.frame = route.meta.target === 'blank' ? 'blank' : 'inside'
             } else if (route.path && (route.path.indexOf('http://') >= 0 || route.path.indexOf('https://') >= 0)) {
                 route.meta.href = route.path
                 route.component = route.component ? _import(route.component) : ToolFrame
                 route.meta.frame = route.meta.target === 'blank' ? 'linkBlank' : 'inside'
             } else {
                 route.component = route.component ? _import(route.component) : EmptyLayout
                 route.meta.frame = route.meta.target === 'blank' ? 'blank' : 'self'
             }
         }
         router.addRoute(parentName || '', {
             path: route.path,
             component: route.component,
             name: route.name,
             redirect: route.redirect,
             meta: route.meta
         })
        if (route.children && route.children.length) {
            route.children = filterAsyncRouter(route.children, route.name, router)
        }
        return route
    })

    return accessedRouters
}

// _router_import.js
module.exports = file => () => import('@/pages/' + file + '/' + 'index.vue')
// tool-frame.vue

<template>
    <div class="tool-frame">
        <i-frame :src="frameUrl" class="frame" />
    </div>
</template>

<script>
    export default {
        name: 'ToolFrame',
        computed: {
            frameUrl () {
                return this.href || this.url
            }
        },
        props: {
            url: {
                type: String,
                default: ''
            }
        },
        data () {
            return {
                href: ''
            }
        },
        created () {
            this.href = this.$route.meta.href
        }
    }
</script>

<style lang="less">
.tool-frame {
    width: 100%;
    height: 100%;
    .frame {
        width: 100%;
        height: 100%;
    }
}
</style>

路由跳转拦截

  • 通过全局路由守卫进行拦截,判断Vuex store中存储的菜单存不存在,因为诸如浏览器刷新等操作会造成Vuex中数据丢失。
  • 如果不存在了就从localStorage中取出来登录获取出来的原始菜单数据。
    • 再次调用动态路由自动生成方法-filterAsyncRouter来自动生成路由
    • 将处理好的路由作为菜单数据存储在Vuex store中
/**
 * 路由拦截
 * 权限验证
 */

router.beforeEach(async (to, from, next) => {
    if (Setting.showProgressBar) iView.LoadingBar.start();
    const userInfo = store.getters['admin/user/userInfo'];
    if (!userInfo) {
        // 如果是刷新等造成菜单丢失的情况
        await store.dispatch('admin/account/getUserInfo', {
            root: true
        })
        const menuSider = store.getters['admin/menu/filterMenuSider'];
        judgeGotoRoute(to, from, next, menuSider)
    } else {
        const menuSider = store.getters['admin/menu/filterMenuSider'];
        judgeGotoRoute2(to, from, next, menuSider)
    }
});
function goTo (to, from, next, needReplace) {
    // sso统一集成,进入到系统说明已经登录成功
    if (needReplace) {
        next({
            path: to.fullPath || to.path,
            query: to.query,
            params: to.params
        })
        return
    }
    // 如果没有了websocket链接,那么重新建立
    const token = window.$realm ? window.$realm.token() : null;
    if (!store.getters['admin/ws/ws'] && token && Setting.ws) {
        store.dispatch('admin/ws/connect', {
            token: token
        })
    }
    next()
}

function judgeGotoRoute (to, from, next, menuSider) {
    const gotoPath = to.fullPath || to.path
    if (gotoPath === '/') {
        const menu0 = menuSider[0]
        let gotoMenu = {}
        if (menu0 && menu0.children && menu0.children.length) {
            gotoMenu = menu0.children[0]
            goTo(gotoMenu, from, next, true);
        } else if (menu0) {
            gotoMenu = menu0
            goTo(gotoMenu, from, next, true);
        } else {
            goTo(to, from, next);
        }
    } else if (menuSider && menuSider.length) {
        goTo(to, from, next, true)
    } else {
        goTo(to, from, next)
    }
}

function judgeGotoRoute2 (to, from, next, menuSider) {
    const gotoPath = to.fullPath || to.path
    if (gotoPath === '/') {
        const menu0 = menuSider[0]
        let gotoMenu = {}
        if (menu0 && menu0.children && menu0.children.length) {
            gotoMenu = menu0.children[0]
            goTo(gotoMenu, from, next, true)
        } else if (menu0) {
            gotoMenu = menu0
            goTo(gotoMenu, from, next, true)
        } else {
            goTo(to, from, next)
        }
    } else {
        goTo(to, from, next)
    }
}

监听路由变化

  • 我们通过对全局路由的监听来判断当路由改变了以后是否侧边栏的菜单需要更改,如果需要则重新通过Vuex 的方法来更改用于存储菜单的store变量。
  • 判断当前打开的菜单所属的父级菜单是什么,存入store中
  • 记录当前激活的菜单是哪个,存入store中
watch: {
    // 监听路由 控制侧边栏显示 标记当前顶栏菜单(如需要)
    async '$route' (to, from) {
        let path = ''
        if (to.fullPath === '/' || to.path === '/') {
            path = '/'
        } else {
            path = to.matched[to.matched.length - 1].path;
        }

        const menuSider = this.$store.getters['admin/menu/filterMenuSider'];
        const isShowHeaderMenu = this.$store.state.admin.layout.headerMenu;
        let headerName = ''
        let openNames = ''

        if (isShowHeaderMenu) {
            headerName = getHeaderName(path, menuSider);
            // 在 404 时,是没有 headerName 的
            if (headerName !== null) {
                this.$store.commit('admin/menu/setHeaderName', headerName || '');
                let siderList = []
                const filterMenuSider = getMenuSider(menuSider, headerName || '');
                const showFistLevel = this.$store.state.admin.layout.menuFirstLevel
                if (filterMenuSider && filterMenuSider.length === 1) {
                    if (filterMenuSider[0].children && filterMenuSider[0].children.length) {
                        siderList = showFistLevel ? filterMenuSider : filterMenuSider[0].children
                    } else {
                        siderList = []
                    }
                } else {
                    siderList = filterMenuSider
                }
                // 重新设置菜单栏
                this.$store.commit('admin/menu/setSider', siderList);
                this.$store.commit('admin/page/init', [
                    ...frameInRoutes,
                    ...filterMenuSider
                ]);
            } else {
                openNames = getSiderSubmenu(path, menuSider);
            }
        }
        openNames = getSiderSubmenu(path, menuSider);
        // 设置当前打开的是哪个菜单
        this.$store.commit('admin/menu/setOpenNames', openNames);
        // 设置当前的路由
        this.$store.commit('admin/menu/setActivePath', to.fullPath || to.path);
        // 多页控制 打开新的页面
        this.$store.dispatch('admin/page/open', to);
        this.appRouteChange(to, from);
    }
}

点击菜单

点击菜单的时候进行判断,当前菜单是哪种类型的,根据不同的类型做对应不同的跳转。

// 侧边栏路由点击跳转
handleClick (menu, type = 'sider') {
    const current = this.$route.path;
    if (current === (menu.fullPath || menu.path)) {
        if ((type === 'sider' || type === 'collapse') && this.menuSiderReload) this.handleReload();
        else if (type === 'header' && this.menuHeaderReload) this.handleReload();
    } else {
        if (type === 'collapse') {
            return
        }
        if (menu.meta && menu.meta.frame && menu.meta.frame === 'linkBlank') {
            const hrefurl = menu.meta.href || ''
            if (hrefurl.indexOf('http://') >= 0 || hrefurl.indexOf('https://') >= 0) {
                window.open(menu.meta.href, '_blank')
            } else {
                window.open('https://' + menu.meta.href, '_blank')
            }
            return
        } else if (menu.meta && menu.meta.frame && menu.meta.frame === 'blank') {
            const routeUrl = this.$router.resolve({
                path: menu.fullPath || menu.path,
                query: menu.query,
                params: menu.params
            });
            window.open(routeUrl.href, '_blank');
            return
        }
        this.$router.push({
            path: menu.fullPath || menu.path,
            query: menu.query,
            params: menu.params
        })
    }
}
// 顶部菜单点击
handleClickHeader (path, type = 'header') {
    const menuSider = this.$store.getters['admin/menu/filterMenuSider'];
    const headerName = getHeaderName(path, menuSider);

    // 在 404 时,是没有 headerName 的
    if (headerName !== null) {
        this.$store.commit('admin/menu/setHeaderName', headerName);
        const filterMenuSider = getMenuSider(menuSider, headerName);
        const menu = this.getFirstMenu(filterMenuSider[0])
        this.$router.push({
            path: menu.fullPath || menu.path,
            query: menu.query,
            params: menu.params
        })
    }
}