vue-element-admin开源项目源码解析

element admin

1.安装依赖

package.json

“tui-editor”: “1.3.3”, “core-js”: “3.6.5”,删掉

把\src\views\components-demo\markdown.vue里面的markdown-editor组件还有引用全都注释掉

2.从入口开始

src/main.js

// 引入vue
import Vue from 'vue'

// 引入js-cookie
// 是用来操作cookie的,后端返回的token就存储在cookie中
// 之前我们是存储在loccalstroage中
import Cookies from 'js-cookie'

// 引入重置样式
import 'normalize.css/normalize.css' // a modern alternative to CSS resets

// Element是PC端的UI组件库
import Element from 'element-ui'
import './styles/element-variables.scss'
// 引入国际包  国际化
// import enLang from 'element-ui/lib/locale/lang/en'// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖

// 引入自已写的CSS全局样式
import '@/styles/index.scss' // global css

// 引入APP组件
import App from './App'
// 引入仓库
import store from './store'
// 引入路由
import router from './router'

// 引入自己封装的icon图标
import './icons' // icon
// 引入权限模块,非常重要,面试必问
import './permission' // permission control
import './utils/error-log' // error log

// 引入过滤器
import * as filters from './filters' // global filters

/**
 * If you don't want to use mock-server
 * you want to use MockJs for mock api
 * you can execute: mockXHR()
 *
 * Currently MockJs will be used in the production environment,
 * please remove it before going online ! ! !
 */
// process是node中的全局变量
// 当通过npm run build时,node_env就是production
// 当通过npm run dev时,node_env就是development
// process.env.NODE_ENV === 'production' 表示是生成环境
if (process.env.NODE_ENV === 'production') {
  // 导入mock模块,模拟接口中的,本项目中的接口,都是模拟的,都是假的
  const { mockXHR } = require('../mock')
  mockXHR()
}

Vue.use(Element, {
  // Button  可以设置  如果不设置默认是medium
  size: Cookies.get('size') || 'medium', // set element-ui default size
  // locale: enLang 表示组件使用英文
  // locale: enLang // 如果使用中文,无需设置,请删除
})

// 注册全局过滤器
// register global utility filters
Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key])
})

// 关闭生成环境下,vue提示
Vue.config.productionTip = false

// new一个Vue实例
new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
})

3.路由模块

src/router/index.js

// 引入vue
import Vue from 'vue'
// 引入vue-router
import Router from 'vue-router'

// 路由就是一个插件,需要use
Vue.use(Router)

// 引入layout组件
// layout组件非常重要
// 一级路由出口中,匹配layout组件
import Layout from '@/layout'

// 引入其它四个路由模块
import componentsRouter from './modules/components'
import chartsRouter from './modules/charts'
import tableRouter from './modules/table'
import nestedRouter from './modules/nested'

/**
 * Note: sub-menu only appear when route children.length >= 1
 * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
 *
 * hidden: true                   if set true, item will not show in the sidebar(default is false)
 * alwaysShow: true               if set true, will always show the root menu
 *                                if not set alwaysShow, when item has more than one children route,
 *                                it will becomes nested mode, otherwise not show the root menu
 * redirect: noRedirect           if set noRedirect will no redirect in the breadcrumb
 * name:'router-name'             the name is used by <keep-alive> (must set!!!)
 * meta : {
    roles: ['admin','editor']    control the page roles (you can set multiple roles)
    title: 'title'               the name show in sidebar and breadcrumb (recommend set)
    icon: 'svg-name'/'el-icon-x' the icon show in the sidebar
    noCache: true                if set true, the page will no be cached(default is false)
    affix: true                  if set true, the tag will affix in the tags-view
    breadcrumb: false            if set false, the item will hidden in breadcrumb(default is true)
    activeMenu: '/example/list'  if set path, the sidebar will highlight the path you set
  }
 */

/**
 * constantRoutes
 * a base page that does not have permission requirements
 * all roles can be accessed
 */
