Vue重构项目之权限配置篇

2,771 阅读5分钟

序言

最近项目需求不是很多,想到公司后台管理项目有些过于臃肿,相帮它减减肥。

奈何君有意,了却她无情。

jQ+bootstrapt的非主流内衣、template-web的硬核外套、html+css+js的老实牌秋裤、cdn铭牌的各种包包,一眼看上去还不错是不是。跟着她回到家,一开门人傻了!......

若要爱请深爱,若不爱请拜拜。

显然我选择了后者,咱们步入正题哈 next()

项目架构-权限配置

分析

一般比较简单的后台管理系统就是系统登录用户角色较少,维护人员不是很多,可能就admin和editor两种,这种权限比较简单,在路由的meta标签加个role差不多就够了,举个栗子

{
    path: '/dynamic',
    component: Layout,
    name: 'dynamic',
    meta: {
      title: '动态管理',
      icon: 'icon-duihuakuang',
      roles: ['admin', 'editor'] // you can set roles in root nav
    },
    children: [
      {
        path: 'dynamicList',
        component: () => import(''),
        name: 'dynamicList',
        meta: { title: '动态列表', noCache: true }
      },
      {
        path: 'rootdynamic',
        component: () => import(''),
        name: 'rootdynamic',
        meta: { title: '机器人动态列表', noCache: true }
      }
    ]
  }

但是我这个有点不一样,复杂一点角色分权限等级,而且用户较多。所以说这样的显然无法实现。

思考

一般登录流程是

获取token--->拿token获取用户信息以及后端返回路由--->前端筛选权限路由--->登录成功--->动态显示该用户的路由

我们没有token,后端有定时任务,过期后端接口都返回403,前端就直接重定向到登录页,所以第一步要不要无所谓了,但是在这文里还是走正常流程(项目上大家也要灵活应变,不走寻常路才能走遍所有路!)。

实践

获取token

思考:你的数据究竟是存储到本地localStorage还是用Vuex?(期待你的看法)

我这里都写下怎么选择取决与个人(不要为了使用vuex而使用vuex,大多数项目真的没必要)

  login({
    commit
  }, userInfo) {
    const {
      username,
      password,
      verification
    } = userInfo
    return new Promise((resolve, reject) => {
      login({
        userName: username.trim(),
        passWord: password,
        captcha: verification
      }).then(response => {
        if (response) {
          //localStorage.setItem('info', JSON.stringify(response.data))
          //假装有token,你有就把后端给的写上
          commit('SET_TOKEN', new Date().getTime())
          commit('SET_LEVEL', response.data.level)
          setToken(new Date().getTime())
          resolve(response)
        } else {
          this.$message({
            type: 'error',
            message: response.errMsg
          })
        }
      }).catch(error => {
        reject(error)
      })
    })
  },

拿token获取用户信息以及后端返回路由

getInfo({
    commit,
    state
  }) {
    return new Promise((resolve, reject) => {
      getInfo(state.token).then(response => {
          //路由信息存本地
        localStorage.setItem('sidebars', JSON.stringify(response.data))
        const data = JSON.parse(localStorage.getItem('info'))
        const {
          roleName,
          userName,
          department
        } = data
        commit('SET_ROLES', roleName)
        commit('SET_NAME', userName)
        commit('SET_AVATAR', '头像')
        commit('SET_INTRODUCTION', department)
        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  }

前端筛选权限路由

后端返回的路由是tree类型的数据,显然不能直接拿过来。

路由.png

既然这样,自己动手吧。前端router.jsconstantRoutesasyncRoutes两个路由菜单

  • constantRoutes:每个用都有的路由,比方说登录页、首页、404...
  • asyncRoutes:所有权限页面
export const constantRoutes = [
  {
    path: '/redirect',
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index')
      }
    ]
  },
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  {
    path: '/404',
    component: () => import('@/views/error-page/404'),
    hidden: true
  },
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index'),
        name: 'Dashboard',
        meta: { title: '首页', icon: 'icon-shouye', affix: true }
      }
    ]
  }
]
export const asyncRoutes = [
]
const router = createRouter()
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}
export default router

main.js里面引入import './permission'

来看看permission.js该怎么写

import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist

router.beforeEach(async(to, from, next) => {
  // 开始进度条
  NProgress.start()
  // 设置页面标题
  document.title = getPageTitle(to.meta.title)
  // 确定用户是否已登录
  const hasToken = getToken()
  if (hasToken) {
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done() 
    } else {
      // 确定用户是否已通过getInfo获得其权限角色
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
          // 拿到用户个人信息
          const roles = await store.dispatch('user/getInfo')
          // 权限路由匹配
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          // 动态添加路由
          router.addRoutes(accessRoutes)
          // hack方法 确保addRoutes已完成
          // 设置replace:true,这样导航就不会留下历史记录
          next({ ...to, replace: true })
        } catch (error) {
          // 初始化
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免费登录白名单中,直接进入
      next()
    } else {
      // 其他没有访问权限的页面将重定向到登录页面。
      next(`/login?redirect=${to.path}`)
      // next()
      NProgress.done()
    }
  }
})
router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

看看generateRoutes这里的roles其实就是用户信息,证明拿到了后端路由然后下一步

