Vue3 + TS 动态路由系统实现总结

Vue3 + TS 动态路由系统实现总结

🎯 系统概述

完整的动态路由系统,支持基于用户角色的权限控制和菜单动态渲染。系统包含用户认证、动态路由管理、菜单生成、404处理等核心功能。

总体实现流程:

1.登录后将用户信息以及token信息保存在Pina状态管理仓库,并同步到LocalStorage

2.实现动态路由以及渲染菜单,流程如下:

a.在用户登录后,从服务器获取路由配置。

b.将动态路由添加到路由实例。

c.根据动态路由生成菜单,并处理跳转。

🏗️ 架构设计

核心模块

  • 路由管理 (src/router/index.ts) - 路由配置和动态路由管理
  • 状态管理 (src/stores/counter.ts) - 用户状态和路由数据存储
  • 组件转换 (src/utils/dynamicRoutes.ts) - 动态组件加载和路由转换
  • API接口 (src/api/api.ts) - 登录和路由数据获取
  • 页面组件 - 登录页、主页、404页面等

🔄 完整实现流程

1. 用户登录流程

用户输入登录信息 表单验证 调用登录API 获取Token和用户信息 保存到Pinia和localStorage 获取动态路由配置 添加动态路由到路由实例 跳转到主页

具体实现 (src/views/loginPage/LoginIndex.vue)
typescript 复制代码
// 登录成功后的处理流程
const submitForm = async (formEl: FormInstance | undefined) => {
  // 1. 表单验证
  // 2. 调用登录API
  login(loginForm.username, loginForm.password).then(async (res: LoginResponse) => {
    if (res.token) {
      // 3. 保存用户信息到Pinia
      userStore.setUser(res.token, res.user)

      // 4. 获取动态路由
      const routesData: RoutesResponse = await routes(res.token)

      // 5. 保存路由到Pinia
      userStore.setRoutes(routesData.routes)

      // 6. 添加动态路由到路由实例
      addDynamicRoutes(routesData.routes)

      // 7. 跳转到主页
      router.push('/home')
    }
  })
}

2. 动态路由管理

基础路由配置 (src/router/index.ts)
typescript 复制代码
const baseRoutes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'login',
    component: login,
    meta: { requiresAuth: false }
  },
  {
    path: '/home',
    name: 'home',
    component: home,
    meta: { requiresAuth: true }
  },
  {
    path: '/:pathMatch(.*)*', // 404路由 - 关键配置
    name: 'NotFound',
    component: () => import('@/views/errorPage/NotFound.vue'),
    meta: { requiresAuth: false }
  }
]
动态路由添加机制
typescript 复制代码
export function addDynamicRoutes(dynamicRoutes: RouteItem[]) {
  const transformedRoutes = transformRoutes(dynamicRoutes)

  // 1. 临时移除404路由
  if (router.hasRoute('NotFound')) {
    router.removeRoute('NotFound')
  }

  // 2. 将动态路由添加到home路由下(嵌套路由)
  transformedRoutes.forEach((route) => {
    router.addRoute('home', route)
  })

  // 3. 重新添加404路由到最后,确保优先级正确
  router.addRoute({
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/errorPage/NotFound.vue'),
    meta: { requiresAuth: false }
  })
}

3. 路由转换与组件加载

动态组件加载 (src/utils/dynamicRoutes.ts)
typescript 复制代码
// 获取所有可用组件
const modules = import.meta.glob('../views/**/*.vue')

// 路由转换
export function transformRoutes(dynamicRoutes: RouteItem[]): RouteRecordRaw[] {
  return dynamicRoutes.map((route): RouteRecordRaw => {
    const baseRoute: RouteRecordRaw = {
      path: route.path,
      name: route.name,
      component: loadComponent(route.component),
      meta: {
        title: route.meta.title,
        icon: route.meta.icon,
        requiresAuth: route.meta.requiresAuth
      }
    }

    // 处理子路由
    if (route.children && route.children.length > 0) {
      const children = route.children.map((child: RouteItem): RouteRecordRaw => {
        // 子路由路径处理:去掉父路由前缀
        let childPath = child.path
        if (childPath.startsWith(route.path)) {
          childPath = childPath.replace(route.path, '')
          if (!childPath.startsWith('/')) {
            childPath = '/' + childPath
          }
        }

        return {
          path: childPath,
          name: child.name,
          component: loadComponent(child.component),
          meta: {
            title: child.meta.title,
            icon: child.meta.icon,
            requiresAuth: child.meta.requiresAuth
          }
        }
      })

      return { ...baseRoute, children }
    }

    return baseRoute
  })
}