// constantRoutes是静态路由
// 本项目,路由分成了两大模块:静态路由 和 动态路由
// 静态路由:所有的用户可以访问,不需要权限
// 动态路由:需要权限,如果有权限,就可以访问,如果没有权限,就不能访问
// 路则规则:就是一个对象
//     path: '/redirect',  访问的url
//     component: Layout,  访问出口中放什么组件  在一级中币出口中放 Layout 组件
//     hidden: true,  隐藏   把侧边栏中不能看到声明式导航
//     children: [  配置二级路由
export const constantRoutes = [
  {
    path: '/redirect',
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path(.*)',
        // 在二级路由出口中放@/views/redirect/index组件
        component: () => import('@/views/redirect/index')
      }
    ]
  },
  {
    // 当访问 /login时,  在一级路由出口放登录页面  一级路由出口是在APP组件中
    path: '/login',
    component: () => import('@/views/login/index'),
    //  把侧边栏中不能看到声明式导航
    hidden: true
  },
  {
    path: '/auth-redirect',
    component: () => import('@/views/login/auth-redirect'),
    hidden: true
  },
  {
    path: '/404',
    component: () => import('@/views/error-page/404'),
    hidden: true
  },
  {
    path: '/401',
    component: () => import('@/views/error-page/401'),
    hidden: true
  },
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index'),
        name: 'Dashboard',
        meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
      }
    ]
  },
  {
    path: '/documentation',
    component: Layout,
    children: [
      {
        path: 'index',
        component: () => import('@/views/documentation/index'),
        name: 'Documentation',
        meta: { title: 'Documentation', icon: 'documentation', affix: true }
      }
    ]
  },
  {
    path: '/guide',
    component: Layout,
    redirect: '/guide/index',
    children: [
      {
        path: 'index',
        component: () => import('@/views/guide/index'),
        name: 'Guide',
        meta: { title: 'Guide', icon: 'guide', noCache: true }
      }
    ]
  },
  {
    path: '/profile',
    component: Layout,
    redirect: '/profile/index',
    hidden: true,
    children: [
      {
        path: 'index',
        component: () => import('@/views/profile/index'),
        name: 'Profile',
        meta: { title: 'Profile', icon: 'user', noCache: true }
      }
    ]
  }
]

/**
 * asyncRoutes
 * the routes that need to be dynamically loaded based on user roles
 */
