松哥之前写了两篇文章和大家分享了 TienChin 我的项目中的菜单数据问题,还没看过的小伙伴请戳这里:
- Vue 里,多级菜单要如何设计才显得业余?
- TienChin 我的项目动静菜单接口分析
这两篇文章次要是和大家阐明了后端如何依据以后登录用户,动静生成一个菜单 JSON。
那么当初的问题就是,以后端收到后端返回来的菜单 JSON 之后,该如何将之渲染进去?这就是咱们目前所面临的问题了。
TienChin 我的项目基于 RuoYi 脚手架来实现,所以本文的剖析你也能够看作是对 RuoYi-Vue3
我的项目的剖析。
1. 整体思路
首先咱们来梳理下整体上的实现思路,首先一点: 整体思路和 vhr 截然不同。
思考到有的小伙伴可能曾经遗记 vhr 中前端动静菜单的实现思路了,因而本文再和大家剖析一下。
为了确保在所有的 .vue
文件中都能拜访到到菜单数据,所以抉择将菜单数据存入 vuex 中,vuex 是 vue 中一个存储数据的公共中央,所有的 .vue
文件都能够从 vuex
中读取到数据。存储在 vuex
中的数据实质上是存在内存中,所以它有一个特点,就是浏览器按 F5 刷新之后,数据就没了。所以在产生页面的跳转的时候,咱们应该去辨别一下,是用户点击了页面上的菜单按钮之后产生了页面跳转还是用户点击了浏览器刷新按钮(或者按了 F5)产生了跳转。
为了实现这一点,咱们须要用到 vue 中的路由导航守卫性能,对于咱们 Java 工程师而言,这些可能听起来有点生疏,然而你把它当作 Java 中的 Filter 来对待就好了解了,实际上咱们视频中和小伙伴们解说的时候就是这么类比的,将一个新事物跟咱们脑海中一个已有的相熟的事物进行类比,就很容易了解了。
vue 中的导航守卫就相似一个监控,它能够监控到所有的页面跳转,在页面跳转中,咱们能够去判断一下 vuex 中的菜单数据是否还在,如果还在,就阐明用户是点击了页面上的菜单按钮实现了跳转的,如果不在,就阐明用户是点击了浏览器的刷新按钮或者是按了 F5 进行页面刷新的,此时咱们就要连忙去服务端从新加载一下菜单数据。
—xxxxxxxxxxxxxxxxxx—
整体上的实现思路就是这样,接下来咱们来看看一些具体的实现细节。
2. 实现细节
2.1 加载细节
首先咱们来看看加载的细节。
小伙伴们晓得,单页面我的项目的入口是 main.js
,路由加载的内容在 src/permission.js 文件中,该文件在 main.js 中被引入,src/permission.js 中的前置导航守卫内容如下:
router.beforeEach((to, from, next) => {NProgress.start()
if (getToken()) {to.meta.title && useSettingsStore().setTitle(to.meta.title)
/* has token*/
if (to.path === '/login') {next({ path: '/'})
NProgress.done()} else {if (useUserStore().roles.length === 0) {
isRelogin.show = true
// 判断以后用户是否已拉取完 user_info 信息
useUserStore().getInfo().then(() => {
isRelogin.show = false
usePermissionStore().generateRoutes().then(accessRoutes => {
// 依据 roles 权限生成可拜访的路由表
accessRoutes.forEach(route => {if (!isHttp(route.path)) {router.addRoute(route) // 动静增加可拜访路由表
}
})
next({...to, replace: true}) // hack 办法 确保 addRoutes 已实现
})
}).catch(err => {useUserStore().logOut().then(() => {ElMessage.error(err)
next({path: '/'})
})
})
} else {next()
}
}
} else {
// 没有 token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,间接进入
next()} else {next(`/login?redirect=${to.fullPath}`) // 否则全副重定向到登录页
NProgress.done()}
}
})
我跟大家捋一下这个前置导航守卫中的思路:
- 首先调用 getToken 办法,这个办法实际上是去 Cookie 中拿认证 Token,也就是登录胜利后后端返回给前端的那个 JWT 字符串。
- 如果 getToken 办法有返回值,阐明用户曾经登录了,那么进入到 if 分支中,如果 getToken 没拿到值,阐明用户未登录,未登录的话,又分为两种状况:i:拜访的指标地址处于免登录白名单中,那么此时间接拜访即可;ii:拜访的指标地址不在白名单中,那么此时就跳转到登录页面去,跳转的时候同时携带一个 redirect 参数,这样不便在登录胜利之后,再跳转回拜访的指标页面。这个免登录拜访的白名单,是一个在 src/permission.js 文件中定义的变量,默认有四个门路,别离是
['/login', '/auth-redirect', '/bind', '/register']
。 - 如果 getToken 拿到了值,阐明用户曾经登录了,此时又分状况:如果用户拜访的门路是登录页面,那么就给他重定向到我的项目首页(也就是在曾经登录的状况下,不容许用户再次拜访登录页面);如果用户拜访的门路不是登录页面,那么首先判断 vuex 中的 roles 是否还有值?如果有值,阐明以后就是用户点击了一个菜单按钮进行跳转的,那么间接跳转就行了;如果没有值,阐明用户是按了浏览器的刷新按钮或者是 F5 按钮刷新进行的页面跳转,那么此时首先调用 getInfo 办法(位于 src/store/modules/user.js 文件中)去服务端从新加载以后用户的根本信息、角色信息以及权限信息,而后再调用 generateRoutes 办法(位于 src/store/modules/permission.js 文件中)去服务端加载路由信息,并将加载到的路由信息放入到 router 对象中(前提是这个路由对象不是一个 http 链接,就是一般的路由地址)。
这就是动静路由的加载整体思路。
在第三步骤中,波及到两个办法,一个是 getInfo 还有一个 generateRoutes,这两个办法也都比拟要害,咱们再来略微看下。
2.2 getInfo
首先这个加载用户信息的办法位于 src/store/modules/user.js
文件中,换言之,这些用户的根本信息加载到之后,是存储在 vuex 中的,如果刷新浏览器这些数据就会失落:
getInfo() {return new Promise((resolve, reject) => {getInfo().then(res => {
const user = res.user
const avatar = (user.avatar == "" || user.avatar == null) ? defAva : import.meta.env.VITE_APP_BASE_API + user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的 roles 是否是一个非空数组
this.roles = res.roles
this.permissions = res.permissions
} else {this.roles = ['ROLE_DEFAULT']
}
this.name = user.userName
this.avatar = avatar;
resolve(res)
}).catch(error => {reject(error)
})
})
},
办法的逻辑其实倒没啥好说的,联合服务端返回的 JSON 格局,应该就很好了解了(局部 JSON):
{
"permissions":["*:*:*"],
"roles":["admin"],
"user":
"userName":"admin",
"nickName":"TienChin 健身",
"avatar":"",
}
}
另外再强调下,之前在 vhr 中,咱们是将申请封装成了一个 api.js 文件,里边有罕用的 get、post、put 以及 delete 申请等,而后在须要应用的中央,间接去调用这些办法发送申请即可,然而在 TienChin 中,脚手架的封装是将所有的申请都提前对立封装好,在须要的时候间接调用封装好的办法,连申请地址都不必传递了(封装的时候就曾经写死了),所以小伙伴们看下面的 getInfo 办法只有办法调用,没有传递门路参数等。
2.3 generateRoutes
generateRoutes 办法则位于 src/store/modules/permission.js 文件中,这里值得说道的中央就比拟多了:
generateRoutes(roles) {
return new Promise(resolve => {
// 向后端申请路由数据
getRouters().then(res => {const sdata = JSON.parse(JSON.stringify(res.data))
const rdata = JSON.parse(JSON.stringify(res.data))
const defaultData = JSON.parse(JSON.stringify(res.data))
const sidebarRoutes = filterAsyncRouter(sdata)
const rewriteRoutes = filterAsyncRouter(rdata, false, true)
const defaultRoutes = filterAsyncRouter(defaultData)
const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
asyncRoutes.forEach(route => { router.addRoute(route) })
this.setRoutes(rewriteRoutes)
this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
this.setDefaultRoutes(sidebarRoutes)
this.setTopbarRoutes(defaultRoutes)
resolve(rewriteRoutes)
})
})
}
首先大家看到,服务端返回的动静菜单数据解析了三次,别离拿到了三个对象,这三个对象都是未来要用的,只不过应用的场景不同,上面联合页面的显示跟大家细说。
- 首先是调用 filterAsyncRouter 办法,这个办法的核心作用就是将服务端返回的 component 组件动静加载为一个 component 对象。不过这个办法在调用的过程中,前面还有两个参数,第二个是 lastRouter 在该办法中并无实质性作用;第三个参数则次要是说是否须要对 children 的 path 进行重写。小伙伴们晓得,服务端返回的动静菜单的 path 属性都是只有一层的,例如一级菜单系统管理的 path 是 system,二级菜单用户治理的 path 则是 user,那么用户治理最终拜访的 path 就是
system/path
,如果第三个参数为 true,则会进行 path 的重写,将 path 最终设置正确。 - 所以这里的 sidebarRoutes 和 defaultRoutes 只是能用于菜单渲染(因为这两个里边的菜单 path 不对),而最终的页面跳转要通过 rewriteRoutes 才能够实现。
- 除了服务端返回的动静菜单,前端自身也定义了一些根底菜单,前端的根底菜单分为两大类,别离是 constantRoutes 和 dynamicRoutes,其中 constantRoutes 是固定菜单,也就是一些跟用户权限无关的菜单,例如 404 页面、首页等;dynamicRoutes 是动静菜单,也就是也依据用户权限来决定是否展现的菜单,例如调配用户、字典数据、调度日志等等。
- filterDynamicRoutes 办法则是将前端提前定义好的 dynamicRoutes 菜单进行过滤,找出那些合乎以后用户权限的菜单将之增加到路由中(这些菜单都不须要在菜单栏渲染进去)。
- 接下来波及到四个不同的保留路由数据的变量,别离是 routes、addRoutes(经松哥剖析,这个变量并无理论作用,能够删除之)、defaultRoutes、topbarRouters 以及 sidebarRouters,四个路由变量的作用各有不同:
routes:
routes 中保留的是 constantRoutes 以及服务端返回的动静路由数据,并且这个动静路由数据中的 path 曾经实现了重写,所以这个 routes 次要用在两个中央:
- 首页的搜寻上:首页的搜寻也能够依照门路去搜寻,所以须要用到这个 routes,如下图:
- 用在 TagsView,这个中央也须要依据页面渲染不同的菜单,也是用的 routes:
sidebarRouters:
这个就是大家所熟知的侧边栏菜单了,具体展现是 constantRoutes+ 服务端返回的菜单,不过这些 constantRoutes 基本上 hidden 属性都是 false,渲染的时候是不会被渲染进去的。
topbarRouters:
这个是用在 TopNav 组件中,这个是将零碎的一级菜单在头部显示进去的,如下图:
一级菜单在顶部显示,右边显示的都是二级三级菜单,那么顶部菜单的渲染,用的就是这个 topbarRouters。
defaultRoutes:
想要开启顶部菜单,须要在 src/layout/components/Settings/index.vue 组件中设置,如下图:
开启顶部菜单之后,点击顶部菜单,右边菜单栏会跟着切换,此时就是从 defaultRoutes 中遍历出相干的菜单设置给 sidebarRouters。
好了,这就是这四个 routes 变量的作用,诚实说,脚手架中这块的代码设计有点凌乱,没必要搞这么多变量,等松哥抽空给大家优化下。
generateRoutes 办法最终会返回 rewriteRoutes 变量到后面说的那个前置导航守卫中,最终前置导航守卫将数据增加到 router 中。
菜单的渲染都是在 src/layout/components/Sidebar/index.vue 中实现的,看了下都是惯例操作,没啥好说的。
3. 小结
好啦,这就是 RuoYi-Vue3 中的动静菜单渲染逻辑,不晓得小伙伴们看明确没有?视频行将奉上,对视频感兴趣的小伙伴请戳这里:TienChin 我的项目配套视频来啦。