🛣️ 路由设计
ZsAdmin 前端采用 Vue Router 4.x 进行路由管理,实现了动态路由、权限路由、路由守卫等功能。本章节将详细介绍 ZsAdmin 前端的路由设计和使用方法。
1. 📋 路由管理概述
1.1 路由类型
ZsAdmin 前端的路由分为以下几种类型:
| 路由类型 | 说明 | 示例 |
|---|---|---|
| 静态路由 | 固定的路由,不需要权限验证 | 登录页、404 页 |
| 动态路由 | 根据用户权限动态生成的路由 | 仪表盘、用户管理等 |
| 嵌套路由 | 包含子路由的路由 | 系统管理路由下包含用户管理、角色管理等子路由 |
| 懒加载路由 | 按需加载的路由,提高页面加载速度 | 大型业务模块路由 |
1.2 路由配置项
ZsAdmin 前端的路由配置包含以下核心配置项:
| 配置项 | 类型 | 说明 |
|---|---|---|
| path | string | 路由路径 |
| name | string | 路由名称 |
| component | Component | 路由对应的组件 |
| redirect | string | 路由重定向地址 |
| children | RouteRecordRaw[] | 子路由配置 |
| meta | object | 路由元信息 |
路由元信息配置
ZsAdmin 前端的路由元信息支持以下配置项,用于控制路由的访问权限、菜单显示和路由行为:
| 配置项 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| roles | string[] | 否 | 控制访问页面的角色列表 |
| requiresAuth | boolean | 是 | 是否需要登录才能访问当前页面(每个路由必须声明) |
| icon | string | 否 | 侧边菜单显示的图标名称 |
| title | string | 否 | 侧边菜单和面包屑显示的标题 |
| permissions | string[] | 否 | 访问该路由需要的权限列表 |
| hideInMenu | boolean | 否 | 如果为 true,不在侧边菜单中显示该路由 |
| hideChildrenInMenu | boolean | 否 | 如果为 true,不在侧边菜单中显示该路由的子路由 |
| alwaysShow | boolean | 否 | 是否总是显示该路由,即使只有一个子路由 |
| activeMenu | string | 否 | 设置高亮显示的菜单名称,用于嵌套路由场景 |
| breadcrumb | boolean | 否 | 是否在面包屑中显示该路由 |
| noCache | boolean | 否 | 如果为 true,不缓存该路由对应的组件 |
| noAffix | boolean | 否 | 如果为 true,该路由标签不会固定在标签栏中 |
| sort | number | 否 | 路由菜单项的排序值,数值越高越靠前 |
1.3 路由元信息类型声明
为了在 TypeScript 中获得更好的类型提示,ZsAdmin 扩展了 Vue Router 的 RouteMeta 接口:
typescript
declare module 'vue-router' {
interface RouteMeta {
roles?: string[];
requiresAuth: boolean;
icon?: string;
title?: string;
permissions?: string[];
hideInMenu?: boolean;
hideChildrenInMenu?: boolean;
alwaysShow?: boolean;
activeMenu?: string;
breadcrumb?: boolean;
noCache?: boolean;
noAffix?: boolean;
sort?: number;
}
}2. 🔧 路由配置
2.1 基础路由配置
typescript
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Layout from '@/layouts/index.vue'
// 静态路由 - 无需权限验证
const constantRoutes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录',
requiresAuth: false,
hideInMenu: true
}
},
{
path: '/403',
name: '403',
component: () => import('@/views/error/403.vue'),
meta: {
title: '无权限',
requiresAuth: false,
hideInMenu: true
}
},
{
path: '/404',
name: '404',
component: () => import('@/views/error/404.vue'),
meta: {
title: '页面不存在',
requiresAuth: false,
hideInMenu: true
}
}
]
// 动态路由 - 需要权限验证
const asyncRoutes: RouteRecordRaw[] = [
{
path: '/',
name: 'Layout',
component: Layout,
redirect: '/dashboard',
meta: {
title: '首页',
icon: 'dashboard',
requiresAuth: true
},
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: {
title: '仪表盘',
icon: 'dashboard',
requiresAuth: true,
}
}
]
},
{
path: '/system',
name: 'System',
component: Layout,
meta: {
title: '系统管理',
icon: 'setting',
requiresAuth: true
},
children: [
{
path: 'user',
name: 'User',
component: () => import('@/views/system/user/index.vue'),
meta: {
title: '用户管理',
icon: 'user',
requiresAuth: true,
}
},
{
path: 'role',
name: 'Role',
component: () => import('@/views/system/role/index.vue'),
meta: {
title: '角色管理',
icon: 'role',
requiresAuth: true,
}
},
{
path: 'menu',
name: 'Menu',
component: () => import('@/views/system/menu/index.vue'),
meta: {
title: '菜单管理',
icon: 'menu',
requiresAuth: true,
}
}
]
}
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [...constantRoutes], // 初始只加载静态路由
scrollBehavior: () => ({ top: 0 })
})
export default router
export { constantRoutes, asyncRoutes }2.2 动态路由生成
在实际应用中,路由通常是根据用户权限从后端动态获取的。以下是动态路由生成的实现示例:
typescript
import type { AppRouteRecordRaw, Router } from 'vue-router'
import { DEFAULT_LAYOUT } from '@/router/constants'
/**
* 加载组件
* @param component 组件路径
*/
const loadView = (component: string) => {
return () => import(`@/views/${component}`)
}
/**
* 转换后端路由数据为前端路由配置
* @param data 后端返回的路由数据
*/
export const transformRoutes = (data: any[]): AppRouteRecordRaw[] => {
return data.map((item: any) => {
const route: AppRouteRecordRaw = {
path: item.path,
name: item.name,
component: item.component ? loadView(item.component) : DEFAULT_LAYOUT,
meta: {
roles: item.meta.roles || ['*'],
title: item.meta.title,
requiresAuth: item.meta.requiresAuth !== false,
icon: item.meta.icon,
sort: item.meta.sort || 0,
hideInMenu: item.meta.hideInMenu || false,
hideChildrenInMenu: item.meta.hideChildrenInMenu || false,
noAffix: item.meta.noAffix || false,
noCache: item.meta.noCache || false,
},
children: item.children ? transformRoutes(item.children) : [],
}
return route
})
}
/**
* 向路由实例添加动态路由
* @param router 路由实例
* @param routes 动态路由数组
*/
export const addRoutesToRouter = (router: Router, routes: AppRouteRecordRaw[]) => {
router.addRoute({
path: '/',
component: DEFAULT_LAYOUT,
children: routes,
})
}
// 使用示例
// const newRoutes = transformRoutes(appStore.serverMenu)
// addRoutesToRouter(router, newRoutes)3. 🛡️ 路由守卫
3.1 全局路由守卫
全局路由守卫用于在路由切换前进行权限校验、页面标题设置等操作:
typescript
import router from './index'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { useUserStore } from '@/stores/user'
import { useAppStore } from '@/stores/app'
import { transformRoutes, addRoutesToRouter } from './utils'
// 进度条配置
NProgress.configure({
showSpinner: false,
trickleSpeed: 200
})
// 白名单路由 - 无需登录即可访问
const WHITE_LIST = ['Login', '403', '404']
/**
* 检查是否已登录
*/
const isLogin = (): boolean => {
return !!localStorage.getItem('token')
}
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
NProgress.start()
const isLoginPage = to.path === '/login'
// 已登录的情况
if (isLogin()) {
if (isLoginPage) {
next('/dashboard') // 已登录跳转到首页
NProgress.done()
return
}
const userStore = useUserStore()
const appStore = useAppStore()
// 未加载动态路由的情况
if (!appStore.hasFetchedMenus) {
try {
await userStore.info() // 获取用户信息
await appStore.fetchServerMenuConfig() // 获取菜单配置
const newRoutes = transformRoutes(appStore.serverMenu) // 转换路由
addRoutesToRouter(router, newRoutes) // 添加动态路由
next({ ...to, replace: true }) // 重新导航到当前路由
} catch (error) {
console.error('路由守卫加载菜单失败:', error)
await userStore.logout() // 清除用户信息
next('/login') // 跳转到登录页
} finally {
NProgress.done()
}
} else {
// 已加载动态路由,直接放行
next()
NProgress.done()
}
} else {
// 未登录的情况
if (WHITE_LIST.includes(to.name as string)) {
next() // 白名单路由直接放行
} else {
next({
name: 'Login',
query: { redirect: to.path, ...to.query } as LocationQueryRaw,
}) // 跳转到登录页
}
NProgress.done()
}
})
// 全局后置守卫
router.afterEach((to) => {
setRouteEmitter(to);
})3.2 路由独享守卫
路由独享守卫只对当前路由生效:
typescript
const routes: RouteRecordRaw[] = [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: {
title: '仪表盘',
icon: 'dashboard'
},
beforeEnter: (to, from, next) => {
// 路由独享守卫逻辑
console.log('进入仪表盘路由')
next()
}
}
]3.3 组件内守卫
组件内守卫可以在组件内部监听路由变化:
vue
<template>
<div>
<h1>用户管理</h1>
<!-- 组件内容 -->
</div>
</template>
<script setup lang="ts">
import { onBeforeRouteEnter, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// 进入路由前触发
onBeforeRouteEnter((to, from, next) => {
console.log('进入用户管理页面')
next()
})
// 离开路由前触发
onBeforeRouteLeave((to, from, next) => {
console.log('离开用户管理页面')
next()
})
// 路由更新时触发(如参数变化)
onBeforeRouteUpdate((to, from, next) => {
console.log('用户管理页面参数更新')
next()
})
</script>4. 🚀 路由使用
4.1 路由跳转
1. 使用 <router-link> 组件
vue
<template>
<div>
<!-- 基本用法 -->
<router-link to="/dashboard">仪表盘</router-link>
<!-- 使用命名路由和参数 -->
<router-link :to="{ name: 'UserDetail', params: { id: 1 } }">用户详情</router-link>
<!-- 使用查询参数 -->
<router-link :to="{ path: '/user', query: { page: 1, size: 10 } }">用户列表</router-link>
<!-- 替换当前路由 -->
<router-link to="/dashboard" replace>仪表盘</router-link>
</div>
</template>2. 使用 router.push() 方法
typescript
import { useRouter } from 'vue-router'
const router = useRouter()
// 跳转到指定路径
router.push('/dashboard')
// 使用命名路由跳转
router.push({ name: 'UserDetail', params: { id: 1 } })
// 使用查询参数跳转
router.push({ path: '/user', query: { page: 1, size: 10 } })
// 替换当前路由
router.replace('/dashboard')4.2 路由参数获取
1. 使用 useRoute() 钩子
typescript
import { useRoute } from 'vue-router'
const route = useRoute()
// 获取动态参数
const userId = route.params.id as string
// 获取查询参数
const page = Number(route.query.page || 1)
const size = Number(route.query.size || 10)2. 在组件中获取
vue
<template>
<div>
<h1>用户详情</h1>
<p>用户ID: {{ $route.params.id }}</p>
<p>当前页码: {{ $route.query.page }}</p>
</div>
</template>4.3 路由导航
typescript
import { useRouter } from 'vue-router'
const router = useRouter()
// 后退一步
router.back()
// 前进一步
router.forward()
// 前进或后退指定步数
router.go(2) // 前进两步
router.go(-1) // 后退一步5. ⚡ 路由优化
5.1 路由懒加载
使用路由懒加载可以提高页面加载速度,减少初始加载时间:
typescript
// 传统方式(不推荐)
import Dashboard from '@/views/dashboard/index.vue'
// 懒加载方式(推荐)
const Dashboard = () => import('@/views/dashboard/index.vue')
// 带命名的懒加载(推荐)
const Dashboard = () => import(/* webpackChunkName: "dashboard" */ '@/views/dashboard/index.vue')5.2 路由组件缓存
使用 <keep-alive> 组件可以缓存路由组件,减少组件的创建和销毁次数:
vue
<template>
<div id="app">
<keep-alive :include="cachedViews">
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</keep-alive>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
// 从状态管理中获取需要缓存的组件名称列表
const cachedViews = computed(() => appStore.cachedViews)
</script>5.3 滚动行为
配置路由的滚动行为,控制页面切换时的滚动位置:
typescript
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [...constantRoutes],
scrollBehavior(to, from, savedPosition) {
// 如果有保存的滚动位置,恢复到该位置(如浏览器前进后退)
if (savedPosition) {
return savedPosition
} else {
// 否则滚动到顶部
return { top: 0 }
}
}
})6. 路由流程图
mermaid
sequenceDiagram
participant User as 用户
participant Router as 路由系统
participant Guard as 路由守卫
participant Backend as 后端
participant Store as 状态管理
User->>Router: 访问路由 /dashboard
Router->>Guard: 触发全局前置守卫
Guard->>Guard: 检查是否已登录
alt 未登录
Guard->>Router: 跳转到登录页
else 已登录
Guard->>Store: 检查动态路由是否已加载
alt 动态路由未加载
Store->>Backend: 请求用户菜单数据
Backend-->>Store: 返回菜单数据
Store->>Guard: 转换为前端路由配置
Guard->>Router: 添加动态路由
Router->>Router: 重新导航到 /dashboard
else 动态路由已加载
Guard->>Router: 允许访问
Router->>User: 渲染页面
end
end7. 📝 总结
ZsAdmin 前端的路由设计采用了 Vue Router 4.x,实现了完整的路由管理功能,包括:
- 路由类型:静态路由、动态路由、嵌套路由、懒加载路由
- 路由配置:完整的路由配置项和元信息支持
- 动态路由:从后端获取路由数据,动态生成前端路由
- 路由守卫:全局守卫、独享守卫、组件内守卫
- 路由使用:多种路由跳转方式和参数获取方法
- 路由优化:懒加载、组件缓存、滚动行为配置