generateRoutes({
    commit
  }, roles) {
    return new Promise(resolve => {
      let accessedRoutes
      if (roles) {
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }  
      /**功能权限点,为后文埋下伏笔
      const data = JSON.parse(localStorage.getItem('sidebars'))
      const permission = []
      data.forEach(element => {
        if (element.children) {
          element.children.forEach(item => {
            item.children.forEach(i => {
              permission.push(i.menuId)
            })
          })
        }
      })
      commit('SET_PERMISSION', permission)
      */
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }

下面详细看看重点路由筛选,添加动态路由(完整文件见附录

/**
 * 递归过滤异步路由表
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes(routes, roles) {
  const res = []
  routes.forEach(route => {
    const tmp = {
      ...route
    }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })
  return res
}
/**
 * 通过和后端路由菜单比较确定当前用户是否有权限
 * @param roles
 * @param route
 */
function hasPermission(roles, route) {
//拿到后端返回的路由,将菜单名递归出来
  const data = JSON.parse(localStorage.getItem('sidebars'))
  const name = []
  data.forEach(element => {
    name.push(element.menuName)
    if (element.children) {
      element.children.forEach(item => {
        name.push(item.menuName)
      })
    }
  })
  //将后端返回的和当前路由对比有的话就通过,进入动态路由无的话就pass
  if (route.meta && route.meta.title) {
    return name.some(item => {
      return route.meta.title === item
    })
  } else {
    return false
  }
}

登录成功动态显示该用户的路由

既然有了页面权限,理论上应该有功能权限 其实应该还会有功能权限点,就是页面内他或许不能新增,只能看到列表,这种是页面内权限点配置,一般采用自定义指令的方式比较方便

权限配置-页面功能权限点配置

将后端返回的路由中的功能权限点通过递归提取出来

首先引入文件

mainjs里面引入

import directives from '@/utils/directives.js'
Vue.use(directives)

然后编写逻辑

directives.js里面封装下指令

// 页面功能权限判断
function checkPermission(el, binding) {
  const { value } = binding
  if (value) {
  //这个是我在筛选路由时顺便存下的
    const permissionArr = store.getters && store.getters.permissionArr
    const permissionRoles = Number(value)

    const hasPermission = permissionArr.some(val => {
      return permissionRoles === val
    })
    if (!hasPermission) {
      el.parentNode && el.parentNode.removeChild(el)
    }
  } else {
    throw new Error(`need roles! Like v-permission="['admin','editor']"`)
  }
}
export default (Vue) => {
  Vue.directive('permission', {
    inserted(el, binding) {//初始化调用
      checkPermission(el, binding)
    }
    // update(el, binding) {//这是在刷新的时候调用
    //   checkPermission(el, binding)
    // }
  })
}

最后页面使用

<el-button
v-for="(item) in dataSource.tool"
:key="item.key"
v-permission="item.permission"
class="filter-item"
type="primary"
@click="handleAdd(item.name)"
>
{{ item.name }}
</el-button>

结尾

谈谈架构框架选择

本来想用react+ant的,但是奈何感觉react都是js文件不太对我的胃口, (个人感觉都是js文件层次不是很清晰,相对来讲vue的熟悉成本较低)

既然有的选,那咱们就用Vue

Vue3.0虽然可以用,但是考虑到不确定因素,还需要时间去完善它

咱们选稳的2.x版本,然后UI库就Element UI吧。

权限配置到这里就结束了,有什么问题大家可以留言一起讨论

现在正在重构页面coding,也遇到一些问题,高德地图结合echarts展示问题、角色页面角色等级问题、二次封装table组件。

等我攒够一波坑,再来和大家分享。

Vue重构项目之权限配置篇到此结束,谢谢你的阅读!!!咱们下期见

附录

vuex里面的permission部分

import {
  asyncRoutes,
  constantRoutes
} from '@/router'

/**
 * 通过和后端路由菜单比较确定当前用户是否有权限
 * @param roles
 * @param route
 */
function hasPermission(roles, route) {
  const data = JSON.parse(localStorage.getItem('sidebars'))
  const name = []
  data.forEach(element => {
    name.push(element.menuName)
    if (element.children) {
      element.children.forEach(item => {
        name.push(item.menuName)
      })
    }
  })
  if (route.meta && route.meta.title) {
    return name.some(item => {
      return route.meta.title === item
    })
  } else {
    return false
  }
}

/**
 * 递归过滤异步路由表
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes(routes, roles) {
  const res = []
  routes.forEach(route => {
    const tmp = {
      ...route
    }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })
  return res
}

const state = {
  routes: [],
  addRoutes: [],
  permissionArr: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  },
  SET_PERMISSION: (state, arr) => {
    state.permissionArr = arr
  }
}

const actions = {
  generateRoutes({
    commit
  }, roles) {
    return new Promise(resolve => {
      let accessedRoutes
      if (roles) {
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }
      const data = JSON.parse(localStorage.getItem('sidebars'))
      const permission = []
      data.forEach(element => {
        if (element.children) {
          element.children.forEach(item => {
            item.children.forEach(i => {
              permission.push(i.menuId)
            })
          })
        }
      })
      commit('SET_ROUTES', accessedRoutes)
      commit('SET_PERMISSION', permission)
      resolve(accessedRoutes)
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

写在最后

我是凉城a,一个前端,热爱技术也热爱生活。

与你相逢,我很开心。

如果你想了解更多,请点这里,期待你的小⭐⭐

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊

  • 本文首发于掘金,未经许可禁止转载💌