项目中的路由配置
简单项目的路由配置
这一块其实并没有什么说的,根据vue-router的文档,创建router/index.js 文件,在其中配置路由信息。配置好之后导出,在main.js中导入并挂载就好了。
中台项目的路由配置
因为复杂中台项目的中的页面众多,不可能把所有的业务都集中在一个文件上进行管理和维护,所以我们需要把路由根据业务或者页面进行了模块拆分。
并且最重要的是,前端的页面主要分为两部分:
- 一部分是所有人都可以访问的,(静态路由,一直存在的路由,公共的路由)比如登录页、游客模式下的首页、404报错页面等等
- 另一部分是只有拥有权限的人才可以访问的,(动态路由,根据用户角色的权限动态添加和删除的路由)比如员工管理、考勤审批、财务审批等等,需要不同的角色才可以访问看到并且能够进行操作。
拆分模块的好处:
- 便于维护,相同父子级目录放到一起,甚至还可以继续拆分
- 方便我们后面进行权限控制功能的实现
注意点:
- 这里的动态路由并不是指路由传参的的动态路由,而是往router实例对象中动态添加的路径,加进去就能访问到,没加进去就无法访问
- 动态路由:需要有权限的人,才能访问的路由(有权限,给你新增,没权限,给你移除)
- 静态路由:所有人都能访问到的,一直存在的路由
上述内容反复强调,思想很重要,因为不同框架不同语言都是类似的,尤其是vue3,除了语法有些变化,路由这一块并没有太大变化,就是创建路由实例的方式变了。
路由页面整理
创建静态路由
src/router/index.js
// 静态路由表 => 静态路由(不需要权限即可访问的)
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: 'Dashboard', icon: 'dashboard' }
}
]
},
// 没有匹配到的页面, 走404
{ path: '*', redirect: '/404', hidden: true }
]
- 注意:上面的代码中的最后一行,就是没有匹配到的页面重定向到404的这个路由配置,按照书写习惯,我们一般要把这个配置放在所有的路由配置最下面,因为我们后面要动态添加,会把这个页面挤上去,所以这里我们删掉,后面在添加动态路由的时候再一起加上去。
业务模块的快速搭建
这个纯技巧,使用git bash(linux) 环境下的mkdir指令,快速创建多个文件夹。比如说下main的结构,可以使用以下指令
// 快速批量创建文件夹
mkdir approvals attendances departments employees permission salarys setting social
// 快速批量创建文件
touch approvals.js attendances.js departments.js employees.js permission.js setting.js salarys.js salarys.js social.js
对路由模块进行拆分
之前的模式
因为不同的模块都是挂载到layout上面的,那么这里的首页跟组织架构都应该是在Layout下面的children里面
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
// 首页模块
{
path: 'dashboard',
name: 'dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '首页', icon: 'dashboard' }
},
// 组织架构模块
{
path: '',
name: 'departments',
component: () => import('@/views/departments'),
meta: { title: '组织架构', icon: 'dashboard' }
}
]
},
初步拆分
这里的意思是拆分成了互不影响的两块,挂载的时候意义一样
// 首页模块
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '首页', icon: 'dashboard' }
}
]
},
// 组织架构模块
{
path: '/departments',
component: Layout,
children: [
{
path: '',
name: 'departments',
component: () => import('@/views/departments'),
meta: { title: '组织架构', icon: 'dashboard' }
}
]
},
再次拆分:那就是拆分出去文件夹了
src/router/modules/departments.js这里以departments为例,导出这个对象
import Layout from '@/layout'
export default {
path: '/departments',
component: Layout,
children: [{
path: '',
name: 'departments',
component: () => import('@/views/departments'),
meta: { title: '组织架构', icon: 'tree' }
}]
}
src/router/index.js导入并挂载,额,这并不是挂载,就是创建了一个动态路由,是一个数组,把这个路由对象放进数组中,将来把这个数组跟静态路由的数组合并。如果说不考虑动态路由的情况,想要按照这种方式拆分出去的话,直接把这个导入的路由对象放进index.js中的routes[]数组里面就行了,效果一样。
import departmentsRouter from './modules/departments'
侧边栏的渲染展示与路由配置相关
这个项目呢是根据花裤衩的 Vue Element Admin 这个项目基础上进行修改的,然后侧边栏的一级目录跟二级目录就是根据我们的路由配置进行渲染的。主要思想就是对路由实例上的路由规则进行遍历,如果说没有children子路由或者子路由只有一个,那么就直接把这个规则或者只有一个的子路由渲染成一级目录,如果说有children子路由数量大于等于2,那么就渲染成一级路由加二级路由的形式。
预览地址:panjiachen.gitee.io/vue-element…
GitHub地址:github.com/PanJiaChen/…
文档地址:panjiachen.gitee.io/vue-element…
- 对路由规则进行遍历,然后被遍历的这个路由规则routes来自于仓库里的permission(权限模块)里面返回的动态路由规则(也就是我们根据权限最后生成的动态路由),如果是
this.$router.options.routes获取到的路由,这里拿到的是src/router/index.js文件里刚创建路由实例时候的带的路由规则。 - 下面是渲染具体侧边栏元素的部分,注意:这里依赖了一个属性叫hidden,我们可以在路由规则中添加一个叫hidden的变量,赋值false或者true来决定这个路由能不能被渲染,true就是隐藏不渲染,false就是不隐藏,渲染。
- 最终渲染,处理了一级目录和二级目录
- 看一眼他处理一级目录跟二级目录的方法
// index.vue
routes() { // 获取路由实例上的路由规则
// return this.$router.options.routes // 这种方式获取的路由规则只是创建路由实例时候的规则
return this.$store.state.permission.routes // 访问vuex中的规则
},
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
// SiderbarItem.vue
methods: {
// 将每一个路由规则中的children进行遍历过滤, 过滤掉添加hidden: true的子路由规则, 筛选出的数组长度为1, 直接将这个子规则渲染成一级导航链接, 如果长度为0直接将自己渲染成一级导航
hasOneShowingChild(children = [], parent) {
// 将每一个路由规则的children过滤,将没有加hidden为true规则筛选出来
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// console.log(showingChildren)
// 如果筛选的children数组得到的数组长度为1, 直接将这个子规则渲染成一级导航链接
if (showingChildren.length === 1) {
return true
}
// 如果所有的子路由规则都是hidden, 直接将自己渲染成一级导航
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
return false
},
// 返回了路由规则的 path的路径 '/dashboard' '/department'
resolvePath(routePath) {
console.log(routePath)
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
- 逻辑:
把静态路由和动态路由暂时合并
我们上面把所有动态路由拆分处理好之后,全部导入router/index.js中,因为开发的原因暂时进行合并,然后创建新路由并导出。在这里,{ path: '*', redirect: '/404', hidden: true } 被放到了最后。
// 引入多个模块的规则
import approvalsRouter from './modules/approvals'
import departmentsRouter from './modules/departments'
import employeesRouter from './modules/employees'
import permissionRouter from './modules/permission'
import attendancesRouter from './modules/attendances'
import salarysRouter from './modules/salarys'
import settingRouter from './modules/setting'
import socialRouter from './modules/social'
// 静态路由规则
export const constantRoutes =
[
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true // 可以控制当前路由是否在左侧菜单显示
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: '/dashboard',
name: 'dashboard',
component: () => import('@/views/dashboard'),
meta: { title: '首页', icon: 'dashboard' }
// title是控制文本 icon是控制图标
// hidden: true// 可以控制当前路由是否在左侧菜单显示
}]
},
// 导入的规则
{
path: '/import',
component: Layout,
children: [{
path: '',
name: 'import',
component: () => import('@/views/import'),
hidden: true // 可以控制当前路由是否在左侧菜单显示
}]
}
// 404 page must be placed at the end !!!
]
// 动态路由表 => 动态路由(需要权限才可以访问的) 我们这里准备一个数组存放
export const asyncRoutes = [
approvalsRouter,
departmentsRouter,
employeesRouter,
permissionRouter,
attendancesRouter,
salarysRouter,
settingRouter,
socialRouter
]
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }), // 管理滚动行为, 让页面切换时回到顶部
routes: [...constantRoutes, ...asyncRoutes, { path: '*', redirect: '/404', hidden: true }] // 临时合并动态路由和静态路由
})
const router = createRouter()
export default router
(重要)RBAC(Role-Based Access control) ,基于角色的权限分配解决方案
一、强行解释
这个东西怎么说呢,思想很简单,实现也很简单,但是这玩意是一个开发思想,虽然简单,但是重要。因为核心思路就是依赖于router路由的动态路由方法,所以放在这个路由板块一起讲。
- 权限模式如下:纯手绘
详细解释
- 简单介绍一下:就是我们先设置权限点,这个权限点其实就是路由规则,每个权限点都是单独的,一个。然后再新建角色,权限点是固定不变的,你动态路由有几个那就设几个,但是角色可以随便设。我们把权限点添加进角色内部,比如说:
- 设置一个角色总经理(代号A),分配权限给角色:
[1,2,3,4,5,6],那就是说这六个路由的页面他都有权限访问。 - 再设置一个角色人事经理(代号B),分配权限给角色:
[1,3,4,7,8,9],那就是拥有这里面的5个页面权限,这种角色可以有很多个自定义的。 - 然后再创建员工,小张、小李、小王
- 分配角色给员工:小张为总经理(A),小李为人事经理(B),小王为超级管理员(A+B)
- 最终员工得到的权限点,就是数组的并集:
- 小张:
(A)[1,2,3,4,5,6] - 小李:
(B)[1,3,4,7,8,9] - 小王:
(A+B)[1,2,3,4,5,6,7,8,9]就好像三只松鼠旗舰店里,有各种各样的大礼包,A礼包,B礼包,C礼包。这礼包就是角色,礼包里面的单袋零食就是权限点,每种类型的礼包就是角色,我们消费者就是员工,然后不同的礼包组合有不同的优惠,我买AB礼包,你买BC礼包,最终得到里面的小零食。
二、页面的实现
这里那就是常规的增删改查了,需要后端配合收发数据。
- 先设置权限点页面,这个一般是死的,除非你新加独立带权限的路由:
- 创建角色并分配权限点,新建角色,根据接口把所选权限绑定的参数传给后端,更新该角色的权限点。
修改角色权限的话需要进行回显哦
- 员工管理页面,给员工分配角色,上传信息同样根据接口来。
三、前端权限:页面访问权===>路由!
上面,我们设置了权限点,把权限点配置给了角色,再给员工添加角色,最终员工拥有了对应角色所拥有的权限点。
那么在用户登录获取资料的时候,会自动查出该用户拥有哪些权限,这个权限需要和我们的菜单还有路由有效结合起来。
而动态路由表其实就是根据用户的实际权限来访问的,接下来我们操作一下:
- 我们员工登录成功时,他身上会带个有关权限点的参数,比如长这样:
- 上面这个数组的数据从哪来呢,已经说了很多遍了。跟我们这个权限点的标识有关:
虽然说这标识只是一个字符串,但是必须要跟路由规则的信息对应
四、addRoutes的基本使用
先看图:这是登录过程的权限拦截流程
src/router/index.js中先把我们刚刚为了开发做的暂时的合并去掉,去掉之后首页的侧边栏就还剩下静态的路由了
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: [
// 待会加回来
...constantRoutes // 静态路由, 首页
// ...asyncRoutes // 所有的动态路由
]
})
- 这个动态添加的过程是写在路由前置守卫里面的,本来是写在router/index.js中的,但是这里我们抽了出来,在根目录创建一个
permission.js文件,把路由导进去,设置路由的前置守卫跟后置守卫。 - 在
permission.js文件中,我们通过addRoutes动态添加试一下。然后发现追加是追加进去了,但是没有效果。就是说可以新增出来,在router的实例对象上的路由规则里已经有了,但是菜单并没有渲染出来===>router.options.routes(拿的是默认配置的项, 拿不到动态新增的) 不是响应式的!
为了能正确的显示菜单, 为了能够将来正确的获取到, 目前用户的路由, 我们需要用vuex管理routes路由数组
import { asyncRoutes } from '@/router/index'
router.beforeEach(async(to, from, next) => {
...
...
if (!store.state.user.userInfo.userId) {
// 调用获取信息的action
const res = await store.dispatch('user/getUserInfo')
console.log('进行权限处理', res)
// 拿到权限信息之后, 应该根据权限信息, 从动态路由模块中筛选出, 需要追加的路由,
// 追加到routes规则中, addRoutes
router.addRoutes(asyncRoutes)
next(to.path) // 重新执行一次
// 要出门了 打开门后 门口没有路 这个操作 next(to.path) 自己铺一条路
// asyncRoutes 在首页使用this.$router.options.routes获取路由规则的时候
// 只获取到了四条静态路由规则, 后来新增的规则并没有获取到 addRoutes方法新增的规则不是响应式的
}
...
...
})
- 新建vuex权限模块:专门维护管理所有的路由routes数组===>响应式
src/store/modules/permission.js建好之后记得导入src/store/index.js挂上去
import { constantRoutes } from '@/router'
const state = {
// 路由表, 标记当前用户所拥有的所有路由
routes: constantRoutes // 默认静态路由表
}
const mutations = {
// otherRoutes登录成功后, 需要添加的新路由
setRoutes(state, filterRoutes) {
// 静态路由基础上, 累加其他权限路由
state.routes = [...constantRoutes, ...filterRoutes]
}
}
const actions = {}
export default {
namespaced: true,
state,
mutations,
actions
}
src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
import permission from './modules/permission'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
settings,
user,
permission
},
getters
})
export default store
- 在
src/store/modules/permission.js里对路由进行筛选。将来登录成功时,个人信息中会有一个roles的menus(根据接口来)信息,我们基于这个menus过滤出我们给这个员工或者用户的动态路由。
menus中的标识又该怎么和路由对应?将路由模块name属性命名和权限标识一致,这样只要标识能对上,就说明用户拥有了该权限。这一步,在我们命名路由的时候已经操作过了
- 接下来最重要的一步:在
src/store/modules/permission.js的actions中创建一个方法,用于过滤出对应的路由: 从router里面拿到静态路由数组constantRoutes和所有的动态路由数组asyncRoutes,用这个asyncRoutes来滤出我们要的数组。过滤出来filterRoutes的时候,返回出去用,本地也存一份。
import { asyncRoutes, constantRoutes } from '@/router'
const actions = {
// 筛选路由权限
filterRoutes(context, menus) {
filterRoutes({ commit }, menus) {
const filterRoutes = asyncRoutes.filter(item => menus.includes(item.children[0].name))
// 提交mutation设置动态路由
commit('setRoutes', filterRoutes) // 到此只是完成了vuex数据的处理
return filterRoutes // 必须返回 供addRoutes新增规则用
}
context.commit('setRoutes', filterRoutes)
return filterRoutes
}
}
- 在 permission 拦截的位置,调用关联action,拿到过滤出来的filterRoutes,并且用addRoutes添加进路由实例的routes中
if (!store.state.user.userInfo.userId) {
// 在放行之前获取个人资料 触发action
const { roles: { menus }} = await store.dispatch('user/getUserInfo')
// 提交action 将过滤出来的动态路由存储在vuex中
const filterRoutes = await store.dispatch('permissions/filterRoutes', menus)
// addRoutes已知缺陷 刷新会留白
router.addRoutes(filterRoutes)
// next({ path: to.path, replace: true }) // 优化 replace: true 路由跳转的时候地址替换
// 3. 要出门了 打开门后 门口没有路 这个操作 next(to.path) 自己铺一条路
// asyncRoutes 在首页使用this.$router.options.routes获取路由规则的时候
// 只获取到了四条静态路由规则, 后来新增的规则并没有获取到 addRoutes方法新增的规则不是响应式的
next(to.path) // 重新执行一次
}
- 在
src/store/getters.js中配置导出myroutes,这一步不要也行,但是获取的时候会麻烦点,因为这个项目用了这个getters,所以放在这里的话,别的地方用会方便一点。
const getters = {
...
myroutes: state => state.permission.routes // 导出当前的路由
}
export default getters
- 在左侧菜单
layout/components/Sidebar/index.vue组件中, 引入myroutes, 使用vuex中的myroutes动态渲染,本质上还不是直接双向绑定响应式,只是我们在拿到menus,过滤出新的filterRoutes,把这个filterRoutes存进了vuex中,所以每次一变化,侧边栏那边就随着更新,这才实现动态。
computed: {
...mapGetters([
'sidebar',
'myroutes'
])
}
- 处理刷新404刷新的问题:页面刷新的时候,本来应该拥有权限的页面出现了404,这是因为404的匹配权限放在了静态路由中 (静态路由的404要删除),我们需要将404放置到动态路由的最后
if (!store.state.user.userInfo.userId) {
// 调用获取信息的action
const { roles } = await store.dispatch('user/getUserInfo')
// 调用action同步到vuex中
const otherRoutes = await store.dispatch('permissions/filterRoutes', roles.menus)
// 动态新增路由
router.addRoutes([...filterRoutes, { path: '*', redirect: '/404', hidden: true }])
next(to.path) // 重新执行一次
return
}
- 退出登录的时候需要重置路由:退出时, 需要将路由权限重置 (恢复默认), 将来登录后, 重新追加
我们在
router/index.js文件中,创建一个重置路由方法.这个方法就是将路由重新实例化,相当于换了一个新的路由,之前加的路由就不存在了,需要在登出的时候, 调用一下即可
// 重置路由
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // 重新设置路由的可匹配路径
}
store/modules/user.js
import { resetRouter } from '@/router'
// 退出的action操作
logout({ commit }) {
commit('removeUserInfo')
commit('removeToken')
// 重置路由规则 提交mutation 参数以是mutation名字 参数2参数 参数3是配置项
// commit('permissions/resetRoutes', null, { root: 'true' })
// 模块a的action提交模块b的mutation 需要加{ root: 'true' }
// 重置路由规则
commit('permissions/setRoutes', [], { root: 'true' })
// 重置路由
resetRouter()
}
- 最终代码:
- 里面用了一个nprogress 的插件,就是进入路由跳转的时候,有个进度条效果,跳转完成在后置守卫里关掉
import router from '@/router'
import store from '@/store'
import nprogress from 'nprogress'
import 'nprogress/nprogress.css'
// beforeEach 路由前置导航守卫,只要路由发生跳转就会经过导航守卫
// to 表示要去哪
// from 表示从哪来
// to 和 from 表示的是路由信息对象, 一般不关心from 更关心的是to
// next 放行函数 这个函数不调用,页面不会跳转显示
// next() 表示直接放行
// next('/路径') 强制去对应的路径
// next() 里面的参数 和 this.$router.push() 一样
// next({name: '路由名称', params: {} ,query:{}}) next({path: '/路径'})
const whiteList = ['/login', '/404']
router.beforeEach(async(to, from, next) => {
// 开启进度条
nprogress.start()
// 1. 先判断token
const token = store.getters.token
if (token) {
// 2. 判断是否去的是登录页面
if (to.path === '/login') {
// 3. 如果有token并且去的还是登录页面 直接去首页
next('/')
nprogress.done()
} else {
// 获取个人信息 如果没有个人信息的名字,获取个人信息
// 不想每次路由切换都重复获取个人信息
if (!store.getters.name) {
const res = await store.dispatch('user/getUserInfo')
console.log(res.roles.menus) // res中有一个roles, roles中有一个menus, 就是当前员工的权限列表信息,
// 基于已有的权限列表 筛选8条动态路由规则
// const filterRoutes = asyncRoutes.filter(item => res.roles.menus.includes(item.children[0].name))
// console.log(filterRoutes)
// 触发mutation将筛选出的动态路由规则存储到vuex中
// store.commit('permission/setRoutes', filterRoutes)
const filterRoutes = await store.dispatch('permission/getFilterRoutes', res.roles.menus)
// 用户进入系统之前 添加一些有权限的路由规则 路由实例上有一个addRoutes方法,是可以给路由添加规则的
router.addRoutes([...filterRoutes, { path: '*', redirect: '/404', hidden: true }]) // addRoutes添加的规则是在路由实例上获取不到的
// // addRoutes 已知问题 使用adRoutes添加的路由规则, 刷新会变白 解决的方案 重新next(to的地方)
next(to.fullPath)
// console.log(router)
}
// 4. 有token直接放行
next()
}
} else {
// 5. 没有token的情况
// 6. 判断是否去的页面是白名单页面,如果是直接放行
if (whiteList.includes(to.path)) {
next()
} else {
// 5. 没有token访问的页面是非白名单 强制重新登录
next('/login')
nprogress.done()
// 被next('/路径')强制跳转的路由是不会经过后置导航守卫的, 需要手动再次调用done
}
}
})
// 后置导航守卫
router.afterEach((to, from) => {
// console.log('经过后置守卫了')
// 关闭进度条
nprogress.done()
})
最后一点:前端权限 - 按钮操作权:就是细节方面,小按钮级别的操作权限
- 也是根据获得用户数据时拿到的,其他权限的数据,我们可以根据这个数据实现按钮级别的权限控制