Vue3+TypeScript动态路由实现方案

在 Vue 3 + TypeScript 中实现动态路由,通常涉及以下关键步骤。这里提供一个完整的实现方案:

1. 基础路由配置

typescript

复制

下载

// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

// 静态路由(所有用户可见)
const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue')
  },
  {
    path: '/404',
    name: 'NotFound',
    component: () => import('@/views/404.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes: constantRoutes
})

export default router

2. 定义动态路由类型

typescript

复制

下载

// src/types/router.d.ts
import { RouteRecordRaw } from 'vue-router'

// 后端返回的路由结构
export interface BackendRoute {
  path: string
  name: string
  componentPath: string
  children?: BackendRoute[]
  meta?: {
    title?: string
    icon?: string
    requiresAuth?: boolean
  }
}

// 前端路由映射
export type DynamicRoutes = RouteRecordRaw[]

3. 路由加载工具函数

typescript

复制

下载

// src/utils/route-loader.ts
import { RouteRecordRaw } from 'vue-router'
import { BackendRoute } from '@/types/router'

/**
 * 转换后端路由为前端路由
 * @param routes 后端路由配置
 * @param parentPath 父级路径(用于嵌套路由)
 */
export const transformRoutes = (
  routes: BackendRoute[],
  parentPath = ''
): RouteRecordRaw[] => {
  return routes.map(route => {
    const fullPath = `${parentPath}${route.path}`
    
    const frontendRoute: RouteRecordRaw = {
      path: fullPath,
      name: route.name,
      component: () => import(`@/views/${route.componentPath}.vue`),
      meta: route.meta || {},
      children: route.children 
        ? transformRoutes(route.children, fullPath)
        : []
    }
    
    return frontendRoute
  })
}

/**
 * 添加动态路由
 * @param routes 动态路由配置
 */
export const addDynamicRoutes = (routes: RouteRecordRaw[]) => {
  const router = useRouter()
  
  routes.forEach(route => {
    router.addRoute(route)
  })
  
  // 最后添加404通配路由
  router.addRoute({
    path: '/:catchAll(.*)',
    redirect: '/404'
  })
}

4. 权限控制和路由加载

typescript

复制

下载

// src/permission.ts
import router from './router'
import { useUserStore } from '@/stores/user'
import { transformRoutes, addDynamicRoutes } from '@/utils/route-loader'
import type { BackendRoute } from '@/types/router'

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  
  // 登录页直接放行
  if (to.path === '/login') {
    next()
    return
  }

  // 检查用户是否已登录
  if (!userStore.token) {
    next(`/login?redirect=${to.path}`)
    return
  }

  // 已登录但未加载路由
  if (!userStore.routesLoaded) {
    try {
      // 模拟从API获取路由
      const backendRoutes: BackendRoute[] = await fetchUserRoutes()
      
      // 转换路由
      const dynamicRoutes = transformRoutes(backendRoutes)
      
      // 添加动态路由
      addDynamicRoutes(dynamicRoutes)
      
      // 存储路由状态
      userStore.setRoutes(dynamicRoutes)
      userStore.setRoutesLoaded(true)
      
      // 重定向到原始请求
      next({ ...to, replace: true })
    } catch (error) {
      // 清除用户数据
      userStore.reset()
      next(`/login?redirect=${to.path}`)
    }
  } else {
    next()
  }
})

// 模拟API请求
async function fetchUserRoutes(): Promise<BackendRoute[]> {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve([
        {
          path: '/dashboard',
          name: 'Dashboard',
          componentPath: 'Dashboard',
          meta: { title: '控制台', icon: 'dashboard' }
        },
        {
          path: '/user',
          name: 'User',
          componentPath: 'UserLayout',
          children: [
            {
              path: 'list',
              name: 'UserList',
              componentPath: 'User/List',
              meta: { title: '用户管理' }
            },
            {
              path: 'role',
              name: 'Role',
              componentPath: 'User/Role',
              meta: { title: '角色管理' }
            }
          ]
        }
      ])
    }, 500)
  })
}

5. 在Pinia中管理路由状态

typescript

复制

下载

// src/stores/user.ts
import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'

