Skip to content

🛣️ 路由设计

ZsAdmin 前端采用 Vue Router 4.x 进行路由管理,实现了动态路由、权限路由、路由守卫等功能。本章节将详细介绍 ZsAdmin 前端的路由设计和使用方法。

1. 📋 路由管理概述

1.1 路由类型

ZsAdmin 前端的路由分为以下几种类型:

路由类型说明示例
静态路由固定的路由,不需要权限验证登录页、404 页
动态路由根据用户权限动态生成的路由仪表盘、用户管理等
嵌套路由包含子路由的路由系统管理路由下包含用户管理、角色管理等子路由
懒加载路由按需加载的路由,提高页面加载速度大型业务模块路由

1.2 路由配置项

ZsAdmin 前端的路由配置包含以下核心配置项:

配置项类型说明
pathstring路由路径
namestring路由名称
componentComponent路由对应的组件
redirectstring路由重定向地址
childrenRouteRecordRaw[]子路由配置
metaobject路由元信息

路由元信息配置

ZsAdmin 前端的路由元信息支持以下配置项,用于控制路由的访问权限、菜单显示和路由行为:

配置项类型是否必填说明
rolesstring[]控制访问页面的角色列表
requiresAuthboolean是否需要登录才能访问当前页面(每个路由必须声明)
iconstring侧边菜单显示的图标名称
titlestring侧边菜单和面包屑显示的标题
permissionsstring[]访问该路由需要的权限列表
hideInMenuboolean如果为 true,不在侧边菜单中显示该路由
hideChildrenInMenuboolean如果为 true,不在侧边菜单中显示该路由的子路由
alwaysShowboolean是否总是显示该路由,即使只有一个子路由
activeMenustring设置高亮显示的菜单名称,用于嵌套路由场景
breadcrumbboolean是否在面包屑中显示该路由
noCacheboolean如果为 true,不缓存该路由对应的组件
noAffixboolean如果为 true,该路由标签不会固定在标签栏中
sortnumber路由菜单项的排序值,数值越高越靠前

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 路由跳转

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
    end

7. 📝 总结

ZsAdmin 前端的路由设计采用了 Vue Router 4.x,实现了完整的路由管理功能,包括:

  • 路由类型:静态路由、动态路由、嵌套路由、懒加载路由
  • 路由配置:完整的路由配置项和元信息支持
  • 动态路由:从后端获取路由数据,动态生成前端路由
  • 路由守卫:全局守卫、独享守卫、组件内守卫
  • 路由使用:多种路由跳转方式和参数获取方法
  • 路由优化:懒加载、组件缓存、滚动行为配置