Vite中实现基于后端的动态路由

动态路由的实现方式

在vue项目要想实现路由的动态控制,有两种主流的实现方向。

  1. 基于用户角色判断路由,并且在后端接口进行权限管理
  2. 基于用户信息单独返回配置的路由信息

对于第一种方法,其优点在于不用后端单独配置路由信息,当用户登陆时可直接根据用户的角色进行前端的路由控制。缺点在于每个用户都可以看到页面的所有路由信息,此时则需要后端单独对接口设置不同的角色权限。

对于第二种方法,其优点在于每个用户都可以配置自己独有的路由,根据返回的路由信息,加载相应的路由页面,不会在前端暴露所有的路由信息。缺点在于每个用户的路由配置相较于第一种更为繁琐,并且增加了服务器查找路由的负担。

第一种方法的实现可以参考: vue-admin-template

本文将详细介绍第二种,基于后端的路由实现

实现思路

graph TD start[开始] --> 登录{用户登录} 登录 --登录成功--> 守卫[全局前置路由守卫] 登录 --登录失败--> endd([结束,回到登陆页面]) 守卫 --> token{token存在} token --token存在--> 获取信息[获取信息] token --token不存在--> endd 获取信息 --> 响应拦截器{axios响应拦截器} 响应拦截器 --token有效--> 设置信息[设置用户和路由信息] 响应拦截器 --token过期--> 清除信息[清除信息] 清除信息 --> endd 设置信息 --> 添加路由([添加路由])

实现过程

用户登录

首先实现登录逻辑,views/login/index.vue

js 复制代码
const handleLogin = () => {
  loginFormRef.value?.validate((valid: boolean) => {
    if (valid) {
      loading.value = true
      useUserStore()
        .login({
          userName: loginForm.userName,
          password: loginForm.password
        })
        .then(() => {
          ElMessage.success({ message: '登录成功' })
          router.push({ path: '/' })
        })
        .catch(() => {
          loginForm.password = ''
        })
        .finally(() => {
          loading.value = false
        })
    } else {
      return false
    }
  })
}

然后实现用户登录token的获取,store/modules/user.ts

js 复制代码
/** 登录 */
  const login = (loginData: ILoginRequestData) => {
    return new Promise((resolve, reject) => {
      loginApi({
        userName: loginData.userName,
        password: loginData.password
      })
        .then((res) => {
          const { data } = res
          setToken(data.t)
          token.value = data.t
          resolve(true)
        })
        .catch((error) => {
          reject(error)
        })
    })
  }

登录成功,进入守卫

路由前置守卫判断token,并添加用户信息和路由,router/permission.ts

js 复制代码
router.beforeEach(async (to, _from, next) => {
  NProgress.start()

  document.title = getPageTitle(to.meta.title as string)

  const userStore = useUserStoreHook()
  const permissionStore = usePermissionStoreHook()
  // 判断该用户是否登录
  if (getToken()) {
    if (to.path === '/login') {
      // 如果已经登录,并准备进入 Login 页面,则重定向到主页
      next({ path: '/' })
      NProgress.done()
    } else {
        try {
          await userStore.getInfo()

          // 生成后端返回的动态路由
          await userStore.getRoutes()
          const asyncRoutes = userStore.asyncRoutes
          // 此处忽略,可为空数组
          const roles = userStore.roles
          permissionStore.setDynamicRoutes(asyncRoutes, roles)
            
          // 将'有访问权限的动态路由' 添加到 Router 中
          permissionStore.dynamicRoutes.forEach((route) => {
            router.addRoute(route)
          })
          // 确保添加路由已完成
          // 设置 replace: true, 因此导航将不会留下历史记录
          next({ ...to, replace: true })
        } catch (err: any) {
          // 过程中发生任何错误,都直接重置 Token,并重定向到登录页面
          userStore.resetToken()
          ElMessage.error(err.message || '路由守卫过程发生错误')
          next('/login')
          NProgress.done()
        }
      }
    }
  } else {
    // 如果没有 Token
    if (whiteList.indexOf(to.path) !== -1) {
      // 如果在免登录的白名单中,则直接进入
      next()
    } else {
      // 其他没有访问权限的页面将被重定向到登录页面
      next('/login')
      NProgress.done()
    }
  }
})

permissionStore.setDynamicRoutes方法的实现,store/modules/permission.ts

js 复制代码
  const routes = ref<RouteRecordRaw[]>([])
  const dynamicRoutes = ref<RouteRecordRaw[]>([])