// asyncRoutes是动态路由
// 页面级(路由级)权限:
//     不同用户,登录到系统,看到的侧边栏是不一样,也就是有不同的页面
//     同一个页面,有的用户可以访问,有的用户不能访问
//  并不是说,你在下面配置完就OK,背后还有很多代码
export const asyncRoutes = [
  {
    path: '/permission',
    component: Layout,
    redirect: '/permission/page',
    alwaysShow: true, // will always show the root menu
    name: 'Permission',
    meta: {
      title: 'Permission',
      icon: 'lock',
      // roles表示什么样的用户可以访问permission
      // 不同的用户有不同的角色
      // 本项目就两个角色:admin   editor
      // roles: ['admin', 'editor']  表示amdin可以访问persmisson  editor也可以访问persmisson
      roles: ['admin', 'editor'] // you can set roles in root nav

      // roles: ['admin']  表示只能admin用户可以访问persmisson
      // roles: ['admin'] // you can set roles in root nav
    },
    children: [
      {
        path: 'page',
        component: () => import('@/views/permission/page'),
        name: 'PagePermission',
        meta: {
          title: 'Page Permission',
          roles: ['admin'] // or you can only set roles in sub nav
        }
      },
      {
        path: 'directive',
        component: () => import('@/views/permission/directive'),
        name: 'DirectivePermission',
        meta: {
          title: 'Directive Permission'
          // 没有roles,表示此路由规则,不需要校验
          // if do not set roles, means: this page does not require permission
        }
      },
      {
        path: 'role',
        component: () => import('@/views/permission/role'),
        name: 'RolePermission',
        meta: {
          title: 'Role Permission',
          roles: ['admin']
        }
      }
    ]
  },

  {
    path: '/icon',
    component: Layout,
    children: [
      {
        path: 'index',
        component: () => import('@/views/icons/index'),
        name: 'Icons',
        meta: { title: 'Icons', icon: 'icon', noCache: true }
      }
    ]
  },

  /** when your routing map is too long, you can split it into small modules **/
  componentsRouter,
  chartsRouter,
  nestedRouter,
  tableRouter,

  {
    path: '/example',
    component: Layout,
    redirect: '/example/list',
    name: 'Example',
    meta: {
      title: 'Example',
      icon: 'el-icon-s-help'
    },
    children: [
      {
        path: 'create',
        component: () => import('@/views/example/create'),
        name: 'CreateArticle',
        meta: { title: 'Create Article', icon: 'edit' }
      },
      {
        path: 'edit/:id(\\d+)',
        component: () => import('@/views/example/edit'),
        name: 'EditArticle',
        meta: { title: 'Edit Article', noCache: true, activeMenu: '/example/list' },
        hidden: true
      },
      {
        path: 'list',
        component: () => import('@/views/example/list'),
        name: 'ArticleList',
        meta: { title: 'Article List', icon: 'list' }
      }
    ]
  },

  {
    path: '/tab',
    component: Layout,
    children: [
      {
        path: 'index',
        component: () => import('@/views/tab/index'),
        name: 'Tab',
        meta: { title: 'Tab', icon: 'tab' }
      }
    ]
  },

  {
    path: '/error',
    component: Layout,
    redirect: 'noRedirect',
    name: 'ErrorPages',
    meta: {
      title: 'Error Pages',
      icon: '404'
    },
    children: [
      {
        path: '401',
        component: () => import('@/views/error-page/401'),
        name: 'Page401',
        meta: { title: '401', noCache: true }
      },
      {
        path: '404',
        component: () => import('@/views/error-page/404'),
        name: 'Page404',
        meta: { title: '404', noCache: true }
      }
    ]
  },

  {
    path: '/error-log',
    component: Layout,
    children: [
      {
        path: 'log',
        component: () => import('@/views/error-log/index'),
        name: 'ErrorLog',
        meta: { title: 'Error Log', icon: 'bug' }
      }
    ]
  },

  {
    path: '/excel',
    component: Layout,
    redirect: '/excel/export-excel',
    name: 'Excel',
    meta: {
      title: 'Excel',
      icon: 'excel'
    },
    children: [
      {
        path: 'export-excel',
        component: () => import('@/views/excel/export-excel'),
        name: 'ExportExcel',
        meta: { title: 'Export Excel' }
      },
      {
        path: 'export-selected-excel',
        component: () => import('@/views/excel/select-excel'),
        name: 'SelectExcel',
        meta: { title: 'Export Selected' }
      },
      {
        path: 'export-merge-header',
        component: () => import('@/views/excel/merge-header'),
        name: 'MergeHeader',
        meta: { title: 'Merge Header' }
      },
      {
        path: 'upload-excel',
        component: () => import('@/views/excel/upload-excel'),
        name: 'UploadExcel',
        meta: { title: 'Upload Excel' }
      }
    ]
  },

  {
    path: '/zip',
    component: Layout,
    redirect: '/zip/download',
    alwaysShow: true,
    name: 'Zip',
    meta: { title: 'Zip', icon: 'zip' },
    children: [
      {
        path: 'download',
        component: () => import('@/views/zip/index'),
        name: 'ExportZip',
        meta: { title: 'Export Zip' }
      }
    ]
  },

  {
    path: '/pdf',
    component: Layout,
    redirect: '/pdf/index',
    children: [
      {
        path: 'index',
        component: () => import('@/views/pdf/index'),
        name: 'PDF',
        meta: { title: 'PDF', icon: 'pdf' }
      }
    ]
  },
  {
    path: '/pdf/download',
    component: () => import('@/views/pdf/download'),
    hidden: true
  },

  {
    path: '/theme',
    component: Layout,
    children: [
      {
        path: 'index',
        component: () => import('@/views/theme/index'),
        name: 'Theme',
        meta: { title: 'Theme', icon: 'theme' }
      }
    ]
  },

  {
    path: '/clipboard',
    component: Layout,
    children: [
      {
        path: 'index',
        component: () => import('@/views/clipboard/index'),
        name: 'ClipboardDemo',
        meta: { title: 'Clipboard', icon: 'clipboard' }
      }
    ]
  },

  {
    path: 'external-link',
    component: Layout,
    children: [
      {
        path: 'https://github.com/PanJiaChen/vue-element-admin',
        meta: { title: 'External Link', icon: 'link' }
      }
    ]
  },

  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]

