Hi,我是前端人类学 !
在 Vue 单页应用开发中,路由管理往往从"配置几个页面"演变为"构建一套复杂的访问控制系统"。随着应用规模的增长,权限控制、模块动态加载、性能优化和状态保持等问题会接踵而至。
本文将从路由守卫权限控制 、动态路由 、懒加载 和路由缓存四个维度,深入探讨 Vue Router 的进阶实践。
文章目录
-
- 一、路由守卫:构建应用的权限防火墙
-
- [1.1 全局守卫](#1.1 全局守卫)
- [1.2 路由独享守卫](#1.2 路由独享守卫)
- [1.3 组件内守卫](#1.3 组件内守卫)
- 二、动态路由:运行时注册路由
-
- [2.1 路由初始化与动态添加](#2.1 路由初始化与动态添加)
- [2.2 路由删除与重置](#2.2 路由删除与重置)
- [2.3 路由权限的精细化控制](#2.3 路由权限的精细化控制)
- 三、懒加载:路由级代码分割
-
- [3.1 基本用法](#3.1 基本用法)
- [3.2 命名 chunk](#3.2 命名 chunk)
- [3.3 预加载与预获取](#3.3 预加载与预获取)
- [3.4 加载状态与错误处理](#3.4 加载状态与错误处理)
- [四、路由缓存:KeepAlive 与 RouterView](#四、路由缓存:KeepAlive 与 RouterView)
-
- [4.1 基本缓存](#4.1 基本缓存)
- [4.2 精细控制缓存](#4.2 精细控制缓存)
- [4.3 基于路由动态决定缓存](#4.3 基于路由动态决定缓存)
- [4.4 缓存下的生命周期](#4.4 缓存下的生命周期)
- [4.5 标签页缓存方案](#4.5 标签页缓存方案)
- 五、综合实践:完整权限路由方案
一、路由守卫:构建应用的权限防火墙
路由守卫(Navigation Guards)是 Vue Router 提供的一套钩子函数,用于在路由跳转前后执行逻辑。它是实现权限控制的核心机制。
1.1 全局守卫
全局前置守卫 router.beforeEach 是最常用的权限控制入口,每次路由跳转前都会触发:
javascript
import router from './router'
router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem('token')
const userStore = useUserStore()
// 白名单:登录页、注册页等无需认证
const whiteList = ['/login', '/register']
if (token) {
if (to.path === '/login') {
// 已登录,跳转到首页
next('/')
} else {
// 获取用户信息(仅首次)
if (!userStore.userInfo) {
await userStore.fetchUserInfo()
}
next()
}
} else {
if (whiteList.includes(to.path)) {
next()
} else {
// 未登录,跳转登录页并携带回跳地址
next(`/login?redirect=${to.fullPath}`)
}
}
})
全局解析守卫 router.beforeResolve 在导航确认前、组件渲染前触发,适用于在渲染前确保数据就绪。
全局后置钩子 router.afterEach 不接收 next 函数,适合日志记录、页面埋点等操作:
javascript
router.afterEach((to, from) => {
// 页面访问统计
trackPageView(to.fullPath)
// 更新页面标题
document.title = to.meta.title || '默认标题'
})
1.2 路由独享守卫
在路由配置中直接定义 beforeEnter 守卫,仅对该路由生效:
javascript
const routes = [
{
path: '/admin',
component: () => import('@/views/Admin.vue'),
beforeEnter: (to, from, next) => {
const role = localStorage.getItem('role')
if (role === 'admin') {
next()
} else {
next('/403')
}
}
}
]
1.3 组件内守卫
在组件内部使用 onBeforeRouteLeave 和 onBeforeRouteUpdate 守卫:
javascript
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// 路由更新时(如 /posts/1 -> /posts/2)
onBeforeRouteUpdate((to, from, next) => {
// 重新加载数据
fetchPost(to.params.id)
next()
})
// 离开当前页面时
onBeforeRouteLeave((to, from, next) => {
if (formDataIsDirty.value) {
if (confirm('有未保存的内容,确定离开吗?')) {
next()
} else {
next(false)
}
} else {
next()
}
})
</script>
二、动态路由:运行时注册路由
动态路由是指应用在运行时根据用户权限、业务需求动态添加路由,而非在初始化时全部注册。这在 RBAC(基于角色的访问控制)系统中尤为重要。
2.1 路由初始化与动态添加
典型的动态路由流程:
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 基础路由:登录页、404、白名单页面
const constantRoutes = [
{ path: '/login', component: () => import('@/views/Login.vue') },
{ path: '/404', component: () => import('@/views/404.vue') }
]
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes
})
export default router
在全局守卫中动态添加权限路由:
javascript
// 异步路由配置(按角色划分)
const asyncRouteMap = {
admin: [
{ path: '/dashboard', component: () => import('@/views/Dashboard.vue') },
{ path: '/user-manage', component: () => import('@/views/UserManage.vue') }
],
editor: [
{ path: '/dashboard', component: () => import('@/views/Dashboard.vue') },
{ path: '/content', component: () => import('@/views/Content.vue') }
],
guest: [
{ path: '/dashboard', component: () => import('@/views/Dashboard.vue') }
]
}
router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem('token')
if (token) {
if (!router.hasRoute('dashboard')) {
// 获取用户角色
const role = await getUserRole()
const asyncRoutes = asyncRouteMap[role] || []
// 动态添加路由
asyncRoutes.forEach(route => {
router.addRoute(route)
})
// 添加 404 兜底(需放在最后)
router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' })
// 重新导航,确保动态路由生效
return next({ ...to, replace: true })
}
next()
} else {
// 未登录处理...
}
})
2.2 路由删除与重置
在某些场景下(如退出登录),需要重置路由:
javascript
// 重置路由的方法
export function resetRouter() {
// 获取所有动态添加的路由
const routes = router.getRoutes()
routes.forEach(route => {
// 保留基础路由(根据 name 过滤)
if (route.name !== 'Login' && route.name !== '404') {
router.removeRoute(route.name)
}
})
}
// 退出登录
function logout() {
resetRouter()
localStorage.clear()
router.push('/login')
}
2.3 路由权限的精细化控制
除了控制路由是否存在,还可以通过路由元信息 meta 配合守卫实现更细粒度的权限控制:
javascript
const routes = [
{
path: '/system',
component: () => import('@/views/System.vue'),
meta: {
roles: ['admin', 'super_admin'],
permissions: ['system:read', 'system:write']
}
}
]
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
// 检查角色
if (to.meta.roles) {
const hasRole = to.meta.roles.some(role => userStore.roles.includes(role))
if (!hasRole) return next('/403')
}
// 检查权限
if (to.meta.permissions) {
const hasPermission = to.meta.permissions.some(
perm => userStore.permissions.includes(perm)
)
if (!hasPermission) return next('/403')
}
next()
})
三、懒加载:路由级代码分割
在大型 SPA 中,首屏加载效率直接影响用户体验。路由懒加载通过代码分割将不同路由的代码拆分为独立的 chunk,只在访问对应路由时才加载。
3.1 基本用法
javascript
const routes = [
{
path: '/dashboard',
// 使用动态 import
component: () => import('@/views/Dashboard.vue')
},
{
path: '/settings',
component: () => import('@/views/Settings.vue')
}
]
3.2 命名 chunk
通过 Webpack/Vite 的魔法注释指定 chunk 名称,便于管理和缓存:
javascript
component: () => import(
/* webpackChunkName: "dashboard" */
'@/views/Dashboard.vue'
)
3.3 预加载与预获取
结合路由元信息,实现智能预加载:
javascript
// 使用 webpackPrefetch 预获取(低优先级)
component: () => import(
/* webpackChunkName: "dashboard" */
/* webpackPrefetch: true */
'@/views/Dashboard.vue'
)
// 使用 webpackPreload 预加载(高优先级,适用于首屏关键路由)
component: () => import(
/* webpackChunkName: "home" */
/* webpackPreload: true */
'@/views/Home.vue'
)
3.4 加载状态与错误处理
vue
<template>
<RouterView>
<template #default="{ Component, route }">
<Suspense>
<template #default>
<component :is="Component" />
</template>
<template #fallback>
<div class="loading-spinner">加载中...</div>
</template>
</Suspense>
</template>
</RouterView>
</template>
结合 Vue 的 <Suspense> 组件,可以优雅地处理异步组件的加载状态。
四、路由缓存:KeepAlive 与 RouterView
在多标签页或频繁切换的场景中,希望保留页面状态(如列表滚动位置、表单输入内容),避免重复渲染和接口请求。
4.1 基本缓存
vue
<template>
<RouterView v-slot="{ Component }">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</RouterView>
</template>
4.2 精细控制缓存
通过 include 和 exclude 属性控制哪些路由组件需要缓存:
vue
<template>
<RouterView v-slot="{ Component }">
<KeepAlive :include="['Dashboard', 'UserList']" :max="5">
<component :is="Component" />
</KeepAlive>
</RouterView>
</template>
4.3 基于路由动态决定缓存
利用路由元信息动态决定是否缓存:
vue
<template>
<RouterView v-slot="{ Component, route }">
<KeepAlive :include="cachedViews">
<component :is="Component" :key="route.fullPath" />
</KeepAlive>
</RouterView>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const cachedViews = ref([])
const route = useRoute()
// 根据路由 meta 动态添加缓存
watch(() => route.meta.keepAlive, (newVal) => {
if (newVal && !cachedViews.value.includes(route.name)) {
cachedViews.value.push(route.name)
}
}, { immediate: true })
</script>
4.4 缓存下的生命周期
被 <KeepAlive> 缓存的组件会经历 activated 和 deactivated 生命周期钩子:
javascript
<script setup>
import { onActivated, onDeactivated } from 'vue'
// 组件从缓存中激活时执行
onActivated(() => {
// 刷新数据
fetchData()
// 恢复滚动位置
restoreScrollPosition()
})
// 组件被缓存、进入非活跃状态时执行
onDeactivated(() => {
// 保存当前状态
saveCurrentState()
})
</script>
4.5 标签页缓存方案
实现类似浏览器的多标签页缓存,需要结合 KeepAlive、RouterView 和标签页管理:
html
<template>
<div class="tabs-layout">
<!-- 标签页栏 -->
<div class="tabs-bar">
<div
v-for="tab in tabs"
:key="tab.path"
class="tab-item"
:class="{ active: tab.path === currentPath }"
@click="switchTab(tab)"
>
{{ tab.title }}
<span class="close-btn" @click.stop="closeTab(tab)">×</span>
</div>
</div>
<!-- 路由视图 + 缓存 -->
<RouterView v-slot="{ Component, route }">
<KeepAlive :include="cachedTabNames">
<component :is="Component" :key="route.path" />
</KeepAlive>
</RouterView>
</div>
</template>
五、综合实践:完整权限路由方案
将上述四部分整合,形成一个完整的权限路由管理方案:
javascript
// stores/permission.js
import { defineStore } from 'pinia'
export const usePermissionStore = defineStore('permission', {
state: () => ({
routes: [],
permissions: []
}),
actions: {
generateRoutes(role) {
// 根据角色获取动态路由
const asyncRoutes = getAsyncRoutesByRole(role)
// 存储到 store
this.routes = asyncRoutes
return asyncRoutes
}
}
})
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { usePermissionStore } from '@/stores/permission'
const constantRoutes = [
{ path: '/login', name: 'Login', component: () => import('@/views/Login.vue') },
{ path: '/403', name: '403', component: () => import('@/views/403.vue') }
]
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes
})
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem('token')
const permissionStore = usePermissionStore()
if (token) {
if (to.path === '/login') {
next('/')
return
}
// 检查是否已加载权限路由
if (!permissionStore.routes.length) {
try {
const userInfo = await getUserInfo()
const routes = permissionStore.generateRoutes(userInfo.role)
routes.forEach(route => router.addRoute(route))
router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' })
// 确保动态路由加载完成后再跳转
return next({ ...to, replace: true })
} catch (error) {
// 权限获取失败,跳转登录
localStorage.removeItem('token')
next(`/login?redirect=${to.fullPath}`)
}
} else {
next()
}
} else {
if (to.meta.requiresAuth !== false) {
next(`/login?redirect=${to.fullPath}`)
} else {
next()
}
}
})
export default router
路由管理是 Vue 应用架构的核心环节之一。合理的权限控制保证系统安全,动态路由提供灵活的扩展能力,懒加载优化性能,缓存提升用户体验。在实际项目中,这四者往往需要协同配合,共同构建一个健壮、高效的路由系统。