// 后端返回路由
  const setDynamicRoutes = (asyncRoutes: any[], roles: string[]) => {
    const cloneRoutes = JSON.parse(JSON.stringify(asyncRoutes))
    // 看下面方法实现
    const resRoutes = getDynamicRoute(cloneRoutes)

    // 添加额外的路由重定向
    const rRoutes = redirectRoutes(roles, resRoutes)
    routes.value = constantRoutes.concat(resRoutes)

    dynamicRoutes.value = rRoutes
  }

getDynamicRoute方法,主要用于递归后端返回的路由信息,并对组件进行加载,utils/asyncRoute.ts

js 复制代码
import { RouteRecordRaw } from 'vue-router'
import Layout from '@/layout/index.vue'
const modules = import.meta.glob('../views/**/*.vue')
/**动态路由 */
export const getDynamicRoute = (asyncRoute: any[]): RouteRecordRaw[] => {
  const newRoute = asyncRoute.map((item) => {
    if (item.component === 'Layout') {
      item.component = Layout
    } else {
      item.component = modules[`../views/${item.component}.vue`]
    }
    if (item.children && item.children.length > 0) {
      item.children = getDynamicRoute(item.children)
    }
    return item
  })
  return newRoute
}

redirectRoutes方法,主要用于对不同角色的用户进行首页的不同重定向

js 复制代码
const redirectRoutes = (roles: string[], routes: RouteRecordRaw[]): RouteRecordRaw[] => {
  if (roles.includes('admin') || roles.includes('operation')) {
    return [
      {
        path: '/',
        name: 'Root',
        redirect: '/d',
        meta: { hidden: true }
      },
      ...routes
    ]
  } else {
    return [
      {
        path: '/',
        name: 'Root',
        redirect: '/s',
        meta: { hidden: true }
      },
      ...routes
    ]
  }
}

拦截器逻辑

这里只展示token失效处理部分

js 复制代码
  // 响应拦截(可根据具体业务作出相应的调整)
  service.interceptors.response.use(
    (response) => {
     // 此处自己写逻辑
    },
    (error) => {
      // Status 是 HTTP 状态码
      const status = get(error, 'response.status')
      const { data } = error.response.data

      switch (status) {
        case 400:
          // error.message = '请求参数错误'
          error.message = data.message
          break
        case 401:
          // Token 过期时,直接退出登录并强制刷新页面(会重定向到登录页)
          useUserStoreHook().logout()
          location.reload()
          break

注意事项

  1. 退出登陆时,需要重置路由信息
js 复制代码
/** 重置路由 */
export function resetRouter() {
  // 注意:所有动态路由路由必须带有 Name 属性,否则可能会不能完全重置干净
  try {
    router.getRoutes().forEach((route) => {
      const { name } = route
      if (name) {
        router.hasRoute(name) && router.removeRoute(name)
      }
    })
  } catch (error) {
    // 强制刷新浏览器
    window.location.reload()
  }
}
  1. 路由信息结构
js 复制代码
{
    path: '/dashboard',
    // 后端此处返回字符串
    component: Layout,
    redirect: '/dashboard/workbench',
    name: 'Dashboard',
    meta: {
      title: '首页',
      svgIcon: 'dashboard',
      roles: ['admin', 'operation']
    },
    children: [
      {
        path: 'workbench',
        // 后端返回 'dashboard/workbench/index'
        component: () => import('@/views/dashboard/workbench/index.vue'),
        name: 'WorkBench',
        meta: {
          title: '工作台',
          svgIcon: 'desktop'
        }
      },
      {
        path: 'analysis',
        component: () => import('@/views/dashboard/analysis/index.vue'),
        name: 'Analysis',
        meta: {
          title: '分析页',
          svgIcon: 'analysis'
        }
      }
    ]
  },
相关推荐
_codeOH7 小时前
Vue 3 vs React 19:框架还在卷,核心原理就这些
前端·vue.js
英勇无比的消炎药8 小时前
新手必看玩转TinyRobot一定要避开这些坑
前端·vue.js
英勇无比的消炎药9 小时前
别再盲目混用AI组件库和传统组件库差距原来这么大
前端·vue.js
英勇无比的消炎药10 小时前
前端提效神器全新AI组件库TinyRobot改写日常开发模式
前端·vue.js
英勇无比的消炎药11 小时前
前端提效神器TinyRobot
前端·vue.js
CDwenhuohuo11 小时前
uni 背景色渐变 全屏
前端·javascript·vue.js
爱怪笑的小杰杰11 小时前
Vue 项目交付第三方开发,如何隐藏核心 JS 源码?
前端·javascript·vue.js
小二·11 小时前
Vue 3 组合式 API 进阶实战
前端·javascript·vue.js
rising start12 小时前
九、vue3 组件通信:全场景详解
前端·vue.js·typescript
编程技术手记13 小时前
Vue Scoped CSS 与动态创建 DOM 的兼容性问题
前端·css·vue.js