Vue3后台管理系统(二):路由系统设计与实现
一、Vue Router 4.x的基础配置
javascript
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router' // Vue Router核心API
import { constantRoutes } from './routes' // 导入静态路由配置
import { ElNotification } from 'element-plus' // ElementPlus通知组件
import { useAuthStore } from '@/stores/auth' // 认证状态管理
import { useUser } from '@/composables/useUser' // 用户信息相关组合式API
import { usePermissionStore } from '@/composables/usePermission' // 权限相关组合式API
import NProgress from 'nprogress' // 进度条
import '@/styles/nprogress.scss' // 进度条样式
import { TokenService } from '@/utils/token.js' // Token管理服务
// 配置 NProgress 进度条
NProgress.configure({
easing: 'ease-out', // 动画方式
speed: 400, // 递增进度条的速度
showSpinner: false, // 不显示加载ico
trickleSpeed: 200, // 自动递增间隔
minimum: 0.3, // 初始化时的最小百分比
})
// 修复重复点击路由报错问题
// 重写路由的push方法,捕获重复导航错误
// 在Vue Router中,如果快速点击同一路由链接会导致导航冗余错误
// 通过这种方式可以避免这个错误,提高用户体验
const originalPush = createRouter.prototype.push
createRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch((err) => err)
}
// 路由模式选择
// 1. History模式需要服务器配置,但URL更友好
// 2. Hash模式兼容性更好,但URL带有#号
// 3. 滚动行为配置增强用户体验,避免切换页面后停留在底部
const router = createRouter({
history: createWebHistory(), // 使用HTML5 History模式,URL更美观没有#号
scrollBehavior: () => ({ top: 0 }), // 切换路由时滚动到页面顶部
routes: [...constantRoutes], // 初始化只加载静态路由
})
// 异步组件加载失败
// 处理代码分割导致的加载失败
// 1. 检测特定的chunk加载失败错误模式
// 2. 自动刷新页面重新加载资源,提高用户体验
// 3. 这种错误通常在网络不稳定或部署后首次访问时可能发生
router.onError((error) => {
const pattern = /Loading chunk (\d)+ failed/g
const isChunkLoadFailed = error.message.match(pattern)
if (isChunkLoadFailed) {
window.location.reload() // 当加载chunk失败时,自动刷新页面重试
}
})
// 路由白名单
const whiteList = ['/login']
// 路由守卫
router.beforeEach(async (to, from, next) => {
// 开启进度条
NProgress.start()
const authStore = useAuthStore()
const { getUserInfo } = useUser()
const permissionStore = usePermissionStore()
// 白名单直接通过
if (whiteList.includes(to.path)) {
return next()
}
// URL中的token
if (to.query.token) {
const token = to.query.token
TokenService.setTokens('Bearer ' + token)
// 移除token参数,避免暴露在URL中
const { token: _, ...query } = to.query
return next({ path: to.path, query })
// 1. 检测URL中是否存在token参数
// 2. 保存token并清除URL中的token参数(安全考虑)
// 3. 适用于第三方系统跳转或分享链接直接登录的场景
}
// 检查是否已登录
if (!authStore.isAuthenticated) {
ElNotification({
title: '登录失败',
message: '登录过期',
type: 'error',
})
return next(`/login?redirect=${to.path}`) // 跳转到登录页,并记录原目标路径
}
try {
// 只在首次加载或刷新页面时执行路由生成
if (!permissionStore.isRoutesGenerated) {
// 如果没有角色信息,先获取用户信息
if (!authStore.hasRoles) {
await getUserInfo()
}
// 生成动态路由
const accessRoutes = await permissionStore.generateRoutes(authStore.state.roles)
// 添加动态路由
accessRoutes.forEach((route) => {
router.addRoute(route)
})
// 重定向到原目标,确保路由已更新
return next({ ...to, replace: true })
// 动态路由生成
// 1. 根据用户角色动态生成路由,实现权限控制
// 2. 避免每次路由跳转都重新生成路由,提高性能
// 3. 使用replace跳转,不产生新的历史记录
}
// 默认首页重定向到账户页
if (to.path === '/') {
return next({ path: '/my/account', replace: true })
}
next()
} catch (error) {
// 异常
console.log('🚀 ~ [error]', error)
authStore.reset()
permissionStore.resetRoutes()
ElNotification({
title: '错误',
message: error.message || '登录失败,请重新登录',
type: 'error',
})
next('/login')
}
})
// 路由后置守卫
router.afterEach(() => {
// 结束进度条
NProgress.done()
})
export default router
这个基本配置已经完成了:
- 创建路由实例,使用History模式
- 配置全局路由守卫,实现权限控制
- 配置NProgress进度条,提升用户体验
- 实现动态路由加载
- 处理路由错误和异步组件加载失败
- 支持URL中携带token的自动登录
二、路由结构设计
javascript
// src/router/routes.js
/**
* @description 路由配置文件
* @Author Wangkaibing
* @Date 2025-04-13
* @features
* 动态导入
* 使用预定义的Layout组件以减少重复加载
*/
// 静态路由 - 无需权限验证
export const constantRoutes = [
{
path: '/',
redirect: '/my', // 根路径重定向到个人中心页面
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'), // 动态导入,实现代码分割
meta: {
title: '登录', // 页面标题,用于面包屑、标签页等显示
},
// 登录页使用动态导入可以减小首屏加载体积
// 因为登录后不再需要访问此页面,没必要打包到主包中
},
{
path: '/404',
name: 'NotFound',
component: () => import('@/views/404.vue'),
meta: {
title: '404 Not Found',
},
// 错误页面也适合动态导入,因为正常使用时很少会被访问
},
{
path: '/403',
name: 'Forbidden',
component: () => import('@/views/403.vue'),
meta: {
title: '403 Forbidden',
},
},
]
// 预加载布局组件以提高页面切换性能
const Layout = () => import('@/layout/index.vue')
// 布局组件单独提取
// 1. 虽然也是动态导入,但通过变量方式可以在多个路由中复用
// 2. 布局组件在切换子路由时不需要重复加载,提升性能
// 3. 保持代码简洁,避免在每个路由配置中重复相同的导入语句
export const defaultRoutes = [
{
path: '/my',
// 使用预定义的Layout组件以减少重复加载
component: Layout,
redirect: '/my/account',
meta: {
title: '我的账户',
icon: '5',
sidebarVisible: true, // 是否在侧边栏显示此菜单项
},
children: [
{
path: 'account',
name: 'MyAccount',
component: () => import('@/views/my/account.vue'),
meta: {
title: '我的账户',
},
},
],
// 嵌套路由结构
// 1. 外层路由使用共享Layout
// 2. 子路由只需关注内容区域的渲染
// 3. meta配置丰富的元信息,用于权限控制和UI渲染
},
]
export const platform = [...]
// 通配路由
export const catchAllRoute = {
path: '/:pathMatch(.*)*', // Vue Router 4.x的通配路由写法
redirect: '/404',
// 兜底路由
// 1. 必须放在最后加载,捕获所有未匹配的路由
// 2. 重定向到404页面,提供友好的错误提示
// 3. Vue Router 4.x使用pathMatch参数代替了之前的*写法
}
模块化路由配置:
- 结构清晰:将路由按功能模块分类
- 易于维护:每个模块可以独立管理
- 按需加载:可以根据权限动态加载不同模块的路由
三、权限控制实现
3.1 基于角色的路由控制
在项目中使用一个专门的组合式API来处理权限和路由生成:
javascript
// src/composables/usePermission.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { defaultRoutes, platform, catchAllRoute } from '@/router/routes'
export const usePermissionStore = defineStore('permission', () => {
// 存储路由配置
const routes = ref([])
// 是否已生成路由
const isRoutesGenerated = ref(false)
// 组合式API风格的Pinia store
// 1. 使用组合式API定义store,比Options API更灵活
// 2. 使用ref而不是reactive,方便解构且不会丢失响应性
// 3. Vue3+Pinia组合使得状态管理更加直观和类型安全
// 生成路由
const generateRoutes = async (roles) => {
try {
let accessedRoutes = []
// 如果是管理员,获取所有路由
if (roles.includes('ROLE_ADMIN')) {
accessedRoutes = [...defaultRoutes, ...platform]
} else {
// 否则根据角色过滤路由
accessedRoutes = filterRoutesByRoles(roles)
}
routes.value = accessedRoutes
isRoutesGenerated.value = true
return accessedRoutes
} catch (error) {
console.error('生成路由失败:', error)
return []
}
// 异步路由生成
// 1. 返回Promise,方便调用者使用async/await
// 2. 完善的错误处理,确保异常情况下返回空数组不会阻塞应用
// 3. 根据不同角色生成不同路由,实现权限隔离
}
// 根据角色过滤路由
const filterRoutesByRoles = (roles) => {
const res = []
// 默认路由对所有人开放
res.push(...defaultRoutes)
// 平台管理路由需要特定角色
if (roles.some(role => ['ROLE_ADMIN', 'ROLE_MANAGER'].includes(role))) {
res.push(...platform)
}
return res
// 基于角色的路由过滤
// 1. 将默认路由给所有已登录用户
// 2. 针对特定模块设置角色要求
// 3. 使用Array.some方法检查是否满足任一角色要求,实现"或"逻辑
// 4. 这种方式比遍历权限树更高效,适合中小型应用
}
// 重置路由状态
const resetRoutes = () => {
routes.value = []
isRoutesGenerated.value = false
// 1. 用于登出时清空权限路由
// 2. 确保权限状态不会跨会话保留
// 3. 下次登录时会重新计算权限
}
return {
routes,
isRoutesGenerated,
generateRoutes,
resetRoutes
}
})
3.2 路由元信息(Meta)
在路由配置中添加meta属性来携带额外信息:
javascript
{
path: 'role',
name: 'PlatformRole',
component: () => import('@/views/platform/role.vue'),
meta: {
title: '角色管理', // 用于面包屑、标题等
icon: '6', // 图标
roles: ['admin'], // 可访问角色
permissions: ['role:view'], // 所需权限
sidebarVisible: true // 是否在侧边栏显示
}
}
3.3 指令级权限控制
javascript
// 示例:根据角色控制元素显示
<el-button v-role="'ROLE_ADMIN'">管理员专属按钮</el-button>
// 示例:根据权限控制元素显示
<el-button v-permission="'user:delete'">删除用户</el-button>
四、路由导航优化
4.1 路由过渡动画
使用Vue3的<transition>
组件为路由切换添加过渡动画:
vue
<template>
<router-view v-slot="{ Component }">
<transition name="el-fade-in">
<keep-alive :include="keepAliveRoutes">
<component :is="Component" :key="$route.path" />
</keep-alive>
</transition>
</router-view>
</template>
4.2 路由缓存与Keep-Alive
对于频繁访问且不需要重新渲染的页面,通过<keep-alive>
组件进行缓存:
javascript
// 需要缓存的路由组件名列表
const keepAliveRoutes = computed(() => {
return routes.value.map((item) => item.name).filter(Boolean)
})
// 提供给其他组件使用的方法和数据
provide('routeHistory', {
routes,
keepAliveRoutes,
})
这样可以保持页面状态,提高用户体验并减少不必要的重新渲染。
4.3 路由错误处理
javascript
// 处理异步组件加载失败的情况
router.onError((error) => {
const pattern = /Loading chunk (\d)+ failed/g
const isChunkLoadFailed = error.message.match(pattern)
if (isChunkLoadFailed) {
window.location.reload()
}
})
五、最佳实践与性能优化
5.1 路由懒加载
为了减小初始加载的包体积,对所有页面组件使用动态导入实现懒加载:
javascript
// 懒加载方式
component: () => import('@/views/platform/user.vue')
// 而不是
// component: PlatformUser
这样页面组件只会在用户访问对应路由时才被下载,大大减小了首屏加载时间。
5.2 路由预加载
对于极有可能被访问的页面,可以考虑在空闲时间预加载:
javascript
// 当用户进入某个页面时,预加载可能会访问的下一个页面
const prefetchComponent = () => {
// 预加载编辑页面
const editPagePromise = import('@/views/platform/user-edit.vue')
}