《Vue3 从入门到大神18篇》路由进阶 —— 懒加载、滚动行为与动态权限路由
前言
在第 17 篇中,我们掌握了 Vue Router 4 的基础配置和导航守卫。
但在实际的企业级项目中,仅仅"能用路由"是不够的,你还需要面对三个核心问题:
-
首屏加载太慢怎么办? → 路由懒加载深度优化
-
页面切换后滚动位置不对? → 滚动行为控制
-
不同角色看到不同菜单? → 动态权限路由
这一篇,我们把这三个问题逐一击破,给出可落地的完整方案。
一、路由懒加载深度优化
1️⃣ 为什么需要懒加载?
假设你的项目有 50 个页面,如果不做懒加载:
import Home from '@/views/Home.vue'
import User from '@/views/User.vue'
import Order from '@/views/Order.vue'
// ... 50 个页面全部 import
❌ 后果:
-
首屏加载需要下载所有页面的代码
-
用户可能永远用不到某些页面,但也得下载
-
首屏时间可能从 1s 变成 10s+
2️⃣ 基础懒加载
{
path: '/user',
component: () => import('@/views/User.vue')
}
✅ 访问 /user时才会加载 User.vue对应的 chunk
3️⃣ 魔法注释:自定义 chunk 名称
{
path: '/user',
component: () => import(/* webpackChunkName: "user" */ '@/views/User.vue')
}
📌 Vite 中同样支持(使用 rollupOptions或 vite-plugin-webpackchunkname)
4️⃣ 按业务模块分组(推荐 ✅)
// 用户模块
const UserList = () => import('@/views/user/List.vue')
const UserDetail = () => import('@/views/user/Detail.vue')
// 订单模块
const OrderList = () => import('@/views/order/List.vue')
const OrderDetail = () => import('@/views/order/Detail.vue')
📌 同一模块的页面打包到同一 chunk,减少 HTTP 请求数
5️⃣ 预加载关键路由
// 在空闲时预加载可能访问的页面
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
import('@/views/User.vue')
})
}
✅ 适合"大概率会访问"的页面(如登录后的首页)
6️⃣ 懒加载 + Loading 过渡
<!-- App.vue -->
<template>
<router-view v-slot="{ Component }">
<Suspense>
<template #default>
<component :is="Component" />
</template>
<template #fallback>
<PageSkeleton />
</template>
</Suspense>
</router-view>
</template>
✅ 路由切换时展示骨架屏,体验丝滑
二、滚动行为控制
1️⃣ 基础配置
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition // 浏览器后退/前进
}
return { top: 0 } // 默认回到顶部
}
})
2️⃣ 不同路由不同滚动行为
scrollBehavior(to) {
if (to.hash) {
return {
el: to.hash,
top: 100, // 偏移量(考虑固定头部)
behavior: 'smooth'
}
}
return { top: 0 }
}
📌 锚点跳转 + 平滑滚动
3️⃣ 异步滚动(等待页面渲染完成)
scrollBehavior(to, from, savedPosition) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ top: 0 })
}, 500)
})
}
✅ 适合需要等待数据加载完成的页面
4️⃣ 列表页保持滚动位置(实战)
// 在列表页组件中
import { onActivated } from 'vue'
let scrollTop = 0
onDeactivated(() => {
scrollTop = window.scrollY
})
onActivated(() => {
window.scrollTo(0, scrollTop)
})
✅ 配合 KeepAlive 使用,列表页返回时位置不变
三、动态权限路由(重点)
这是中后台系统的核心架构,我会给出一个完整的可落地方案。
1️⃣ 整体设计思路
登录 → 获取用户信息(含角色/权限) → 根据权限过滤路由表 → 动态添加路由 → 渲染菜单
2️⃣ 定义路由表的两种类型
// 所有路由(不需要权限)
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/Login.vue')
},
{
path: '/403',
component: () => import('@/views/403.vue')
}
]
// 需要权限的路由
export const asyncRoutes = [
{
path: '/dashboard',
component: () => import('@/layouts/DashboardLayout.vue'),
meta: { requiresAuth: true, roles: ['admin', 'editor'] },
children: [
{
path: 'user',
component: () => import('@/views/User.vue'),
meta: { roles: ['admin'] }
},
{
path: 'order',
component: () => import('@/views/Order.vue'),
meta: { roles: ['admin', 'editor'] }
}
]
}
]
3️⃣ 权限过滤函数
// utils/filterRoutes.ts
import { RouteRecordRaw } from 'vue-router'
export function filterRoutesByRoles(
routes: RouteRecordRaw[],
roles: string[]
): RouteRecordRaw[] {
return routes.filter(route => {
// 没有 meta.roles 表示任何人都能访问
if (!route.meta?.roles) return true
// 判断用户角色是否在允许列表中
const hasPermission = route.meta.roles.some(role => roles.includes(role))
if (hasPermission && route.children) {
route.children = filterRoutesByRoles(route.children, roles)
}
return hasPermission
})
}
4️⃣ 在路由守卫中动态添加
// router/index.ts
import { useUserStore } from '@/stores/user'
let routesAdded = false
router.beforeEach(async (to, from) => {
const token = localStorage.getItem('token')
const userStore = useUserStore()
// 未登录,跳转到登录页
if (!token && to.path !== '/login') {
return '/login'
}
// 已登录,访问登录页则跳转到首页
if (token && to.path === '/login') {
return '/dashboard'
}
// 已登录但未添加动态路由
if (token && !routesAdded) {
// 获取用户信息(含角色)
const roles = await userStore.fetchUserInfo()
// 根据角色过滤路由
const accessibleRoutes = filterRoutesByRoles(asyncRoutes, roles)
// 动态添加路由
accessibleRoutes.forEach(route => {
router.addRoute(route)
})
// 添加 404(必须在最后)
router.addRoute({
path: '/:pathMatch(.*)*',
component: () => import('@/views/404.vue')
})
routesAdded = true
// 重新触发导航(重要!)
return { ...to, replace: true }
}
})
📌 return { ...to, replace: true }是关键:
动态添加路由后,需要重新触发一次导航才能匹配到新路由。
5️⃣ 菜单渲染
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const routes = router.getRoutes()
// 过滤出需要渲染的菜单项
const menuRoutes = routes.filter(r => r.meta?.title)
</script>
<template>
<aside class="sidebar">
<nav v-for="route in menuRoutes" :key="route.path">
<router-link :to="route.path">
{{ route.meta.title }}
</router-link>
</nav>
</aside>
</template>
6️⃣ 退出登录时重置路由
import { createRouter } from 'vue-router'
const router = createRouter({ ... })
export function resetRouter() {
const newRouter = createRouter({ ... })
;(router as any).matcher = (newRouter as any).matcher
}
📌 直接替换 matcher 是最干净的路由重置方式
四、完整权限流程图
┌─────────────┐
│ 访问页面 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 有 Token? │── 否 ──→ 跳转到 /login
└──────┬──────┘
│ 是
▼
┌─────────────┐
│ 路由已添加? │── 是 ──→ 正常放行
└──────┬──────┘
│ 否
▼
┌─────────────┐
│ 获取用户信息 │
└──────┬──────┘
▼
┌─────────────┐
│ 过滤权限路由 │
└──────┬──────┘
▼
┌─────────────┐
│ 动态添加路由 │
└──────┬──────┘
▼
┌─────────────┐
│ 重新触发导航 │
└─────────────┘
五、常见坑位与解决方案
❌ 坑 1:刷新页面后动态路由丢失
原因:动态添加的路由不在代码中持久化
解决:在 beforeEach中判断,每次刷新都重新添加
❌ 坑 2:404 路由匹配不到动态路由
原因:404 路由如果在静态路由中,会优先匹配
解决:404 路由必须在动态路由添加之后注册
❌ 坑 3:addRoute 后路由不生效
原因:添加路由后没有重新触发导航
解决:return { ...to, replace: true }
六、面试高频问答
Q1:动态路由和静态路由的区别?
静态路由在创建 router 时就注册,动态路由在运行时通过
addRoute添加。
Q2:为什么 404 路由要放在最后?
因为路由匹配是按注册顺序的,放在前面会拦截所有未匹配的路由。
Q3:动态路由刷新后怎么保持?
在路由守卫中重新获取用户信息并再次
addRoute。
七、总结(架构级)
-
路由懒加载是性能优化的第一步
-
滚动行为控制是用户体验的细节
-
动态权限路由是中后台系统的标配
-
三者结合才能支撑企业级应用
📢 下期预告
👉 第 19 篇:HTTP 请求封装 —— Axios 在 Vue3 中的最佳实践
更多推荐



所有评论(0)