基于vue-router的动态路由权限控制

本文是从用户登录到动态路由权限控制的全流程梳理。

1、用户登录

我们使用pinia做状态管理,新建userStore.ts文件,用于处理用户登录、登出和储存用户信息。便于其他组件使用用户数据和方法。

主要定义两个方法,登录和登出。登录方法中,登录接口调用成功后,将用户名、token等信息存入store;登出方法,调用登出接口并清除store中的token。

代码如下。


import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'
import loginServer from '@/api/loginServer'
export const userStore = defineStore('user', () => {
  const token = useLocalStorage('token', '')
  const role = useLocalStorage('role', '')
  const userName = useLocalStorage('userName', '')
  const password = useLocalStorage('password', '')
  const isRemember = useLocalStorage('remember', false)
  const login = (param: any) => {
    return new Promise<void>((resolve, reject) => {
      loginServer
        .login({
          username: param.username,
          password: param.password
        })
        .then((res) => {
          token.value = res.token
          userName.value = res.userName
          role.value = res.role
          isRemember.value = param.isRemember
          password.value = ''
          if (param.isRemember) {
            //缓存密码
            password.value = param.password
          }
          resolve()
        })
        .catch((error) => {
          reject(error)
        })
    })
  }

  const logout = () => {
    return new Promise((resolve, reject) => {
      loginServer
        .logout()
        .then((res) => {
          // 清除token
          token.value = ''
          resolve(res)
        })
        .catch((error) => {
          reject(error)
        })
    })
  }

  return {
    token,
    userName,
    password,
    isRemember,
    role,
    login,
    logout
  }
})

用户输入用户名密码后,点击登录按钮,调用userStore的login方法,接口返回值存入userStore。在登录组件中使用userStore的代码如下。

import { userStore } from '@/store/userStore'
const userStore = userStore()
const login = async (values: any) => {
  login_loading.value = true
  try {
    await userStore.login({
      ...values
    })
    router.push('/')
  } catch (error) {
  }
}

2、路由守卫

登录后进入到路由阶段。

2.1 动态引入组件

这里使用的是动态引入组件,根据菜单数据中配置好的component字段,其值为组件的路径字符串,从而实现动态加载组件。

首先加载views目录下所有vue文件,编写一个根据路径字符串转换为组件的方法,接收菜单数据参数。

// 使用 import.meta.glob 动态加载所有以.vue结尾的文件
const requireComponent = import.meta.glob(['@/views/**/*.vue'])

// 根据路径字符串转换为组件的方法
const convertPathToComponent = (limitMenus: any) => {
  if (limitMenus && limitMenus.length > 0) {
    limitMenus.forEach((item: any) => { // 遍历菜单数据
      const componentPath = item.component // 菜单的component字段值为该组件的路径字符串
      item.component = requireComponent[`/src/views/${componentPath}/index.vue`]
      if (item.children && item.children.length > 0) {
        convertPathToComponent(item.children) // 递归转换
      }
    })
  }
}

菜单数据示例:

let menus = [
  {
    "menuId": "1",
    "meta": {
      "hasChildren": true,
      "icon": "system",
      "menuId": 1,
      "title": "系统配置"
    },
    "path": "system",
    "component": null,
    "children": [
      {
        "menuId": "2",
        "meta": {
          "hasChildren": false,
          "icon": null,
          "menuId": 2,
          "title": "配置列表"
        },
        "path": "setList",
        "component": "system/setList"
      },
      {
        "menuId": "3",
        "meta": {
          "hasChildren": false,
          "icon": null,
          "menuId": 3,
          "title": "配置文件"
        },
        "path": "files",
        "component": "system/files"
      }
    ]
  }
  
]

2.2 路由守卫逻辑

1)判断目标是否为登录页,是,就移除动态路由,清除权限,并放行;如果目标不是登录页,到2)

2)判断用户已经登录,可以通过token存储状态判断是否登录,是就到3);否就跳到登录页

3)根据menuID获取按钮权限,存入menuStore中

4)判断menuStore中是否有菜单权限数据,是,就放行;否,进入5)

5)获取菜单权限,存储在menuStore,并处理数据,转换组件,并设置重定向到首页

路由守卫中的判断流程图如下图所示。

route.ts关键代码如下:

const requireComponent = import.meta.glob(['@/views/**/*.vue'])

const loginPath = '/login'

const dynamicRoutes: RouteRecordRaw = {
  path: '/',
  name: 'root',
  component: Layout,
  children: []
}
let removeRoute: () => void

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: loginPath,
      component: Login
    },
    {
      path: '/:pathMatch(.*)*',
      component: () => import('@/views/notFound/index.vue')
    }
  ]
})

router.beforeEach(async (to, from, next) => {
  const userStore = userStore()
  const menuStore = menuStore()

  if (to.path === loginPath) {
    removeRoute && removeRoute()
    menuStore.limitMenus = []
    next()
  } else {
    if (userStore.token) {
      //已登录
        const res = await getBtnLimit(to.meta.menuId)
        res.forEach((item) => {
          menuStore.btnPermissions.add(item)
        })
      if (menuStore.limitMenus && menuStore.limitMenus.length > 0) {
        next()
      } else {
        try {
          const res = await loginServer.getMenus()
          userStore.role = res.role
          if (res.menus && res.menus.length > 0) {
            res.menus.forEach((item) => {
              if (item.children && item.children.length > 0) {
                const subItem = item.children[0]
                item.redirect = item.path + '/' + subItem.path
              }
            })
            //转换组件
            convertPathToComponent(res.menus)

            menuStore.limitMenus = [
              ...res.menus
            ]
            dynamicRoutes.children = menuStore.limitMenus

            //重定向第一个路由
            const firstMenu = menuStore.limitMenus[0]
            dynamicRoutes.redirect = firstMenu.path
            removeRoute = router.addRoute(dynamicRoutes)
            next({ ...to, replace: true })
          } else {
            menuStore.limitMenus = []
            next({
              path: loginPath
            })
          }
        } catch (error) {
          //权限获取失败跳转到登录页
          next({
            path: loginPath
          })
        }
      }
    } else {
      //未登录
      next({
        path: loginPath
      })
    }
  }
})


// 根据路径字符串转换为组件的方法
const convertPathToComponent = (limitMenus: any) => {
  if (limitMenus && limitMenus.length > 0) {
    limitMenus.forEach((item: any) => { // 遍历菜单数据
      const componentPath = item.component // 菜单的component字段值为该组件的路径字符串
      item.component = requireComponent[`/src/views/${componentPath}/index.vue`]
      if (item.children && item.children.length > 0) {
        convertPathToComponent(item.children) // 递归转换
      }
    })
  }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值