需求
- 用户登录后仅能看到自己权限内的菜单列&页面。
- 用户强行跳转到不在自己权限内的页面,页面自动跳转到404页面。
实现
- 后端返回权限码存放在SessionStorage中,前端根据权限码生成对应的动态菜单menu。
- 前端在beforeRouter的守卫导航中,判断是否已经生成对应的router,若没有,则生成对应的router,添加到总router中。
- 动态菜单List和动态router存放在vuex中。
- 若是跳转回登陆页面,则要清空SessionStorage中的权限码以及清空vuex的动态菜单和动态router。
- 请求接口时,每次请求都在请求头中带上token,后端用于做权限控制。
Vuex 基本概念
前端目前进行组件化开发,若是同级的组件与组件之间不需要共享状态,那么简单使用vue的emit方式进行父子组件之间的状态传递即可。
传统的单组件的状态管理,如图所示。
如果开发的应用是多个组件共享同一个状态时,管理state状态就会非常复杂。
官方文档中也指出了这些问题:
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
为了解决上述的问题,需要将要共享状态抽取出来,以一个全局单例模式进行管理。在这种模式下,组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为(即变更状态的动作)。
官网的参考链接:开始 | VueX 。
Vuex将这些状态保存在浏览器的内存当中。
Vuex主要涉及到这五个概念: state、getter、mutation、action、module。
state
即状态,作为一个唯一的数据源存在。
// 定义
const state = {
count: 0
}
export default {
state
}
// 组件中调用时
this.$store.state.count
getter
对state里的状态进行加工,将加工的结果返回,但不变更state的状态。
注意:
- getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。
- getter 若作为方法调用时,则每次都会去调用方法,不进行缓存。
const getters = {
addCount: state => {
return state.count + 1
}
}
// 组件中调用时
this.$store.getters.addCount
mutation
更改 Vuex 的 store 中的状态的唯一方法。state作为第一参数,若还需要传递别的参数,即为mutation的payload。
mutation必须为同步函数。若要进行异步操作,则使用action。
mutations: {
increment (state, n) {
state.count += n
}
}
// 组件中调用
this.$store.commit('increment', 10)
action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
// 定义action
actions: {
// 异步操作提交
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
})
// 组件中分发action
this.$store.dispatch('increment')
更多action的异步调用例子查看官方文档 —— Action | Vuex
module
当状态非常多的时候,可以将这些状态按照业务逻辑进行拆分,最后统一组装起来。代码例子如下。
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
// 组件中调用时
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
代码逻辑
该部分代码参考vue-element-admin(可以去github上给原作者点个星星)。
定义router
Router分为两部分,一部分为根据权限码动态生成router,一部分为静态的Router(比如login界面和404界面)。
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/login'
import ErrorPage from '@/components/error'
Vue.use(Router)
export const constantsRoutes = [
{
path: '/',
redirect: '/home'
},
{
path: '/login',
name: 'Login',
component: Login
// 也可以用懒加载
// component: () => import('@/components/Login')
},
{
path: '/error',
name: 'ErrorPage',
component: ErrorPage
}
]
const createRouter = () => new Router({
routes: constantsRoutes
})
const router = createRouter()
// 刷新页面后,vuex的state会丢失,此时也要重置router
// https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
构建vuex
src
文件夹下创建 store
文件夹,该文件夹专门用于管理vuex的状态。
store
文件夹下新增modules
文件夹(该文件夹下用于管理vuex的各个模块状态的处理),添加permission.js
,代码如下:
import {constantsRoutes} from '@/router'
// 根据权限码动态生成对应的 router
export function generateAsyncRoutes(authCode) {
var asyncRoutes = []
/** 根据权限码生成对应的动态 router **/
...
/** 不合规的地址全部跳转到 error 页面,一定要设置在 router 最后 **/
asyncRoutes.push({path: '*', redirect: '/error', hidden: true})
return asyncRoutes
}
const state = {
// 总routes,动态 router和静态 router
routes: [],
// 动态 routes
asyncRoutes: [],
// 权限码
authCode: 0
}
const mutations = {
/** 设置 routes **/
SET_ROUTES: (state, routes) => {
state.asyncRoutes = routes
state.routes = constantsRoutes.concat(routes)
},
/** 设置权限码,供生成动态菜单和动态 router 使用 **/
SET_AUTHCODE: (state, code) => {
state.authCode = code
},
/** 重置state状态 **/
CLEAR_ALL: state => {
state.routes = []
state.asyncRoutes = []
state.authCode = 0
}
}
const actions = {
GenerateRoutes({commit}, authCode) {
let asyncRoutes = generateAsyncRoutes(authCode)
commit('SET_ROUTES', asyncRoutes)
return asyncRoutes
},
SetAuthCode({commit}, authCode) {
commit('SET_AUTHCODE', authCode)
},
// 重置该state
Reset({commit}) {
commit('CLEAR_ALL')
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
getters进行统一管理。
import {generateMenu} from '@/utils/menuUtil'
const getters = {
permission_routes: state => state.permission.routes,
permission_asyncRoutes: state => state.permission.asyncRoutes,
// 生成对应的权限菜单
permission_menu: state => generateMenu(state.permission.asyncRoutes)
}
export default getters
src文件夹下创建 store
文件夹,添加 index.js
文件,该文件里的内容直接用的是vue-element-admin
,文件内容如下。
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
Vue.use(Vuex)
// https://webpack.js.org/guides/dependency-management/#requirecontext
const modulesFiles = require.context('./modules', true, /\.js$/)
// you do not need `import app from './modules/app'`
// it will auto require all vuex module from modules file
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
// set './app.js' => 'app'
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default
return modules
}, {})
const store = new Vuex.Store({
modules,
getters
})
export default store
设置beforeEach守卫导航
每一次进入router前都会调用该方法,控制动态 router 以及控制何时向后端请求获取 authCode。
router.beforeEach((to, from, next) => {
let obj = getToken();
if (to.path === '/login') {
if (obj != null) {
// 清空所有的权限
userLogout() // 清空用户权限
store.dispatch('permission/Reset') // 调用 action 方法重置 vuex 中的 state
}
next()
} else {
if (obj != null) {
// 若有token, 判断动态添加的路由
if (store.getters.permission_asyncRoutes.length > 0) {
// 若动态添加的路由已经生成
next()
} else {
// 若动态添加的路由未生成,重新获取authCode生成
getMenuAuthCode().then(res => {
const authCode = res
const accessRoutesPromise = store.dispatch('permission/GenerateRoutes', authCode);
accessRoutesPromise.then(accessRoutes => {
router.addRoutes(accessRoutes);
next({ ...to, replace: true });
})
}).catch(err => {
console.error(err)
});
}
} else {
// 若没有token,则跳转到login页面,清空
next('/login')
}
}
});
登陆页面
登陆页面的代码省略。登陆页面只需要
- 向后台发送登陆请求,请求结果将
uid
和token
放入sessionStorage中,动态router的生成放在router的守卫导航中。 - 调用
this.$store.getters.permission_menu
获取到动态的菜单,渲染到menu组件。
总结
总结下来就是,要根据需求去做对应的权限控制。定好了业务需求之后,才能进行后续的开发,否则后续一旦需求变更,权限方面就要大改。
像上述的权限也只是做了菜单的权限控制和访问页面的控制(前端控制)。此外,我的后端同事还做了请求该接口的权限控制,达到了一个双向的权限认证管理。
如果要做到用户每一个view每一个组件都需要进行权限控制,那么上述的方法就不合适了。
举个例子,如果是根据用户的权限,控制table展示某几列,则需要额外自己去封装一个table组件,在生成页面的时候额外向后端获取一次table列,再进行渲染。