const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})

const router = createRouter()

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}

export default router

4.permission模块

src/permission.js

// 引入路由器
import router from './router'
// 引入仓库
import store from './store'
// 引入elementui中的提示性组件
import { Message } from 'element-ui'
// 引入nprogress  进度条
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style

// getToken就是从cookie中获取token
// 如果登录了,是有token,如果没有登录,就没有token
import { getToken } from '@/utils/auth' // get token from cookie
// 得到页面中的title
import getPageTitle from '@/utils/get-page-title'

// 配置NProgress
NProgress.configure({ showSpinner: false }) // NProgress Configuration

// whiteList是白名单   不需要登录,就可以直接访问
const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist

// 全局前置路由守卫
// 核心代码都是在守卫中
router.beforeEach(async(to, from, next) => {
  //  开启进度条
  NProgress.start()

  // 给页面的title赋值
  document.title = getPageTitle(to.meta.title)

  // 从cookie中获取token 如果有token表示登录了
  const hasToken = getToken()

  if (hasToken) {
    // 有token,说明已经登录了
    if (to.path === '/login') {
      // 已经登录了,你又去登录,放行到后面首页
      next({ path: '/' })
      // 关闭进度条
      NProgress.done()
    } else {
      // 已经登录了,去其它页面
      // store.getters.roles得到vuex中的角色
      // 如果登录了,我们会调用一个接口,去拿用户信息,在用户信息中,有当前用户的角色
      // 点击登录,先发一个登录请求,服务器响应一个token,前端把token存储到cookie
      // 紧接着发第二个请求,是用来获取用户信息的,前端把用户信息存储到了vuex中,用户信息中有一个角色
      // 也就是说,在vuex中是可以获取角色的  通过store.getters.roles
      // store.getters.roles.length > 0 表示vuex是有角色
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        // 从vuex中获取角色,直接放行
        next()
      } else {
        // else表示vuex中没有角色  当你又去刷新浏览器时,vuex中就没有角色,vuex中的数据也是存储在内存
        try {
          // get user info
          // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
          // store.dispatch('user/getInfo')  重新获取用户信息   肯定是在vuex中发送ajax请求
          // roles 表示用户信息,用户信息中包含用户角色
          const { roles } = await store.dispatch('user/getInfo')

          // generate accessible routes map based on roles
          // dispatch('permission/generateRoutes', roles)  根据用户角色,生成路由规则
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)

          // dynamically add accessible routes
          // 一个路由器中,可以有很多的规则,计算了当前用户角色有18个规则
          // 利用addRoutes,把这18个规则,添加到路由器
          router.addRoutes(accessRoutes)

          // hack method to ensure that addRoutes is complete
          // set the replace: true, so the navigation will not leave a history record
          // 上面已经把规则添加到路由器中,放行,此时,你就可以看到,你有权限看到的页面了
          next({ ...to, replace: true })
        } catch (error) {
          // remove token and go to login page to re-login
          // 如果在生成规则时,出问题了
          // store.dispatch('user/resetToken')  清除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()
})

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

5.状态管理

src/store/modules/user.js

import {
  login,
  logout,
  getInfo
} from '@/api/user'
import {
  getToken,
  setToken,
  removeToken
} from '@/utils/auth'
import router, {
  resetRouter
} from '@/router'

const state = {
  token: getToken(), // token
  name: '', // 用户名
  avatar: '', // 用户头像
  introduction: '', // 用户简介
  roles: [] // 当前用户所具有的角色
}

const mutations = {
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  SET_INTRODUCTION: (state, introduction) => {
    state.introduction = introduction
  },
  SET_NAME: (state, name) => {
    state.name = name
  },
  SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
  },
  SET_ROLES: (state, roles) => {
    state.roles = roles
  }
}

const actions = {
  // 用户登录
  login({
    commit
  }, userInfo) {
    // 组件中派发action
    const {
      username,
      password
    } = userInfo
    return new Promise((resolve, reject) => {
      // login是接口
      login({
        username: username.trim(),
        password: password
      }).then(response => {
        const {
          data
        } = response
        // commit('SET_TOKEN', data.token) commit一个mutaion,给token赋值
        commit('SET_TOKEN', data.token)
        setToken(data.token)
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // 获取用户信息
  getInfo({
    commit,
    state
  }) {
    return new Promise((resolve, reject) => {
      // getInfo 也是接口
      getInfo(state.token).then(response => {
        const {
          data
        } = response

        if (!data) {
          reject('Verification failed, please Login again.')
        }

        const {
          roles,
          name,
          avatar,
          introduction
        } = data

        // roles must be a non-empty array
        if (!roles || roles.length <= 0) {
          reject('getInfo: roles must be a non-null array!')
        }

        // 把用户信息存储到vuex
        commit('SET_ROLES', roles)
        commit('SET_NAME', name)
        commit('SET_AVATAR', avatar)
        commit('SET_INTRODUCTION', introduction)
        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  },

  // 用户退出登录
  logout({
    commit,
    state,
    dispatch
  }) {
    return new Promise((resolve, reject) => {
      logout(state.token).then(() => {
        commit('SET_TOKEN', '')
        commit('SET_ROLES', [])
        removeToken()
        resetRouter()

        // reset visited views and cached views
        // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485
        dispatch('tagsView/delAllViews', null, {
          root: true
        })

        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // 清除token
  resetToken({
    commit
  }) {
    return new Promise(resolve => {
      commit('SET_TOKEN', '')
      commit('SET_ROLES', [])
      removeToken()
      resolve()
    })
  },

  // 动态修改角色
  async changeRoles({
    commit,
    dispatch
  }, role) {
    const token = role + '-token'

    commit('SET_TOKEN', token)
    setToken(token)

    const {
      roles
    } = await dispatch('getInfo')

    resetRouter()

    // generate accessible routes map based on roles
    const accessRoutes = await dispatch('permission/generateRoutes', roles, {
      root: true
    })
    // dynamically add accessible routes
    router.addRoutes(accessRoutes)

    // reset visited views and cached views
    dispatch('tagsView/delAllViews', null, {
      root: true
    })
  }
}

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

src/store/modules/permission.js

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

// 使用roles:Array<string> 和 route:{path,component,meta:{roles:[]}}
// 判断当前用户能不能访问当前路由规则,返回布尔值。
function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    // 如果路由规则上有meta,并且meta上有roles这个自定义字段,说明这条路由是有权限的,要进行过滤权限。
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    // 如果路由规则上没有meta这个属性,或者meta上没有roles这个自定义字段,任何用户都可以访问。
    return true
  }
}

// 使用roles:Array<string>, 和 routes:Array<route> routes是所有的动态路由,roles是当前的角色
// 生成当前用户可访问的路由规则。
export function filterAsyncRoutes(routes, roles) {
  const res = []
  routes.forEach(route => {
    // 深复制(深拷贝)仅对那些比较简单对象进行深拷贝
    const tmp = {
      ...route
    }
    // hasPermission(['admin', 'editor'], {path,component,meta:{roles:['editor']}}): boolean
    if (hasPermission(roles, tmp)) {
      // 有没有嵌套视图,如果进行递归
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })
  return res
}

const state = {
  routes: [], // 静态路由规则 + 当前用户可访问的动态路由规则
  addRoutes: [] // 只是当前用户可访问的动态路由规则
}

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

const actions = {
  // 用于生成当前用户可访问的路由规则
  // roles = ['admin', 'editor']
  generateRoutes({
    commit
  }, roles) {
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        // admin用户,不需要计算可访问的动态路由,把所有的动态路由都赋值给accessedRoutes
        // 工作经验:工作中的管理系统,admin一般只用看到功能性页面,不需要看那些业务性页面。
        // accessedRoutes = asyncRoutes || []
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      } else {
        // 非admin用户,filterAsyncRoutes会计算当前用户可以访问当前用户可以访问那些动态路由,asyncRoutes是所有路由,roles是当前的角色
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }
      // 把当前用户可访问的路由规则放在vuex中
      commit('SET_ROUTES', accessedRoutes)
      // 把当前用户可访问的路由规则给到.then()
      resolve(accessedRoutes)
    })
  }
}

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

6.前端权限设计总结

不同的公司,权限设计是不一样的,从前后端协作的角度说,权限设计大体上有两种思路,第一种是单纯由前端来实现,第二种由后端来实现。后端实现,我们后面会讲一个案例。由前端来设计管理系统的权限,开发成本相对较低。

中小型公司:前端做权限设计,本开源项目主要前端做的权限设计。

大公司:后端做权限设计,后面再讲一下后端的权限设计。

前端实现思路:前端用token调接口换取当前用户信息,再使用用户的角色信息动态生成有权访问的菜单和路由。核心知识:

  • vue-router 中的全局守卫、路由元信息
  • router.addRoutes() 动态添加路由匹配规则
  • 使用 vuex 管理用户信息、路由菜单信息等

核心逻辑

  • 全局路由守卫,用于拦截用户对系统内部页面的访问
  • 当权限路由动态生成后,才能进入系统内部渲染 Menu 菜单
  • 在登录流程中,先用 token 调接口获取用户信息,再根据用户信息生成权限路由,并保存在状态管理工具中去,后面就可以正常访问权限路由了。
  • 当用户在系统内部刷新页面时,这就是刷新流程。当我们刷新时,Vuex中的用户信息和权限菜单信息会丢失。所以,在刷新流程中,会重新使用 token 换取用户信息,再使用用户角色来生成权限路由。

面试题:你做的管理系统,你是如何处理权限的问题的?

答:我们之前的公司中小公司,做的项目,大概有30个左右的模块,所以权限这一块,就前端去处理的。我们处理的流程大致是这样的:前端登录换取token,在导航守卫中,实现权限设计,首先判断有没有token,没有token,直接跳到登录页面。有token会进一步判断vuex中有没有用户信息。如果没有用户信息,拿着token,调用接口获取用户信息,用户信息中保存了最重要的字段,就是角色,有了角色后,通过算法生成当前用户可访问的动态路由规则(算法大至是使用后端返回的角色和路由元信息中的角色进行对比,得到可以访问的动态路由规则),有了动态访问的路由规则,再通过addRoutes方法,把得到的动态访问的路由规则添加到路由系统。

面试题:前端做权限这一块,你感觉有什么不足?

答:如果要修改权限,必须要去修改前端代码,重新打包,重新上线。前端处理权限,只适合中小项目(模块少,角色少),一般中小公司权限处理都是前端实现。

面试题:管理系统左侧的菜单是什么时候生成的?

答:在登录流程中,登录成功后,得到的token,根据token获取用户信息,用户信息中包含角色,根据角色生成可访问的动态路由规则(accessRoutes),把路由规则,也保存到了Vuex中,跳到系统内部页面,渲染Layout组件,在渲染Layout组件时,会渲染菜单。在刷新流程中,使用token换取用户信息,生成可访问的动态路由规则,保存到Vuex中,再次渲染Layout时,生成左侧菜单。

7.后端权限设计总结

后端处理权限,一般在管理系统中,都有两个模块,用户管理,角色管理。

后端实现思路:前端用token换取用户信息,用户信息中就已经包含了与路由渲染相关的菜单字段。后端可以在数据库中手动配置菜单数据,还可以开发专门的权限管理模块来动态管理菜单数据。如果要动态地通过增删改查来管理菜单数据,常常得有角色管理、用户管理和菜单管理等功能。

8.axios封装

src/utils/request.js

import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

// create an axios instance
// baseURL是服务器的地址
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // do something before request is sent

    if (store.getters.token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
   */

  /**
   * Determine the request status by custom code
   * Here is just an example
   * You can also judge the status by HTTP Status Code
   */
  response => {
    // res是服务器响应的结果
    const res = response.data

    // if the custom code is not 20000, it is judged as an error.
    // 状态码分两类:http状态码,业务状态码
    if (res.code !== 20000) {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })

      // token过期
      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        // to re-login
        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
          confirmButtonText: 'Re-Login',
          cancelButtonText: 'Cancel',
          type: 'warning'
        }).then(() => {
          store.dispatch('user/resetToken').then(() => {
            location.reload()
          })
        })
      }
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    // 如果没有给出响应,服务器没有响应就触发下面的代码
    console.log('err' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default service
  • 26
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值