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

被折叠的 条评论
为什么被折叠?



