【解决方案】一分钟讲清楚动态路由

46 阅读2分钟

在前端项目中只配置基础的静态路由,比如登录页、首页、404页面,登录时根据用户权限获取菜单,通过菜单转换为动态路由。

前置条件

  • 根路由( / )代表登录后要访问的首页
  • /login 登录页面
  • 动态路由全部添加在根路由下
  • 登录后将根路由重定向到动态路由中的第一个路由

路由关系图

image-20251103134323612.png

实现流程

登录 -> 获取菜单配置 -> 将菜单配置转换为 vue-router 需要的路由配置 -> 添加路由 -> 将根路由重定向到动态路由中的第一个路由

基础静态路由配置

后续的动态路由都将添加到根路由下

const routeItems = [
  {
    name: 'home',
    path: '/',
    component: () => import('@renderer/views/home/main.vue'),
    meta: {},
    children: []
  },
  {
    name: 'login',
    path: '/login',
    component: () => import('@renderer/views/login/main.vue'),
    meta: {}
  },
  {
    name: 'notFound',
    path: '/404',
    component: () => import('@renderer/views/notFound/main.vue'),
    meta: {}
  }
]

登录后获取菜单

登录后调用接口获取对应权限下的菜单配置。

[
  {
    id: 3001,
    parentId: 3000,
    name: '用户管理',
    path: 'management/userMgmt',
    component: null,
    componentName: null,
    icon: null,
    visible: true,
    keepAlive: true,
    alwaysShow: true,
    children: []
  }
]

将获取到的菜单转换为路由配置

将获取到的菜单通过递归的形式转换为路由配置。

export function mapServerRoutesToVueRoutes(serverRoutes) {
  return serverRoutes.map((item) => {
    const route = {
      path: item.path.startsWith('/') ? item.path : `/${item.path}`,
      name: item.name,
      component: getViewComponent(item),
      meta: {
        title: item.name,
        icon: item.icon,
        keepAlive: item.keepAlive,
        visible: item.visible
      },
      children: []
    }
​
    if (item.children && item.children.length > 0) {
      route.children = mapServerRoutesToVueRoutes(item.children)
    }
​
    return route
  })
}

最终生成的路由配置文件中的 component 属性要映射为真正的路由组件

function getViewComponent(item) {
  try {
    // 将 / 开头的组件路径转换成相对路径
    const _path = item.path.startsWith('/') ? item.path.substring(1) : item.path
​
    return () => import(`@renderer/views/${_path}/main.vue`)
  } catch (e) {
    // 若组件文件不存在,加载一个通用的占位页
    return () => import('@renderer/views/notFound/main.vue')
  }
}

将动态路由添加到路由下

// dynamicRoutes 就是根据菜单生成的动态路由
dynamicRoutes.forEach((route) => {
  // 将动态路由添加到 home 下
  router.addRoute('home', route)
})

跳转到根路由

await router.replace({
  path: '/'
})

路由守卫

image-20251103143428783.png

let isRegisterDynamicRouter = false
​
// 伪代码
router.beforeEach(async (to, from, next) => {
  console.log(`to: ${to.path}, from: ${from.path}`)
  const userStore = useUserStore()
  const accessToken = userStore.accessToken
​
  // 未登录时只能访问登录页
  if (!accessToken) {
    if (to.name === 'login') {
      return next()
    }
​
    // 访问 / 不添加 redirect
    if (to.name === 'home') {
      return next({
        name: 'login'
      })
    }
​
    return next({
      name: 'login',
      query: {
        redirect: to.fullPath
      }
    })
  }
​
  // 已登录时访问登录页,跳转到首页
  if (to.name === 'login') {
    return next({ path: '/' })
  }
​
  // 已登录,访问非登录页面
  if (!isRegisterDynamicRouter) {
    try {
      if (!userStore.roleMenu) {
        userStore.logout()
        return next({ name: 'login' })
      }
​
      // 获取当前选中的角色,获取动态路由
      const routes = userStore.roleMenu[userStore.currentRoleCode]
​
      await registerDynamicRoutes(routes)
      isRegisterDynamicRouter = true
      return next({ ...to, replace: true })
    } catch (error) {
      console.error('动态路由注册失败:', error)
      // 注册失败,跳回登录
      userStore.logout()
      return next({ name: 'login' })
    }
  }
​
  // 未匹配到任何路由 或 访问的是 /,跳转到第一个动态路由
  if (!to.matched.length || to.path === '/') {
    if (!userStore.roleMenu) {
      userStore.logout()
      return next({ name: 'login' })
    }
​
    const routes = userStore.roleMenu[userStore.currentRoleCode]
    const dynamicRoutes = mapServerRoutesToVueRoutes(routes)
    if (dynamicRoutes.length > 0) {
      const firstPath = dynamicRoutes[0].path
      console.warn(`访问不存在的路由,重定向到第一个动态路由: ${firstPath}`)
      return next({ path: firstPath })
    }
  }
​
  // 动态路由已注册,直接放行
  next()
})