问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

基于Vue Router的动态路由权限控制详解

创作时间:
作者:
@小白创作中心

基于Vue Router的动态路由权限控制详解

引用
CSDN
1.
https://blog.csdn.net/bingjilingvsh2o/article/details/145261451

本文详细介绍了基于Vue Router的动态路由权限控制的实现方法,包括用户登录、路由守卫和动态组件加载等关键步骤。通过本文,读者可以了解如何使用Pinia进行状态管理,以及如何实现基于菜单权限的动态路由加载。

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
  }
})

用户输入用户名密码后,点击登录按钮,调用userStorelogin方法,接口返回值存入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) // 递归转换
      }
    })
  }
}
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号