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'
        }
      }
    ]
  },
相关推荐
只会写Bug18 小时前
后台管理项目中关于新增、编辑弹框使用的另一种展示形式
前端·vue.js
M ? A19 小时前
Vue v-bind 转 React:VuReact 怎么处理?
前端·javascript·vue.js·经验分享·react.js·面试·vureact
军军君0119 小时前
数字孪生监控大屏实战模板:政务服务大数据
前端·javascript·vue.js·typescript·前端框架·echarts·less
忆往wu前19 小时前
前端请求三部曲:Ajax / Fetch / Axios 演进与 Vue 工程化封装
前端·vue.js
.Cnn20 小时前
Ajax与Vue 生命周期核心笔记
前端·javascript·vue.js·笔记·ajax
吴声子夜歌21 小时前
Vue3——渲染函数
前端·vue.js·vue·es6
Ruihong21 小时前
你的 Vue KeepAlive 组件,VuReact 会编译成什么样的 React 代码?
vue.js·react.js·面试
Ruihong21 小时前
你的 Vue slot 插槽,VuReact 会编译成什么样的 React 代码?
vue.js·react.js·面试
一 乐21 小时前
房产租赁管理|基于springboot + vue房产租赁管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·房产租赁管理系统
2501_9136800021 小时前
Vue3项目快速接入AI助手的终极方案 - 让你的应用智能升级
前端·vue.js·人工智能·ai·vue·开源软件