// 组件动态加载
function loadComponent(componentName: string) {
  // 组件路径映射:'Dashboard' -> '../views/Dashboard.vue'
  const componentPath = `../views/${componentName}.vue`

  if (modules[componentPath]) {
    return modules[componentPath]
  }

  // 备用路径尝试
  const fallbackPaths = [
    `../views/${componentName}/${componentName.split('/')[1] || componentName}.vue`,
    `../views/${componentName.toLowerCase()}.vue`
  ]

  for (const path of fallbackPaths) {
    if (modules[path]) {
      return modules[path]
    }
  }

  // 找不到组件时返回404页面
  return () => import('@/views/errorPage/NotFound.vue')
}

4. 菜单渲染系统

菜单组件 (src/views/homePage/HomeIndex.vue)
html 复制代码
<template>
  <el-menu :default-active="$route.path" class="el-menu-vertical-demo" router>
    <!-- 有子路由的菜单项 -->
    <template v-for="item in menuList" :key="item.id">
      <el-sub-menu v-if="item.children && item.children.length > 0" :index="item.path">
        <template #title>
          <el-icon><component :is="item.meta.icon" /></el-icon>
          <span>{{ item.meta.title }}</span>
        </template>
        <el-menu-item v-for="children in item.children" :key="children.id" :index="children.path">
          <el-icon><component :is="children.meta.icon" /></el-icon>
          <span>{{ children.meta.title }}</span>
        </el-menu-item>
      </el-sub-menu>

      <!-- 没有子路由的菜单项 -->
      <el-menu-item v-else :index="item.path">
        <el-icon><component :is="item.meta.icon" /></el-icon>
        <span>{{ item.meta.title }}</span>
      </el-menu-item>
    </template>
  </el-menu>
</template>

<script setup lang="ts">
const userStore = useUserStore()
const menuList = computed(() => userStore.routes)
</script>

5. 404页面处理机制

404页面设计
html 复制代码
<!-- src/views/errorPage/NotFound.vue -->
<template>
  <div class="not-found">
    <h1>404 - 页面未找到</h1>
    <p>抱歉,您访问的页面不存在</p>
    <el-button type="primary" @click="$router.push('/home')">返回首页</el-button>
  </div>
</template>
404处理策略
  1. 基础路由中的404:确保应用启动时就有404保护
  2. 动态路由中的404重排序:每次添加动态路由时重新排序404路由
  3. 组件加载失败的404回退:当动态组件加载失败时回退到404页面
typescript 复制代码
// 路由守卫处理
router.beforeEach((to, _from, next) => {
  const token = localStorage.getItem('token')

  if (to.meta.requiresAuth && !token) {
    next('/') // 未认证跳转登录
  } else if (to.path === '/' && token) {
    next('/home') // 已认证访问登录页跳转首页
  } else {
    next() // 正常放行
  }
})

6. 状态管理持久化

Pinia Store设计 (src/stores/counter.ts)
typescript 复制代码
export const useUserStore = defineStore('user', () => {
  const token = ref<string>('')
  const userInfo = ref<UserInfo | null>(null)
  const routes = ref<RouteItem[]>([])
  const isAuthenticated = computed(() => !!token.value)

  // 设置用户信息(持久化到localStorage)
  const setUser = (userToken: string, userData: UserInfo) => {
    token.value = userToken
    userInfo.value = userData
    localStorage.setItem('token', userToken)
    localStorage.setItem('userInfo', JSON.stringify(userData))
  }

  // 设置动态路由(持久化到localStorage)
  const setRoutes = (dynamicRoutes: RouteItem[]) => {
    routes.value = dynamicRoutes
    localStorage.setItem('routes', JSON.stringify(dynamicRoutes))
  }

  // 初始化用户状态(从localStorage恢复)
  const initUser = () => {
    const savedToken = localStorage.getItem('token')
    const savedUserInfo = localStorage.getItem('userInfo')
    const savedRoutes = localStorage.getItem('routes')

    if (savedToken && savedUserInfo) {
      token.value = savedToken
      userInfo.value = JSON.parse(savedUserInfo)
    }

    if (savedRoutes) {
      routes.value = JSON.parse(savedRoutes)
    }
  }

  return {
    token,
    userInfo,
    routes,
    isAuthenticated,
    setUser,
    setRoutes,
    clearUser,
    initUser
  }
})

