• 从壹开始 [vueAdmin后台] 之三 || 动态路由配置 & 项目快速开发


    回顾

    今天VS 2019正式发布,实验一波,你安装了么?Blog.Core 预计今天会升级到 Core 3.0 版本。

    (此处之前开场白已删除)

    一、传统的权限路由是如何设计的?

    首先先来个动图(注意这个方案已经弃用了,看看个过程了解了解即可,下边第二节(动态生成路由实例)有个动图,才是以后开发的模板方案 :)

     

    1、设计页面

     这个步骤很简单,也很普通,我们开发项目,肯定需要设计页面了,这个不属于权限路由的一部分,

    不过要说的就是,这个页面路径的设计,要考虑清楚,有的小伙伴,习惯一股脑全部并列放页面,也有详情页用 detail.vue 或者 id.vue ,还有的是 _id.vue 的,具体的这些规则,需要好好想想,设计清楚,不细说。

    2、路由实例配置

     ( routerManuaConfig.js 文件 )。相信这个路由大家都已经很明白了,只要是写过 vue 的,有一点点基础的,项目初期,我们每次开发页面,如果需要在项目中使用,就必须将路由实例添加到 Vue 实例里,其实说白了,就是一个对象,这样 Vue 实例才能调用,也就是能通过 url 访问到我们的页面。这个其实是很合理的,也是很正确的,官方也是这么处理的,比如我的 Blog.Admin 项目中,就是这么配置的:

    可是,嗯,就怕可是,现在我的页面大概有十多个,就已经一大串了,有时候修改一个路由,需要找半天,这个设计貌似不是随着项目的扩大,越来越不合理,我见过一个项目,页面有50+,那配置了一大大大页的路由 json,嗯,感觉会有解决方案的,没错,vue 官方也说提到了,具体的,下文会详细说明,咱们先安装这个方案进行下去。

    那既然是权限路由,路由有了,就该权限了。

    3、配置权限菜单(路由)

    Login.vue 、App.vue 文件)虽然我已经覆盖了 GitHub 上的代码,但是这个方案的写法还保留着,大家如果真的就喜欢这种配置,也可以看看。

     上边我们是把所有的路由都注入到了 Router 实例里,那如何渲染权限菜单呢,没错,思路很简单:

    1、当前用户登录;

    2、获取到用户标识,比如 Id ;

    3、根据用户标识获取权限菜单的 Json 数据;

    4、根据 Json  数据,渲染到左侧菜单上。//查看 App.vue 文件中的 routes 这个数组。

     其实前三步都是在 Login.vue 页面内进行的,很简单的两个方法 GetNavigationBar() ,自己看看即可。

    4、菜单表中,添加新建的页面菜单

    上边的步骤中,仅仅是将之前的页面渲染出来了,如果我们新建了一个页面,新的页面还没有显示出来,

    所以就需要在后台管理 -> 菜单权限管理 -> 菜单管理中,新建一条数据:

    5、角色与菜单的分配

    上边菜单数据配置好后 ,就需要对当前用户所对应的角色,进行权限的再次分配了,这个很好理解,这个是核心,前边所以的操作都是给这个做铺垫的:

    6、存在的小问题

    其实关于权限路由,上边五步走已经实现了,是不是感觉很简单(这里只说菜单,不说API权限问题,这个是上一篇的《二 || 完美实现 JWT 滑动授权刷新》已经说到了),但是现在面临着两个问题:

    1、就是上边提到的,需要将很多的路由一一的配置好,注入到路由实例;

    2、因为这样是一次性把全部的路由都注入,如果当前用户没有这个页面的权限,虽然左侧的菜单看不到这个页面,但是如果他强行访问这个url的话,还是会出现的;

    当然我们设计了 api 权限,他看不到内容,但是再强行访问这个URL的时候,还是会在当前页面停留,并看到页面骨架(就是没有数据,但是有按钮啥的),

    当然我们可以让用户直接跳转到403页面,嗯,也是一个办法。

     那有没有什么办法,可以动态的生成路由实例呢?欸,要是有这个想法,就是人才,请往下看。

    二、动态生成路由实例


     上边我们研究了一般实现权限路由菜单的方法,很简单,很直观,但是有两个小问题,其实对我来说,主要的还是手动配置路由的问题,页面现在的太多,每次开发一个页面,不仅需要添加路由,还需要在菜单表里,增加该菜单,然后对权限勾选该菜单,从这个逻辑来看,好像在vue项目中,配置路由就成了冗余的一步了,那如果不配置了,我们该怎么办呢?这就是今天要说的重头戏 —— 动态路由实例。

    首先再来个动图,以后就用这个方法进行快速开发了:

     

     上边的这个已经对当前路由页面做了权限匹配,我们匹配好了页面,剩下的就是开发 API 接口了,开发成功后,要注意两点:

    1、增加接口的授权特性;

    2、后台对路由进行编辑,增加api接口;

    1、在路由实例中注入项目基础路由

     这里要说的重点是 基础路由 ,那什么是基础路由呢?就是我们在项目启动的时候首次运行的页面(比如登录页),或者不需要参与数据库权限配置的某些路由(比如欢迎页,404页),比如我将基本的路由实例重新整理如下:

    在项目的 src 文件夹下,新建一个 router 文件夹,然后新建主程序文件 —— index.js ,这个文件以后就是我们的新的路由方案,用来替换之前的 router.js(现在更名为 routerManuaConfig.js

     大家可以看到,现在除了这几个基础路由,其他的都被删除了,统一通过动态注入的方式添加。

    2、根据环境配置导入组件文件

     这里其实是一个小坑,以前没有研究过,后来搜索了下,才知道的,原来 vue 动态导入 vue 文件,在不同的环境还不一样,所以这里特别用了一节来说明下,虽然内容很简单:

    还是在 src/router 文件夹下,建立两个文件,然后定义导入方法:

    _import_development.js
    //开发环境导入组件
    module.exports = file => require('@/views' + file + '.vue').default
    
    
    _import_production.js
    //生产环境导入组件
    module.exports = file => () => import('@/views' + file + '.vue')

    那如何进行导入呢,就是下边的重头戏了。

    3、动态生成权限路由(核心) 

    这个具体的code 在 src 的根目录下的 promissionRouter.js 文件里,

    A、组件导入 —— _import

    这个其实很简单,只需要根据当前的环境变量,获取指定的导入方案,传入路径地址作为参数,就可以很好的实现当然地址的注入。

    //获取组件的方法
    const _import = require('./router/_import_' + process.env.NODE_ENV)
    
    // .......
    
    //导入路径下的组件
    route.component = _import(route.path)

    B、获取数据与保存钩子 —— router.beforeEach

     这个是一个核心,就是我们每次在路由切换的时候,都需要动态处理路由实例,这里还有点儿瑕疵,我会在以后慢慢完善,但是思路就是这样的,这里的路由数据来自两个方面,一个是api接口获取,一个是将获取到的数据存放在本地:

    // promissionRouter.js

    var
    storeTemp = store; router.beforeEach((to, from, next) => { //验证Token { if (!storeTemp.state.token) { storeTemp.commit("saveToken", window.localStorage.Token) } if (!storeTemp.state.tokenExpire) { storeTemp.commit("saveTokenExpire", window.localStorage.TokenExpire) } saveRefreshtime(); if (to.meta.requireAuth) { // 判断该路由是否需要登录权限 var curTime = new Date() var expiretime = new Date(Date.parse(window.localStorage.TokenExpire)) if (storeTemp.state.token && storeTemp.state.token != "undefined") { // 通过vuex state获取当前的token是否存在 next(); } else { store.commit("saveToken", ""); store.commit("saveTokenExpire", ""); store.commit("saveTagsData", ""); window.localStorage.removeItem('user'); window.localStorage.removeItem('NavigationBar'); window.localStorage.removeItem('router'); if (global.IS_IDS4) { applicationUserManager.login(); }else{ next({ path: "/login", query: { redirect: to.fullPath } // 将跳转的路由path作为参数,登录成功后跳转到该路由 }); window.location.reload() } } } else { next(); } } //动态添加路由 { //不加这个判断,路由会陷入死循环 if (!getRouter) { if (!getObjArr('router')) { var user = window.localStorage.user ? JSON.parse(window.localStorage.user) : null; if (user && user.uID > 0) { var loginParams = { uid: user.uID }; getNavigationBar(loginParams).then(data => { console.log('router before each get navigation bar from api succeed!') if (data.success) { getRouter = data.response.children//后台拿到路由 saveObjArr('router', getRouter) //存储路由到localStorage routerGo(to, next)//执行路由跳转方法 } }); } } else { //从localStorage拿到了路由 getRouter = getObjArr('router')//拿到路由 routerGo(to, next) } } else { if (to.name && to.name != 'login') { getRouter = getObjArr('router')//拿到路由 global.antRouter = getRouter // routerGo(to, next)//执行路由跳转方法 } next() } } });

    具体的写法都很简单,就是判断和保存到路由,不细说了,大家pull 下代码,看看即可。

    C、路由过滤 ——  filterAsyncRouter

     这里要说下,这个是关键,因为我们获取到的,仅仅是一个json格式的权限列表,我们的路径还是一个字符串 url ,但是我们的动态路由,需要一个 component ,也就是刚刚我们获取到的对应组件,所以,我们需要来一个过滤器,将获取到的json数据中,每一个菜单,都添加进去一个 component 组件信息。

    // router/index.js
    
    export function filterAsyncRouter(asyncRouterMap) {
        //注意这里的 asyncRouterMap 是一个数组
        const accessedRouters = asyncRouterMap.filter(route => {
            if (route.path && !route.IsButton) {
                if (route.path === '/' || route.path === '-') {//Layout组件特殊处理
                    route.component = Layout
                } else {
                    try {
                        route.component = _import(route.path.replace('/:id',''))
                    } catch (e) {
                        try {
                            route.component = () => import('@/views' + route.path.replace('/:id','') + '.vue');
                        } catch (error) {
                            console.info('%c 当前路由 ' + route.path.replace('/:id','') + '.vue 不存在,因此如法导入组件,请检查接口数据和组件是否匹配,并重新登录,清空缓存!', "color:red")
                        }
                    }
                }
            }
            if (route.children && route.children.length && !route.IsButton) {
                route.children = filterAsyncRouter(route.children)
            }
            return true
        })
    
        return accessedRouters
    }

    D、生成路由实例与运行 —— routerGo

     上边所有的工程中,我们把路由已经生成好了,就剩下最后一步注入到实例里,没错,就是红色的部分,是不是很简单:

    // promissionRouter.js
    
    function routerGo(to, next) {
    
        //过滤路由
        getRouter = filterAsyncRouter(getRouter)
        resetRouter()
    
        //动态添加路由
        router.$addRoutes(getRouter)
    
        //将路由数据传递给全局变量,做侧边栏菜单渲染工作
        global.antRouter = getRouter
        next({ ...to, replace: true })
    }

    4、遗留问题——重登新用户路由不同步

    这个是个很搞笑的问题,怎么说搞笑呢,好像上边的 addRoutes 这个方法设计之初,没有设计过动态删除?额好吧,我就是开个玩笑,对尤大大的框架,还是很给力的。

    现在有一个问题,就是我现在是test账号,然后没有 测试管理 这一组,这个期间我刷新了页面,然后当我切换超级管理员 blogadmin 的时候,虽然有这个左侧菜单,但是点击测试页面1的时候,提示404,证明啥,证明这个路由虽然渲染出来了,但是没有导入到路由实例里,路由对象里,还是之前的,这个时候,必须刷新一下才能正常访问,请看:

    这个时候我研究出来两个方案,虽然两个在本地都可以,但是第二种在发布模式下不行,也就是dist 上传到服务器就不行了,这里做下记录,暂时使用第一种方案,可能你遇不到。

    1、在系统登出,也就是退出登录的时候,刷新页面,使用 window.location.reload(),在 App.vue 的logout 方法里;

    2、使用 router.matcher 来替换,在 router/index.js 下的 resetRouter方法,和调用处:promissionRouter.js 下的 routerGo 方法里调用;

    目前一切正常,就暂时使用刷新页面的第一种方案,第二种以后慢慢研究。

    您可参考这个issues

    三、支持多级菜单——递归

    这个其实很简单,以前我的版本只是支持两级菜单,那如果想实现无限级别的多级菜单的话,就肯定需要用到递归的概念了,简单来说,就是——递归组件。

    具体的原理相信有一点基础的小伙伴都能理解,就是写一个递归组件,将路由菜单 JSON 给递归渲染出来即可:

    1、设计菜单递归组件

    在 src -> components -> 下,新建 Sidebar.vue 组件,实现自身嵌套递归:

    <template>
        <div>
            <template v-if="item.children">
                <el-submenu :index="item.id+'index'" v-if="!item.leaf">
                    <template slot="title">
                        <i class="fa" :class="item.iconCls"></i>
                        <span class="title-name" slot="title">{{item.name}}</span>
                    </template>
                    <template v-for="child in item.children">
                        <!-- 这里实现自己递归嵌套 注意这个名称 -->
                        <sidebar
                                v-if="child.children&&child.children.length>0"
                                :item="child"
                                :key="child.path+'5'"/>
                        <el-menu-item v-else :key="child.path+'5'" :index="child.path+'5'">
                            <i v-if="child.children&&child.children.length>0" :class="item.iconCls"></i>{{child.name}}
                        </el-menu-item>
                    </template>
                </el-submenu>
            </template>
            <!-- 没有子节点,直接输出 -->
            <template v-else>
                <el-menu-item :index="item.path">
                    <i class="fa" :class="item.iconCls"></i>
                    <template slot="title">
                        <span class="title-name" slot="title">{{item.name}}</span>
                    </template>
                </el-menu-item>
            </template>
        </div>
    </template>
    
    <script>
        export default {
            name: 'Sidebar',
            props: {
                item: {
                    type: Object,
                    required: true
                }
            }
        }
    </script>

    2、在App.vue 中进行调用

    当然,你也可以放到模板组件,也就是 Layout.vue 中调用,把左侧菜单,页头,页脚放到 Lauout 布局页,不过我都放到了 App.vue 了:

     <el-menu  :default-active="$route.path"
                                     class="el-menu-vertical-demo" @open="handleopen" @close="handleclose" @select="handleselect"
                                     unique-opened router :collapse="isCollapse"
                                     background-color="#2f3e52"
                                     text-color="#fff"
                                     active-text-color="#ffd04b">
                                <sidebar v-for="(menu,index) in routes" :key="index" :item="menu" />
                            </el-menu>

    记得导入组件:

    来看看最终的效果吧:

    四、结语

     好啦,今天的文章,就先写到这里吧,最近比较累心,感觉努力付出和收获不是正比,虽然还在学习,还是想要歇一歇了。

    今天的所有内容都已经提交到 Github,然后在线 demo 也做了改变,大家可以自行查看。

    五、Github && Gitee

    .NET CORE 源码:

    Github:  https://github.com/anjoy8/Blog.Core

    Gitee :   https://gitee.com/laozhangIsPhi/Blog.Core

    VUE 项目开源代码:

    https://github.com/anjoy8/Blog.Vue

    --- ♥♥♥ ---

  • 相关阅读:
    [BZOJ4029][HEOI2015]定价
    [BZOJ3261]最大异或和
    [BZOJ3166][Heoi2013]Alo
    [BZOJ1030][JSOI2007]文本生成器
    [BZOJ2595][Wc2008]游览计划
    Speculative store buffer
    十四 oracle 视图
    十三oracle --控制结构(分支,循环,控制)
    十二、oracle 数据库(表)的逻辑备份与恢复
    十一、oracle 数据库管理员
  • 原文地址:https://www.cnblogs.com/laozhang-is-phi/p/10643993.html
Copyright © 2020-2023  润新知