前言
感觉 vue-element-admin 开源项目 对登录的权限校验和动态路由的逻辑复用性还是很强的,可以在自己的项目中直接拿来用了。为了加深记忆就写个文章记录一下学习笔记。
文章主要以记录逻辑为主,再配合些关键代码,完整代码可以直接去下载该项目的源码,或者我个人整理的精简版(把其他代码删掉,只保留了登录部分的功能)
动态路由的实现
逻辑
- 配置两个路由数组:
- 一个是公共的,
无需权限都可以加载,比如首页,登录页,404页面等; - 一个是动态的,
配置角色权限,从而动态选择是否显示;
- 一个是公共的,
- 点击登录后,会返回该用户的权限信息,拿去和动态路由数组的角色权限做配对,把该用户可以访问的路由筛选出来。
- 最后通过 vue 的 addRoutes 方法把筛选出来的数组动态添加到实际路由对象即可。
第一步:配置两个路由数组
目录:src/router/index.js
// 该 js 实现了逻辑的第一步,配置两个路由
// 公共的路由
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true // 因为无需在侧边栏显示,可以用这个字段来控制隐藏
},
{
path: '/',
component: Layout, // 这是一个框架组件,顶部和侧边栏会固定加载
redirect: '/dashboard', // 重定向的就是中间的内容部分
children: [
{
path: 'dashboard',
component: () => import('@/views/dashboard/index'),
name: 'Dashboard',
meta: { title: 'Dashboard', icon: 'dashboard'}
}
]
}
]
// 动态的路由 (最后通过 addRoutes 添加进去)
export const asyncRoutes = [
{
path: '/user',
component: Layout,
redirect: '/user/create',
children: [
{
path: 'create',
name: 'userCreate',
component: () => import('@/views/create/create'),
meta: { roles: ['admin'] } // 只有管理员才能访问
},
{
path: 'list',
name: 'userList',
component: () => import('@/views/user/list'),
meta: { roles: ['admin','editor'] } // 管理员和编辑可以访问
},
]
}
]
// 先把公共路由添加进路由实例,动态的路由手动添加
const createRouter = () => new Router({
// mode: 'history', // 是否使用 history 模式
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
第二第三步:在路由守卫处实现
点击登录后,页面跳转前会进入到路由守卫。
路由守卫的逻辑可以直接看代码,有注释还是挺好理解的,然后也补充几个细节点配合阅读:
- 页面加载的进度条是用了第三方库 nprogress 实现,简单易用;
- 第一次登录其实会
发送两次请求,第一次是点击登录时获取 token ,第二次就是在路由守卫这里获取用户拥有的权限;- 拿到当前用户权限后,就会去筛选我们配置好的动态路由数组,再用
addRoutes动态添加;- 路由重定向的情况:当没有 token 而且要去其他路由比如 “用户列表” 的时候,就可以把“用户列表”路由先赋值给参数,在登录页重新登录后,会先判断有没有该参数来实现重定向跳转。
目录:src/promission.js (会在 main.js 中引入)
// 省略 import 引入的代码哦!
NProgress.configure({ showSpinner: false }) // 进度条设置,把转圈圈关掉
const whiteList = ['/login'] // 设置白名单
// 路由守卫
router.beforeEach(async (to, from, next) => {
// 进度条开始
NProgress.start()
// 设置标题
document.title = getPageTitle(to.meta.title)
// 第一次登录后会把 token 存在 cookie 中,此处就是通过 cookie 拿 token
const hasToken = getToken()
if (hasToken) {
/* 有 token,证明已成功登录 */
if (to.path === '/login') {
next({ path: '/' }) // 如果要去登录页,会自动跳转到首页,然后会再次进入这个路由守卫
NProgress.done()
} else {
// 在 vuex 中获取用户权限,因为第一次登录时会把请求回来的用户权限存在 vuex 中
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next() // 如果有 token 并且有权限,直接跳转
} else {
try {
// 有 token 无权限,这就是用户第一次登录的情况,需要发送请求获取
const { roles } = await store.dispatch('user/getInfo')
// 筛选动态路由数组
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 动态添加筛选后的路由数组
router.addRoutes(accessRoutes)
// replace: true 表示不会记录到浏览器 history
next({ ...to, replace: true })
} catch (error) {
// 获取用户权限报错会要求重新登录,并且删掉 token
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`) // 重定向
NProgress.done()
}
}
}
} else {
/*没有 token,证明 token 过期了或者未登录,*/
if (whiteList.indexOf(to.path) !== -1) {
next() // 如果是白名单,直接跳转
} else {
// 要去其他路由,先把路由地址赋值给跳转参数,登陆成功后拿出来跳转。
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// 结束进度条
NProgress.done()
})
登录页实现重定向
上面说了,情况有可能是 token 过期或者还没登录而是通过其他链接跳转到内部页面,需要先登录才可以跳转;
此时就会把要去的路由地址作为参数带到登录页,登录后会先判断有没有 redirect 参数。
目录:src/views/login/index.vue
methods:{
// 点击登录调用的方法
handleLogin() {
// 字段校验(element-ui的东西)
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true // 登录时按钮里面会有个圈在转
this.$store.dispatch('user/login', this.loginForm) // 发送登录请求获取 token
.then(() => {
// 重定向,先判断有没有redirect参数
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
this.loading = false
})
.catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
}
到这整个动态添加路由和登录权限验证就已经实现了,至于 vuex 中怎么筛选合并路由数组,怎么发送请求的代码逻辑这里就不说了。
在此之前其实就已经实现了权限检验和动态路由的添加,接下来配合视图渲染,把我们筛选合并后的路由数组渲染到菜单上就大功告成了。
菜单渲染
菜单渲染是使用了 element-ui 的侧边栏组件实现,不过里面细节还是挺多挺复杂的。也参考了慕课网的课程文档《「小慕读书」管理后台》
目录:src\layout\components\Sidebar\SidebarItem.vue
// 该组件就是负责渲染侧边栏的每一个目录
<template>
<div v-if="!item.hidden">
<!-- template 部分渲染没有子目录的目录 -->
<!-- hasOneShowingChild:判断是否只有一个需要显示的子路由 -->
<!-- !onlyOneChild.children||onlyOneChild.noShowingChildren:判断需要展示的子菜单是否包含 children 属性,
如果包含,则说明子菜单可能存在孙子菜单,此时则需要再判断 noShowingChildren 属性 -->
<!-- !item.alwaysShow:判断路由中是否存在 alwaysShow 属性,只要配置了 alwaysShow 属性就会直接进入下面的那部分-->
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<!-- el-submenu 部分是渲染含有子目录的目录 -->
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<!-- 子目录是嵌套本组件实现,所以可以理解为整个组件只会渲染一个目录,子目录是递归整个组件产生 -->
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
// 如果 children 中的路由包含 hidden 属性,则返回 false
if (item.hidden) {
return false
} else {
// 将子路由赋值给 onlyOneChild,用于只包含一个路由时展示
this.onlyOneChild = item
return true
}
})
// 如果过滤后,只包含展示一个路由,则返回 true
if (showingChildren.length === 1) {
return true
}
// 如果没有子路由需要展示,则将 onlyOneChild 的 path 设置空路由,并添加 noShowingChildren 属性,表示虽然有子路由,但是不需要展示子路由
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
// 返回 false,表示不需要展示子路由,或者超过一个需要展示的子路由
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>
总结
放一张慕课网课程的文档《「小慕读书」管理后台》里面整理的图片,虽然我是觉得看代码还简单点... 但是看完代码再看这个图应该会加深理解很多。
路由守卫的代码逻辑图:
新手上路,有错误的地方热烈欢迎指正。