在 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>
关键点说明
-
路由分割:
-
静态路由:所有用户可见(登录页/404)
-
动态路由:根据权限动态加载
-
-
路由转换:
-
后端返回标准化路由结构
-
使用
import()
动态加载组件 -
递归处理嵌套路由
-
-
权限控制:
-
路由守卫中检查登录状态
-
首次加载后标记路由状态
-
使用 Pinia 管理全局状态
-
-
刷新处理:
-
使用 provide/inject 提供刷新方法
-
通过 v-if 控制 router-view 重载
-
保持路由状态持久化
-
-
安全处理:
-
404 通配路由最后添加
-
路由加载失败时重置状态
-
组件路径白名单验证(防止恶意路径)
-
注意事项
-
组件路径安全:
typescript
复制
下载
// 在转换路由时添加安全验证 if (!/^[\w/-]+$/.test(route.componentPath)) { throw new Error(`Invalid component path: ${route.componentPath}`) }
-
路由缓存:
对于频繁访问的路由,可在获取后存储在 localStorage 中,设置合适的过期时间 -
路由更新:
当用户权限变更时,需要:typescript
复制
下载
// 重置路由状态 userStore.setRoutesLoaded(false) // 清空现有动态路由 router.removeRoute(routeName) // 重新加载路由
-
TypeScript 增强:
扩展路由 meta 类型:typescript
复制
下载
// src/types/router.d.ts declare module 'vue-router' { interface RouteMeta { title?: string icon?: string requiresAuth?: boolean hidden?: boolean } }
这个方案提供了完整的动态路由实现,包括类型安全、权限控制、菜单生成和状态管理,可以根据实际项目需求进行调整和扩展。