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'
        }
      }
    ]
  },
相关推荐
燃先生._.2 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
2401_857600955 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_857600955 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL5 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js
轻口味5 小时前
Vue.js 核心概念:模板、指令、数据绑定
vue.js
2402_857583495 小时前
基于 SSM 框架的 Vue 电脑测评系统:照亮电脑品质之路
前端·javascript·vue.js
java_heartLake6 小时前
Vue3之性能优化
javascript·vue.js·性能优化
ddd君317747 小时前
组件的声明、创建、渲染
vue.js
前端没钱8 小时前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
顽疲8 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端