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'
        }
      }
    ]
  },
相关推荐
leoZ2316 小时前
Claude 驱动的全栈开发:Spring Boot + Vue 的 BS 架构实践
vue.js·spring boot·架构
荒诞英雄10 小时前
从 17MB Vendor 大包到按需加载:Vite 多入口 Vue 组件库首屏优化实践
vue.js·vite
jsonbro10 小时前
手把手带你实现发布订阅模式
前端·vue.js·设计模式
Cobyte12 小时前
23.Vue Vapor 组件 attrs 的实现
前端·javascript·vue.js
斯特凡今天也很帅13 小时前
xml页面可以打开,不报错,但是显示数据的地方也不显示,我是怎么解决的
xml·vue.js
柯克七七14 小时前
给老项目加了 TypeScript,本来只想自己爽,结果全公司代码审查标准被我抬高了
前端·vue.js·typescript
自然 醒16 小时前
前端如何实现在线预览office常见文件功能?
前端·vue.js
肉肉不吃 肉17 小时前
前端调试跨域如何解决
前端·vue.js
卓怡学长17 小时前
w268基于springboot + vue 物流系统
java·数据库·vue.js·spring boot·spring·intellij-idea
2601_9628517418 小时前
计算机毕业设计之基于SSM的网络安全课程资源分享平台
vue.js·算法·安全·web安全·yolo·目标检测·课程设计