Skip to content
目录

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参数代替了之前的*写法
}

模块化路由配置:

  1. 结构清晰:将路由按功能模块分类
  2. 易于维护:每个模块可以独立管理
  3. 按需加载:可以根据权限动态加载不同模块的路由

三、权限控制实现

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')
}

Released under the MIT License.