Vue Vuex 菜单&页面权限控制

需求

  1. 用户登录后仅能看到自己权限内的菜单列&页面。
  2. 用户强行跳转到不在自己权限内的页面,页面自动跳转到404页面。

实现

  1. 后端返回权限码存放在SessionStorage中,前端根据权限码生成对应的动态菜单menu。
  2. 前端在beforeRouter的守卫导航中,判断是否已经生成对应的router,若没有,则生成对应的router,添加到总router中。
  3. 动态菜单List和动态router存放在vuex中。
  4. 若是跳转回登陆页面,则要清空SessionStorage中的权限码以及清空vuex的动态菜单和动态router。
  5. 请求接口时,每次请求都在请求头中带上token,后端用于做权限控制。

Vuex 基本概念

前端目前进行组件化开发,若是同级的组件与组件之间不需要共享状态,那么简单使用vue的emit方式进行父子组件之间的状态传递即可。

传统的单组件的状态管理,如图所示。

vuex官方图片

如果开发的应用是多个组件共享同一个状态时,管理state状态就会非常复杂。

官方文档中也指出了这些问题:

  1. 多个视图依赖于同一状态。
  2. 来自不同视图的行为需要变更同一状态。

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。

对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

为了解决上述的问题,需要将要共享状态抽取出来,以一个全局单例模式进行管理。在这种模式下,组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为(即变更状态的动作)。

官网的参考链接:开始 | VueX

Vuex将这些状态保存在浏览器的内存当中。

Vuex主要涉及到这五个概念: state、getter、mutation、action、module。

state

即状态,作为一个唯一的数据源存在。

// 定义
const state = {
    count: 0
}

export default {
    state
}

// 组件中调用时
this.$store.state.count

getter

对state里的状态进行加工,将加工的结果返回,但不变更state的状态。

注意:

  1. getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。
  2. 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')
    }
  }
});

登陆页面

登陆页面的代码省略。登陆页面只需要

  1. 向后台发送登陆请求,请求结果将uidtoken放入sessionStorage中,动态router的生成放在router的守卫导航中。
  2. 调用this.$store.getters.permission_menu获取到动态的菜单,渲染到menu组件。

总结

总结下来就是,要根据需求去做对应的权限控制。定好了业务需求之后,才能进行后续的开发,否则后续一旦需求变更,权限方面就要大改。

像上述的权限也只是做了菜单的权限控制和访问页面的控制(前端控制)。此外,我的后端同事还做了请求该接口的权限控制,达到了一个双向的权限认证管理。

如果要做到用户每一个view每一个组件都需要进行权限控制,那么上述的方法就不合适了。

举个例子,如果是根据用户的权限,控制table展示某几列,则需要额外自己去封装一个table组件,在生成页面的时候额外向后端获取一次table列,再进行渲染。

参考

  1. 手摸手,带你用vue撸后台 系列二(登录权限篇)
  2. VueX 官方文档
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容