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

回顾

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

 

哈喽你们周三好!原本今天呢要写 Id4 了,可是写到了一半,忽然有人问到了关于 Blog.Admin 管理后台的一些问题,想着这个先后端系列是第一个项目,并且是之后学习的基础,不能草草了事,因此就把重心往 Blog.Core + Blog.Admin 两个项目上靠拢了下,明天再更新 IdentityServer4 吧,由于从上周末到今天,这几天修改了一些东西,这里就不一一的写文章了,若是你是跟着系列看的小伙伴,应该知道我写的是什么意思,若是是路人,额不敢保证你能看懂我在说什么,总结来讲有如下五点:vue

 

一、Blog.Core 项目增长了单元测试项目 Blog.Core.Tests ,不过就几行代码,作了个小测试而已;git

 

二、Blog.Core 项目配置了 AOP Sql 的功能,就是你们在操做仓储的时候,会生成指定的Sql语句,保存到日志中,方便查看,这是SqlSugar的核心功能;github

 

 

三、Blog.Core 项目增长多线程日志功能,防止出现死锁,能够查看 BlogLogAOP.cs ,固然你能够封装起来;
vue-router

 具体查看方法:static void OutSql2Log(string dataIntercept)数据库

 

四、DDD开发之 Christ3D 项目完成了末尾开发,实现了 Identity 登陆(注意不是 Id4 ),并完成部署json

地址:http://123.206.33.109:4773后端

 

五、Blog.Admin 项目实现数据库配置动态路由,这个是今天的重点内容,其余的都很简单,你们看看就行。api

而后也简单说说,如何在 Blog.Admin 项目中,快速添加页面,虽然这个真的很简单。数组

 

好啦,立刻开始今天的 Share Time。

由于这个系列我尚未写过一些文章,因此今天就把两种写法都写上,既补充了以前的方案,又设计新的方案。

 

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

首先先来个动图(注意这个方案已经弃用了,看看个过程便可,下边第二节有个动图,才是之后开发的模板):

 

 

 

一、设计页面

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

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

 

二、路由实例配置

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

 

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

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

 

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

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

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

一、当前用户登陆;

二、获取到用户标识,好比 Id ;

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

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

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

 

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

上边的步骤中,仅仅是将以前的页面渲染出来了,若是咱们新建了一个页面,新的页面尚未显示出来,

因此就须要在后台管理 -> 菜单权限管理 -> 菜单管理中,新建一条数据:

 

五、角色与菜单的分配

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

 

六、存在的小问题

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

一、就是上边提到的,须要将不少的路由一一的配置好,注入到路由实例;

二、由于这样是一次性把所有的路由都注入,若是当前用户没有这个页面的权限,虽然左侧的菜单看不到这个页面,可是若是他强行访问这个url的话,仍是会出现的;

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

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

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

 

2、动态生成路由实例


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

 

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

 

 

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

一、增长接口的受权特性;

 

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

 

 

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

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

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

 

 你们能够看到,如今除了这几个基础路由,其余的都被删除了,统一经过动态注入的方式添加。

 

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

 这里实际上是一个小坑,之前没有研究过,后来搜索了下,才知道的,原来 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')

 

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

 

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

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

A、组件导入 —— _import

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

//获取组件的方法
const _import = require('./router/_import_' + process.env.NODE_ENV)

// .......

//导入路径下的组件
route.component = _import(route.path)

 

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

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

var storeTemp = store;
router.beforeEach((to, from, 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 {
            console.log(to)
           if(to.name&&to.name != 'login'){
               getRouter = getObjArr('router')//拿到路由
               global.antRouter = getRouter
               routerGo(to, next)//执行路由跳转方法
           }
            next()
        }
    }
});

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

 

 

C、路由过滤 ——  filterAsyncRouter

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

//遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap) {
    //注意这里的 asyncRouterMap 是一个数组
    const accessedRouters = asyncRouterMap.filter(route => {
        if (route.path) {
            if (route.path === '/') {//Layout组件特殊处理
                route.component = Layout
            } else {
                route.component = _import(route.path)
            }
        }
        if (route.children && route.children.length) {
            route.children = filterAsyncRouter(route.children)
        }
        return true
    })

    return accessedRouters
}

 

 

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

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

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

 

四、遗留问题——重登新用户路由不一样步

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

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

 

 

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

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

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

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

您可参考这个issues

 

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

这个其实很简单,之前个人版本只是支持两级菜单,那若是想实现无限级别的多级菜单的话,就确定须要用到递归的概念了,简单来讲,就是——递归组件。

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

一、设计菜单递归组件

在 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>

 

二、在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>

 

记得导入组件:

 

来看看最终的效果吧:

 

 

 

4、结语

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

今天的全部内容都已经提交到 Github,而后在线 demo 也作了改变,你们能够自行查看。

 

5、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

 

--- ♥♥♥ ---