66. 路由元信息与权限控制
📖 概述
路由元信息是Vue Router中用于存储路由附加信息的机制,而权限控制则是根据用户权限决定是否允许访问特定路由的功能。结合路由元信息和导航守卫,我们可以实现灵活、高效的权限控制方案。本集将深入讲解Vue Router 4.x中路由元信息的定义、扩展、使用,以及如何基于路由元信息实现权限控制,帮助你构建更安全、更灵活的单页应用。
✨ 核心知识点
1. 路由元信息基础
什么是路由元信息
- 路由元信息是在路由配置中使用
meta字段存储的附加信息 - 可以包含任意自定义数据,如标题、图标、权限要求等
- 元信息会被传递到路由对象中,可以在组件和导航守卫中访问
- 适用于权限控制、菜单生成、页面标题设置等场景
配置路由元信息
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue'),
meta: {
title: '首页', // 页面标题
icon: 'home', // 菜单图标
requiresAuth: false, // 是否需要认证
showInMenu: true // 是否在菜单中显示
}
},
{
path: '/dashboard',
name: 'dashboard',
component: () => import('../views/DashboardView.vue'),
meta: {
title: '仪表盘',
icon: 'dashboard',
requiresAuth: true,
showInMenu: true,
permissions: ['dashboard:view'] // 权限要求
}
},
{
path: '/admin',
name: 'admin',
component: () => import('../views/AdminLayout.vue'),
meta: {
title: '管理中心',
icon: 'settings',
requiresAuth: true,
showInMenu: true,
permissions: ['admin:view']
},
children: [
{
path: 'users',
name: 'admin-users',
component: () => import('../views/AdminUsersView.vue'),
meta: {
title: '用户管理',
icon: 'users',
requiresAuth: true,
showInMenu: true,
permissions: ['user:manage']
}
},
{
path: 'roles',
name: 'admin-roles',
component: () => import('../views/AdminRolesView.vue'),
meta: {
title: '角色管理',
icon: 'lock',
requiresAuth: true,
showInMenu: true,
permissions: ['role:manage']
}
}
]
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
export default router2. 扩展路由元信息类型
使用TypeScript扩展路由元信息
为了获得更好的TypeScript类型支持,我们可以扩展Vue Router的RouteMeta接口。
// src/router/types.ts
import 'vue-router'
// 扩展路由元信息类型
declare module 'vue-router' {
interface RouteMeta {
// 页面标题
title?: string
// 菜单图标
icon?: string
// 是否需要认证
requiresAuth?: boolean
// 是否在菜单中显示
showInMenu?: boolean
// 权限要求
permissions?: string[]
// 页面缓存
keepAlive?: boolean
// 面包屑导航
breadcrumb?: boolean
// 页面过渡动画
transition?: string
}
}3. 访问路由元信息
在组件中访问元信息
1. 使用Composition API
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
// 访问路由元信息
const pageTitle = route.meta.title
const requiresAuth = route.meta.requiresAuth
const permissions = route.meta.permissions
</script>2. 使用Options API
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
computed: {
pageTitle() {
return this.$route.meta.title
},
requiresAuth() {
return this.$route.meta.requiresAuth
}
}
})
</script>在导航守卫中访问元信息
// src/router/index.ts
router.beforeEach((to, from, next) => {
// 访问路由元信息
const requiresAuth = to.meta.requiresAuth || false
const permissions = to.meta.permissions || []
// 权限控制逻辑
if (requiresAuth && !isAuthenticated()) {
next({ name: 'login' })
return
}
if (permissions.length > 0 && !hasPermissions(permissions)) {
next({ name: '403' })
return
}
next()
})4. 基于路由元信息的权限控制
1. 实现权限控制逻辑
// src/utils/permission.ts
// 模拟用户权限
const userPermissions: string[] = ['dashboard:view', 'user:manage']
// 检查用户是否已认证
export function isAuthenticated(): boolean {
return !!localStorage.getItem('token')
}
// 检查用户是否有权限
export function hasPermission(permission: string): boolean {
return userPermissions.includes(permission)
}
// 检查用户是否有多个权限
export function hasPermissions(permissions: string[]): boolean {
return permissions.every(permission => hasPermission(permission))
}
// 检查用户是否有任一权限
export function hasAnyPermission(permissions: string[]): boolean {
return permissions.some(permission => hasPermission(permission))
}2. 在导航守卫中实现权限控制
// src/router/index.ts
import { isAuthenticated, hasPermissions } from '../utils/permission'
router.beforeEach((to, from, next) => {
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - Vue Admin`
}
// 权限控制
const requiresAuth = to.meta.requiresAuth || false
const permissions = to.meta.permissions || []
// 检查是否需要认证
if (requiresAuth && !isAuthenticated()) {
next({
name: 'login',
query: { redirect: to.fullPath } // 保存重定向路径
})
return
}
// 检查权限
if (permissions.length > 0 && !hasPermissions(permissions)) {
next({ name: '403' })
return
}
next()
})3. 动态生成菜单
// src/utils/menu.ts
import { RouteRecordRaw } from 'vue-router'
import { hasPermissions } from './permission'
// 根据路由配置和用户权限生成菜单
export function generateMenu(routes: Array<RouteRecordRaw>): any[] {
const menu: any[] = []
routes.forEach(route => {
// 跳过不在菜单中显示的路由
if (!route.meta?.showInMenu) {
return
}
// 检查权限
if (route.meta?.permissions && !hasPermissions(route.meta.permissions)) {
return
}
const menuItem: any = {
path: route.path,
name: route.name,
title: route.meta?.title,
icon: route.meta?.icon,
children: []
}
// 处理子路由
if (route.children && route.children.length > 0) {
menuItem.children = generateMenu(route.children)
// 如果没有子菜单,跳过父菜单
if (menuItem.children.length === 0) {
return
}
}
menu.push(menuItem)
})
return menu
}5. 路由元信息的高级应用
1. 页面缓存控制
// 配置路由元信息控制页面缓存
const routes: Array<RouteRecordRaw> = [
{
path: '/dashboard',
name: 'dashboard',
component: () => import('../views/DashboardView.vue'),
meta: {
keepAlive: true // 启用页面缓存
}
},
{
path: '/user/:id',
name: 'user',
component: () => import('../views/UserView.vue'),
meta: {
keepAlive: false // 禁用页面缓存
}
}
]<!-- App.vue -->
<template>
<div>
<!-- 根据meta.keepAlive控制是否缓存页面 -->
<router-view v-slot="{ Component }">
<keep-alive>
<component
:is="Component"
v-if="$route.meta.keepAlive"
/>
</keep-alive>
<component
:is="Component"
v-if="!$route.meta.keepAlive"
/>
</router-view>
</div>
</template>2. 页面过渡动画
// 配置路由元信息控制页面过渡动画
const routes: Array<RouteRecordRaw> = [
{
path: '/home',
name: 'home',
component: () => import('../views/HomeView.vue'),
meta: {
transition: 'fade' // 淡入淡出动画
}
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
meta: {
transition: 'slide' // 滑动动画
}
}
]<!-- App.vue -->
<template>
<div>
<!-- 根据meta.transition设置过渡动画 -->
<router-view v-slot="{ Component }">
<transition :name="$route.meta.transition || 'default'">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>
<style>
/* 淡入淡出动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 滑动动画 */
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease;
}
.slide-enter-from {
transform: translateX(100%);
}
.slide-leave-to {
transform: translateX(-100%);
}
/* 默认动画 */
.default-enter-active,
.default-leave-active {
transition: all 0.3s ease;
}
.default-enter-from,
.default-leave-to {
opacity: 0;
transform: translateY(20px);
}
</style>3. 面包屑导航生成
// src/utils/breadcrumb.ts
import { RouteRecordRaw } from 'vue-router'
// 根据当前路由生成面包屑
export function generateBreadcrumb(routes: Array<RouteRecordRaw>, currentPath: string): any[] {
const breadcrumb: any[] = []
const pathSegments = currentPath.split('/').filter(segment => segment)
let currentPathSegment = ''
pathSegments.forEach(segment => {
currentPathSegment += `/${segment}`
// 查找对应的路由
const route = findRouteByPath(routes, currentPathSegment)
if (route && route.meta?.breadcrumb !== false) {
breadcrumb.push({
path: currentPathSegment,
name: route.name,
title: route.meta?.title || segment
})
}
})
return breadcrumb
}
// 递归查找路由
function findRouteByPath(routes: Array<RouteRecordRaw>, path: string): RouteRecordRaw | undefined {
for (const route of routes) {
if (route.path === path) {
return route
}
if (route.children) {
const found = findRouteByPath(route.children, path)
if (found) {
return found
}
}
}
return undefined
}6. 动态权限控制
1. 动态添加路由
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { hasPermissions } from '../utils/permission'
// 基础路由
const baseRoutes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue')
}
]
// 需要权限的路由
const protectedRoutes: Array<RouteRecordRaw> = [
{
path: '/admin',
name: 'admin',
component: () => import('../views/AdminLayout.vue'),
meta: {
requiresAuth: true,
permissions: ['admin:view']
},
children: [
// 子路由...
]
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: baseRoutes
})
// 动态添加权限路由
export function addProtectedRoutes() {
protectedRoutes.forEach(route => {
// 检查路由权限
if (!route.meta?.permissions || hasPermissions(route.meta.permissions)) {
router.addRoute(route)
// 递归添加子路由
if (route.children) {
addChildRoutes(route.name as string, route.children)
}
}
})
}
// 递归添加子路由
function addChildRoutes(parentName: string, children: Array<RouteRecordRaw>) {
children.forEach(child => {
// 检查子路由权限
if (!child.meta?.permissions || hasPermissions(child.meta.permissions)) {
router.addRoute(parentName, child)
// 递归添加嵌套子路由
if (child.children) {
addChildRoutes(child.name as string, child.children)
}
}
})
}
export default router2. 在登录后添加路由
<!-- LoginView.vue -->
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { addProtectedRoutes } from '../router'
const router = useRouter()
async function handleLogin() {
// 登录逻辑
const token = await login()
localStorage.setItem('token', token)
// 添加权限路由
addProtectedRoutes()
// 重定向到首页或之前的页面
const redirect = router.currentRoute.value.query.redirect as string || '/'
router.push(redirect)
}
</script>📝 最佳实践
合理设计元信息结构
- 根据项目需求设计元信息字段
- 使用TypeScript扩展元信息类型,提高类型安全性
权限控制最佳实践
- 使用导航守卫实现集中式权限控制
- 结合路由元信息和用户权限进行检查
- 实现动态路由加载,只添加用户有权限访问的路由
菜单生成最佳实践
- 根据路由元信息动态生成菜单
- 考虑菜单的嵌套结构
- 支持菜单图标、标题、权限等配置
页面标题设置
- 在导航守卫中统一设置页面标题
- 支持国际化的页面标题
页面缓存策略
- 根据页面特性决定是否缓存
- 避免过度缓存导致的性能问题
页面过渡动画
- 根据页面内容选择合适的过渡动画
- 保持动画的一致性和流畅性
💡 常见问题与解决方案
路由元信息不生效
- 检查路由配置中是否正确添加了
meta字段 - 确保TypeScript类型扩展已正确配置
- 检查是否在组件或导航守卫中正确访问元信息
- 检查路由配置中是否正确添加了
权限控制不生效
- 检查导航守卫中的权限检查逻辑
- 确保用户权限已正确获取和存储
- 检查路由元信息中的权限配置
动态路由添加失败
- 确保在登录后调用添加路由的函数
- 检查路由名称是否唯一
- 确保父路由已正确添加
菜单生成错误
- 检查菜单生成函数的逻辑
- 确保路由配置的嵌套结构正确
- 检查权限检查逻辑
📚 进一步学习资源
🎯 课后练习
基础练习
- 配置路由元信息
- 扩展路由元信息类型
- 在组件和导航守卫中访问元信息
进阶练习
- 基于路由元信息实现权限控制
- 动态生成菜单
- 实现页面缓存控制
实战练习
- 构建一个完整的权限控制系统
- 实现动态路由加载
- 生成带有权限控制的菜单
性能优化练习
- 优化动态路由添加逻辑
- 优化菜单生成性能
- 优化页面缓存策略
通过本集的学习,你已经掌握了Vue Router 4.x中路由元信息的定义、扩展、使用,以及基于路由元信息的权限控制、菜单生成、页面缓存控制等高级应用。在实际项目中,合理运用路由元信息,能够构建出更安全、更灵活、更易于维护的单页应用。下一集我们将深入学习路由守卫深度解析,进一步提升Vue 3路由系统的使用能力。