场景引入

大部分后台管理系统都有这类需求:

  1. 未登录用户跳转任何页面都强制回登录页;
  2. 不同角色看到的菜单不同、能进的页面不同;
  3. 路由按需加载,首屏不打包全部页面。

这些需求都靠 Vue Router 的守卫和动态路由解决。

一、路由元信息:给路由打标签

meta 是挂在路由上的自定义数据,权限、标题、缓存标记都放这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const routes = [
{
path: '/admin',
component: () => import('@/views/Admin.vue'),
meta: {
requiresAuth: true, // 需要登录
roles: ['admin'], // 允许的角色
title: '管理后台'
}
},
{
path: '/login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录' }
}
]

二、全局守卫:登录拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import router from './router'
import { useUserStore } from '@/stores/user'

// 每次路由跳转前执行
router.beforeEach((to, from, next) => {
// 动态设置页面标题
document.title = to.meta.title ? `${to.meta.title} - 我的后台` : '我的后台'

const userStore = useUserStore()

// 需要登录但没登录 → 去登录页,并记录来源
if (to.meta.requiresAuth && !userStore.token) {
next({ path: '/login', query: { redirect: to.fullPath } })
return
}

// 角色校验
if (to.meta.roles && !to.meta.roles.includes(userStore.role)) {
next('/403') // 无权限页
return
}

next()
})

守卫的三参数:

  • to:目标路由(即将进入)
  • from:来源路由
  • next():放行;next(false) 取消;next('/xx') 重定向

三、登录后跳回原页面

登录页拿到 redirect 参数,登录成功后回跳:

1
2
3
4
5
6
7
8
const router = useRouter()
const route = useRoute()

async function handleLogin() {
await login()
// 有来源就回去,没有就去首页
router.push(route.query.redirect || '/')
}

四、动态路由:按角色添加菜单

静态路由只放公共页面,业务页面登录后根据权限动态添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 后端按角色返回菜单配置
const menus = [
{ path: '/user', name: 'User', component: 'user/UserList' },
{ path: '/order', name: 'Order', component: 'order/OrderList' }
]

// 把组件名字映射到实际组件
const modules = import.meta.glob('@/views/**/*.vue')

function buildRoutes(menus) {
return menus.map((menu) => ({
path: menu.path,
name: menu.name,
// 通过映射动态加载组件
component: modules[`/src/views/${menu.component}.vue`]
}))
}

// 登录成功后动态添加路由
router.addRoute({
path: '/',
component: () => import('@/layout/MainLayout.vue'),
children: buildRoutes(menus)
})

重点坑:addRoute 添加的路由刷新页面就没了(内存态),所以刷新后要在入口重新拉菜单、重新 addRoute,否则 404。常见方案是在 beforeEach 里加”是否已初始化”判断:

1
2
3
4
5
6
7
8
9
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
if (userStore.token && !userStore.menusLoaded) {
await userStore.fetchAndRegisterRoutes() // 拉菜单 + addRoute
next({ ...to, replace: true }) // 重新进入目标路由
return
}
next()
})

五、路由懒加载与打包优化

Vue Router 4 默认推荐路由级懒加载,页面按需打包:

1
2
3
4
5
// 基础写法:单独 chunk
component: () => import('@/views/Home.vue')

// 把相关页面合成一个 chunk(webpackChunkName 注释)
component: () => import(/* webpackChunkName: "user" */ '@/views/user/UserList.vue')

Vite 里用 build.rollupOptions.output.manualChunks 控制分包,路由级懒加载天然就会产生多个 chunk。

六、组件内守卫与路由滚动

组件内守卫适合”离开前确认”:

1
2
3
4
5
6
7
8
9
10
11
12
// 在组件里
onBeforeRouteLeave((to, from, next) => {
if (hasUnsavedChanges.value) {
if (confirm('有未保存的修改,确定离开吗?')) {
next()
} else {
next(false)
}
} else {
next()
}
})

路由切换保持滚动位置:

1
2
3
4
5
6
7
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
return savedPosition || { top: 0 }
}
})

常见坑

  1. 守卫里访问 store 时机:beforeEach 里用 Pinia 要在 app.use(pinia) 之后,否则拿不到实例;
  2. addRoute 重复添加报错:同名路由重复 add 会警告,初始化前先 router.hasRoute(name) 判断;
  3. 动态路由刷新 404:几乎所有人都会踩,务必处理”菜单初始化”逻辑;
  4. 守卫死循环:next('/login') 但 login 页面也满足拦截条件时,会无限重定向,拦截逻辑要排除登录页。

总结

Vue Router 4 的进阶能力就三块:meta 元信息打标签 → 全局守卫做登录/权限拦截 → 动态 addRoute 按角色挂路由。配合路由懒加载,后台管理系统的权限骨架就完整了。