任何多页面 SPA 都绕不开 Vue Router。但很多项目对它的使用停留在"能跳转"——路由懒加载没配、守卫写得一团乱麻、动态路由权限方案稀里糊涂。本文以 Vue Router 4(对应 Vue 3)为准,把这几个中级必考点一次讲透。
基础配置与懒加载
路由懒加载是必选项
打包工具默认会把所有页面组件打进一个 bundle,首屏加载动辄几 MB。动态 import() 让每个页面按需加载:
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('@/views/Home.vue'),
children: [
{
path: 'orders',
component: () => import('@/views/order/List.vue')
},
{
path: 'orders/:id',
component: () => import('@/views/order/Detail.vue'),
props: true // 把路由参数作为 props 传入组件
}
]
}
]
})配合 Vite 的手动分包,可以进一步把公共依赖抽出来:
// vite.config.ts
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-vue': ['vue', 'vue-router', 'pinia'],
'vendor-ui': ['element-plus']
}
}
}
}
}props 解耦:别在组件里读 $route
<!-- ❌ 与路由强耦合,组件无法复用 -->
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
const id = route.params.id
</script>
<!-- ✅ props: true 后,组件像普通组件一样接收参数 -->
<script setup>
defineProps({ id: String })
</script>函数模式更灵活:
{
path: 'orders/:id',
component: OrderDetail,
props: route => ({ id: Number(route.params.id), tab: route.query.tab })
}动态路由与权限系统
后台管理系统的经典需求:不同角色看到不同菜单。标准方案是前置白名单 + 动态添加路由:
第一步:定义静态与动态路由
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
// 静态路由:任何人都能访问
export const constantRoutes = [
{ path: '/login', component: () => import('@/views/Login.vue') },
{ path: '/404', component: () => import('@/views/404.vue') }
]
// 动态路由:按角色分配,meta.roles 记录可访问角色
export const asyncRoutes = [
{
path: '/',
component: () => import('@/views/Layout.vue'),
children: [
{
path: 'dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '工作台', roles: ['admin', 'operator'] }
},
{
path: 'system',
component: () => import('@/views/system/Index.vue'),
meta: { title: '系统管理', roles: ['admin'] }
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes
})第二步:登录后过滤并挂载
// stores/permission.ts
import { defineStore } from 'pinia'
import { asyncRoutes, constantRoutes } from '@/router'
function hasPermission(roles, route) {
return route.meta?.roles
? roles.some(role => route.meta.roles.includes(role))
: true // 没声明 roles 默认放行
}
export function filterRoutes(routes, roles) {
return routes.reduce((acc, route) => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterRoutes(tmp.children, roles)
}
acc.push(tmp)
}
return acc
}, [])
}
export const usePermissionStore = defineStore('permission', {
state: () => ({
accessibleRoutes: []
}),
actions: {
generateRoutes(roles) {
this.accessibleRoutes = [...constantRoutes, ...filterRoutes(asyncRoutes, roles)]
return this.accessibleRoutes
}
}
})第三步:addRoute 动态注册
// 登录成功后
const permissionStore = usePermissionStore()
const routes = permissionStore.generateRoutes(user.roles)
routes.forEach(route => router.addRoute(route))注意:addRoute 后必须用 router.replace 或返回新 location 触发一次重新导航,否则匹配不到刚添加的路由。
导航守卫体系
Vue Router 4 的守卫分三类,执行顺序必须烂熟于心:
导航触发
→ beforeEach(全局前置)
→ beforeEnter(路由独享)
→ beforeRouteUpdate / beforeRouteEnter(组件内)
→ afterEach(全局后置)全局前置守卫:登录鉴权标准模板
const WHITE_LIST = ['/login', '/404']
router.beforeEach(async (to, from) => {
const userStore = useUserStore()
// 1. 白名单直接放行
if (WHITE_LIST.includes(to.path)) return true
// 2. 未登录跳登录页,带上 redirect 参数
if (!userStore.token) {
return { path: '/login', query: { redirect: to.fullPath } }
}
// 3. 已登录但还没拉取用户信息/动态路由
if (!userStore.userInfo) {
try {
await userStore.fetchUserInfo()
const permissionStore = usePermissionStore()
const routes = permissionStore.generateRoutes(userStore.roles)
routes.forEach(r => router.addRoute(r))
// 关键:addRoute 后重新进入当前路由
return { ...to, replace: true }
} catch {
userStore.logout()
return { path: '/login', query: { redirect: to.fullPath } }
}
}
// 4. 正常放行
return true
})守卫返回值语义
Vue Router 4 的守卫返回值规则:
| 返回值 | 效果 |
|---|---|
undefined / true | 放行 |
false | 取消导航 |
| 路由地址对象 / 字符串 | 重定向 |
| Promise | resolve 上述值,reject 则取消并报错 |
afterEach:动态修改页面标题
router.afterEach((to) => {
document.title = to.meta?.title
? `${to.meta.title} - 管理系统`
: '管理系统'
})组件内守卫:离开确认
表单页防止用户误关,用 onBeforeRouteLeave:
<script setup>
import { ref } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
const dirty = ref(false)
onBeforeRouteLeave(() => {
if (dirty.value) {
return window.confirm('表单未保存,确定离开?')
}
})
</script>滚动行为与过渡动画
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition // 前进后退恢复位置
if (to.hash) return { el: to.hash, behavior: 'smooth' } // 锚点
return { top: 0 } // 默认回到顶部
}
})路由切换配合 <transition> 做页面动画:
<template>
<router-view v-slot="{ Component, route }">
<transition name="fade" mode="out-in">
<component :is="Component" :key="route.path" />
</transition>
</router-view>
</template>
<style>
.fade-enter-active,
.fade-leave-active { transition: opacity 0.2s ease; }
.fade-enter-from,
.fade-leave-to { opacity: 0; }
</style>useRoute 与 useRouter
<script setup> 中通过组合式 API 获取路由:
import { useRoute, useRouter } from 'vue-router'
const route = useRoute() // 当前路由信息(响应式):params、query、meta
const router = useRouter() // 路由实例:push、replace、go
// 带参数跳转
router.push({ name: 'order-detail', params: { id: 123 }, query: { tab: 'logs' } })
// query 变化时重新拉数据(同一路由复用时 watch)
watch(() => route.query.tab, (tab) => {
fetchList(tab)
})易错点:route.params 不是深度响应式的可靠来源,路径参数变化而组件复用时(如 /orders/1 → /orders/2),要 watch route.params.id 或给 router-view 加 :key。
常见坑速查
- 刷新 404:动态路由方案里,刷新后路由表被重置,必须在守卫里重新 addRoute(见上文模板第 3 步)
- 通配符路由位置:
{ path: '/:pathMatch(.*)*' }必须放在动态路由 addRoute 之后,否则先匹配到 404 - history 模式 404:服务器需配置所有路径回落到 index.html(Nginx 的
try_files $uri $uri/ /index.html;) - 循环重定向:守卫里
next()与返回值混用会导致逻辑混乱,Vue Router 4 统一用返回值风格 - keep-alive 失效:配合动态路由时 include 需要组件 name,确保组件显式声明了
name(defineOptions({ name: 'OrderList' }))
总结
- 懒加载是标配,
props: true让组件与路由解耦 - 权限路由三步走:静态/动态路由分离 → 登录后按角色过滤 → addRoute + 重新导航
- 守卫统一用返回值风格,beforeEach 模板可以直接抄走
onBeforeRouteLeave处理离开确认,scrollBehavior处理滚动恢复- 刷新 404 和通配符路由顺序是动态路由方案最常踩的两个坑
路由系统是后台管理项目的骨架,把这套方案吃透,遇到再复杂的权限场景都能从容拆解。
评论 (0)