前言

在第 17 篇中,我们掌握了 Vue Router 4 的基础配置和导航守卫。

但在实际的企业级项目中,仅仅"能用路由"是不够的,你还需要面对三个核心问题:

  1. 首屏加载太慢怎么办?​ → 路由懒加载深度优化

  2. 页面切换后滚动位置不对?​ → 滚动行为控制

  3. 不同角色看到不同菜单?​ → 动态权限路由

这一篇,我们把这三个问题逐一击破,给出可落地的完整方案


一、路由懒加载深度优化

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 中同样支持(使用 rollupOptionsvite-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 中的最佳实践

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