interface UserState {
  token: string | null
  routesLoaded: boolean
  dynamicRoutes: RouteRecordRaw[]
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    token: localStorage.getItem('token'),
    routesLoaded: false,
    dynamicRoutes: []
  }),
  actions: {
    setToken(token: string) {
      this.token = token
      localStorage.setItem('token', token)
    },
    setRoutes(routes: RouteRecordRaw[]) {
      this.dynamicRoutes = routes
    },
    setRoutesLoaded(loaded: boolean) {
      this.routesLoaded = loaded
    },
    reset() {
      this.token = null
      this.routesLoaded = false
      this.dynamicRoutes = []
      localStorage.removeItem('token')
    }
  }
})

6. 动态生成菜单(示例)

vue

复制

下载

<!-- src/components/Layout/Sidebar.vue -->
<script setup lang="ts">
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 递归生成菜单项
const generateMenu = (routes: RouteRecordRaw[]) => {
  return routes.filter(route => {
    // 过滤隐藏的路由
    return !route.meta?.hidden
  }).map(route => ({
    path: route.path,
    title: route.meta?.title || route.name,
    icon: route.meta?.icon,
    children: route.children ? generateMenu(route.children) : []
  }))
}

const menuItems = computed(() => 
  generateMenu(userStore.dynamicRoutes)
)
</script>

<template>
  <el-menu>
    <template v-for="item in menuItems" :key="item.path">
      <el-sub-menu v-if="item.children.length" :index="item.path">
        <template #title>
          <i :class="`el-icon-${item.icon}`"></i>
          <span>{{ item.title }}</span>
        </template>
        <el-menu-item 
          v-for="child in item.children" 
          :key="child.path" 
          :index="child.path"
        >
          {{ child.title }}
        </el-menu-item>
      </el-sub-menu>
      <el-menu-item v-else :index="item.path">
        <i :class="`el-icon-${item.icon}`"></i>
        <span>{{ item.title }}</span>
      </el-menu-item>
    </template>
  </el-menu>
</template>

7. 处理路由刷新问题

在 App.vue 中添加路由视图:

vue

复制

下载

<!-- src/App.vue -->
<template>
  <router-view v-slot="{ Component }">
    <transition name="fade">
      <component :is="Component" v-if="routerViewVisible" />
    </transition>
  </router-view>
</template>

<script setup lang="ts">
import { ref, provide, nextTick } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
const routerViewVisible = ref(true)

// 提供刷新方法
const reload = () => {
  routerViewVisible.value = false
  nextTick(() => {
    routerViewVisible.value = true
  })
}
provide('reload', reload)

// 初始化时检查路由状态
if (userStore.token && !userStore.routesLoaded) {
  // 触发路由重新加载
  reload()
}
</script>

关键点说明

  1. 路由分割

    • 静态路由:所有用户可见(登录页/404)

    • 动态路由:根据权限动态加载

  2. 路由转换

    • 后端返回标准化路由结构

    • 使用 import() 动态加载组件

    • 递归处理嵌套路由

  3. 权限控制

    • 路由守卫中检查登录状态

    • 首次加载后标记路由状态

    • 使用 Pinia 管理全局状态

  4. 刷新处理

    • 使用 provide/inject 提供刷新方法

    • 通过 v-if 控制 router-view 重载

    • 保持路由状态持久化

  5. 安全处理

    • 404 通配路由最后添加

    • 路由加载失败时重置状态

    • 组件路径白名单验证(防止恶意路径)

注意事项

  1. 组件路径安全

    typescript

    复制

    下载

    // 在转换路由时添加安全验证
    if (!/^[\w/-]+$/.test(route.componentPath)) {
      throw new Error(`Invalid component path: ${route.componentPath}`)
    }
  2. 路由缓存
    对于频繁访问的路由,可在获取后存储在 localStorage 中,设置合适的过期时间

  3. 路由更新
    当用户权限变更时,需要:

    typescript

    复制

    下载

    // 重置路由状态
    userStore.setRoutesLoaded(false)
    // 清空现有动态路由
    router.removeRoute(routeName)
    // 重新加载路由
  4. TypeScript 增强
    扩展路由 meta 类型:

    typescript

    复制

    下载

    // src/types/router.d.ts
    declare module 'vue-router' {
      interface RouteMeta {
        title?: string
        icon?: string
        requiresAuth?: boolean
        hidden?: boolean
      }
    }

这个方案提供了完整的动态路由实现,包括类型安全、权限控制、菜单生成和状态管理,可以根据实际项目需求进行调整和扩展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值