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