7. 应用启动路由恢复

启动时路由初始化 (src/main.ts)
typescript 复制代码
// 应用启动时,如果用户已登录且有路由数据,立即加载动态路由
if (userStore.isAuthenticated && userStore.routes.length > 0) {
  console.log('应用启动:用户已登录,加载动态路由')
  addDynamicRoutes(userStore.routes)
}
主页路由检查 (src/views/homePage/HomeIndex.vue)
typescript 复制代码
onMounted(async () => {
  // 检查登录状态
  if (!userStore.isAuthenticated) {
    router.push('/')
    return
  }

  // 如果路由为空,重新获取
  if (menuList.value.length === 0) {
    try {
      clearDynamicRoutes()
      const routesData: RoutesResponse = await routes(userStore.token)
      userStore.setRoutes(routesData.routes)
      addDynamicRoutes(routesData.routes)
    } catch (error) {
      console.error('获取路由失败:', error)
    }
  }
})

🔧 关键技术点

1. 路由优先级管理

  • 404路由必须放在最后,避免误拦截正常路由
  • 动态路由添加时需要重新排序404路由

2. 组件懒加载

typescript 复制代码
// 使用Vite的glob API实现组件懒加载
const modules = import.meta.glob('../views/**/*.vue')

3. 嵌套路由结构

typescript 复制代码
// 动态路由作为home路由的子路由
router.addRoute('home', dynamicRoute)

4. 类型安全

typescript 复制代码
// 完整的TypeScript类型定义
export interface RouteItem {
  id: number
  index: number
  path: string
  name: string
  component: string
  meta: {
    title: string
    icon: string
    requiresAuth: boolean
  }
  children?: RouteItem[]
}

🎨 系统特性

✅ 优势

  1. 完整的404保护:任何情况下访问不存在的路径都有404页面
  2. 状态持久化:刷新页面后用户状态和路由配置不丢失
  3. 动态权限控制:根据服务器返回的路由配置控制菜单显示
  4. 组件懒加载:按需加载页面组件,优化性能
  5. 类型安全:完整的TypeScript类型支持
  6. 路由重用:支持路由的动态添加和清理

🔧 处理边界情况

  1. 页面刷新:自动恢复路由状态
  2. 组件加载失败:回退到404页面
  3. 路由冲突:清理旧路由后添加新路由
  4. 未授权访问:路由守卫拦截跳转登录

🚀 使用流程总结

是 否 是 否 应用启动 用户已登录? 恢复动态路由 显示登录页 进入主页 用户登录 获取路由配置 添加动态路由 渲染菜单 用户导航 路由存在? 显示对应页面 显示404页面

📝 注意事项

  1. 404路由位置:确保404路由始终在路由表的最后位置
  2. 组件路径映射:动态组件路径必须与实际文件结构匹配
  3. 路由清理:重新登录时需要清理旧的动态路由
  4. 状态同步:Pinia状态与localStorage保持同步
  5. 权限控制:服务器返回的路由配置已经过权限过滤

这个动态路由系统提供了完整的用户认证、权限控制和菜单管理功能,具有良好的扩展性和维护性。

相关推荐
贺今宵2 小时前
el-table-v2element plus+大量数据展示虚拟表格实现自定义排序,选择样式调整行高亮
javascript·vue.js·ecmascript
计算机程序设计小李同学2 小时前
基于 Spring Boot 和 Vue.js 技术栈的网上订餐系统
vue.js·spring boot·后端
毕设十刻3 小时前
基于Vue的家教预约系统7fisz(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
风舞红枫3 小时前
node代理vue打包后的文件,实现本地测试
前端·javascript·vue.js·node.js
全栈陈序员3 小时前
Vue 实例挂载的过程是怎样的?
前端·javascript·vue.js·学习·前端框架
+VX:Fegn08954 小时前
计算机毕业设计|基于springboot + vue健康茶饮销售管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
怒放的生命19914 小时前
Vue 2 vs Vue 3对比 编译原理不同深度解析
前端·javascript·vue.js
小北方城市网4 小时前
第7课:Vue 3应用性能优化与进阶实战——让你的应用更快、更流畅
前端·javascript·vue.js·ai·性能优化·正则表达式·json
大学生资源网5 小时前
基于Vue的网上购物管理系统的设计与实现(java+vue+源码+文档)
java·前端·vue.js·spring boot·后端·源码