Vue Router 原理深度解析:从路由匹配到导航守卫
Vue Router 是 Vue.js 官方路由管理器,几乎每个 Vue 项目都在用。但很多人只停留在 <router-link> 和 <router-view> 的使用层面,对它背后的工作原理知之甚少。本文将从路由匹配、历史模式、导航守卫三个核心维度,深入拆解 Vue Router 的运行机制。
路由匹配系统
路由配置的本质
当我们写如下配置时:
const routes = [
{ path: '/', component: Home },
{ path: '/users/:id', component: UserDetail },
{ path: '/users/:id/posts/:postId', component: UserPost }
]
Vue Router 内部会将这些路径模式编译成一棵路由匹配树。每个路径模式会被 path-to-regexp(Vue Router 4 内部使用自己的简化实现)解析成一个正则表达式和参数提取规则。
路径模式到正则的编译
// /users/:id → 正则和参数定义
{
re: /^\/users\/([^/]+)\/?$/i,
keys: [{ name: 'id', prefix: '/', suffix: '', pattern: '[^/]+' }],
score: 20 // 优先级评分
}
// /users/:id/posts/:postId
{
re: /^\/users\/([^/]+)\/posts\/([^/]+)\/?$/i,
keys: [
{ name: 'id', prefix: '/', pattern: '[^/]+' },
{ name: 'postId', prefix: '/', pattern: '[^/]+' }
],
score: 40
}
路由优先级评分
Vue Router 4 引入了评分机制来解决路由冲突问题。每条路由都有一个分值,分值高的优先匹配:
| 模式 | 分值 | 说明 |
|---|---|---|
/ |
1 × 10 = 10 | 静态段,分值最高 |
/users |
1 × 10 + 1 × 10 = 20 | 两个静态段 |
/users/:id |
20 + 4 = 24 | 静态段 + 动态参数 |
/users/:id/posts |
24 + 10 = 34 | 再加一个静态段 |
/users/:id/posts/:postId |
34 + 4 = 38 | 再加一个动态参数 |
/users/:id(\\d+) |
20 + 10 = 30 | 自定义正则参数分值更高 |
/* |
1 | 通配符,分值最低 |
核心规则:静态路径 > 动态参数 > 通配符。自定义正则的参数比普通参数分值更高,因为更具体。
匹配过程
当浏览器 URL 变化时,Vue Router 的匹配流程:
URL 变化 → 解析 path → 遍历路由记录(按分值降序) → 第一个匹配的记录
→ 提取参数 → 组装 matched 数组(含嵌套路由)
// 简化的匹配逻辑
function matchRoute(path: string, records: RouteRecord[]): RouteMatch[] {
// 按 score 降序排列
const sorted = records.sort((a, b) => b.score - a.score)
for (const record of sorted) {
const match = record.re.exec(path)
if (match) {
// 提取参数
const params = {}
record.keys.forEach((key, i) => {
params[key.name] = decodeURIComponent(match[i + 1])
})
// 递归获取父级路由(处理嵌套)
const matched = [record]
let parent = record.parent
while (parent) {
matched.unshift(parent)
parent = parent.parent
}
return { matched, params }
}
}
return null
}
History 模式
三种历史模式对比
| 模式 | URL 样式 | 服务器配置 | 适用场景 |
|---|---|---|---|
| Hash | /#/users |
不需要 | 简单部署、无服务端控制 |
| HTML5 | /users |
需要 fallback | 生产环境推荐 |
| Memory | 内存中 | 不需要 | SSR、测试 |
Hash 模式实现原理
Hash 模式利用 URL 的 # 片段。hashchange 事件可以监听变化,且不会触发服务端请求:
class HTML5History extends RouterHistory {
constructor() {
super()
// 监听 hashchange
window.addEventListener('hashchange', () => {
this.transitionTo(this.current.path)
})
}
push(path: string) {
window.location.hash = path
}
get current() {
// 从 hash 中提取路径,去掉 #
const hash = window.location.hash.slice(1) || '/'
return hash
}
}
HTML5 History 模式实现原理
HTML5 模式使用 pushState / replaceState API,URL 更干净:
class HTML5History extends RouterHistory {
constructor() {
super()
// 监听 popstate(前进/后退按钮触发)
window.addEventListener('popstate', (e) => {
this.transitionTo(window.location.pathname, e.state)
})
}
push(path: string) {
// pushState 不会触发 popstate,需要手动 transition
window.history.pushState({ path }, '', path)
this.transitionTo(path)
}
replace(path: string) {
window.history.replaceState({ path }, '', path)
this.transitionTo(path)
}
}
关键区别:pushState 不会触发 popstate 事件,所以 push 后需要手动调用 transitionTo。而浏览器的前进/后退按钮会触发 popstate,在事件中读取 event.state 恢复状态。
服务端 fallback 配置
HTML5 模式下,用户直接访问 /users/123 时,服务器会尝试寻找对应的文件,返回 404。需要配置所有路由回退到 index.html:
location / {
try_files $uri $uri/ /index.html;
}
导航守卫机制
完整的导航解析流程
一次完整的路由导航会经历以下阶段:
导航触发
→ beforeEach(全局前置)
→ beforeEnter(路由独享)
→ beforeRouteEnter(组件内,next 版)
→ beforeRouteUpdate(组件复用时)
→ beforeRouteLeave(组件内,离开时)
→ 异步组件加载
→ beforeResolve(全局解析)
→ 导航确认
→ afterEach(全局后置)
→ DOM 更新 / 组件挂载
守卫执行顺序实战
const router = createRouter({
routes: [
{
path: '/users',
component: Users,
beforeEnter: (to, from) => {
console.log('2. 路由独享守卫')
},
children: [
{
path: ':id',
component: UserDetail,
beforeEnter: [guard1, guard2] // 支持数组
}
]
}
]
})
// 全局前置
router.beforeEach((to, from) => {
console.log('1. 全局前置守卫')
// return false 取消导航
// return '/login' 重定向
// return { path: '/login', query: { redirect: to.fullPath } }
})
// 全局解析
router.beforeResolve((to, from) => {
console.log('3. 全局解析守卫')
// 此时所有组件内守卫和异步组件已加载完毕
})
// 全局后置
router.afterEach((to, from) => {
console.log('4. 全局后置钩子')
// 不能改变导航,常用于页面标题、统计
document.title = to.meta.title || 'My App'
})
组件内守卫
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// 离开守卫:防止未保存的修改丢失
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
const answer = window.confirm('有未保存的修改,确定离开?')
if (!answer) return false
}
})
// 复用守卫:从 /users/1 → /users/2 时触发
onBeforeRouteUpdate((to, from) => {
// 需要手动根据新参数获取数据
fetchUser(to.params.id)
})
</script>
异步导航守卫
守卫可以返回 Promise,Vue Router 会等待 resolve:
router.beforeEach(async (to, from) => {
// 等待用户信息加载
if (!userStore.user) {
await userStore.fetchUser()
}
// 权限检查
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
return { path: '/login', query: { redirect: to.fullPath } }
}
if (to.meta.roles && !to.meta.roles.includes(userStore.user.role)) {
return { name: 'forbidden' }
}
})
导航失败的类型
import { NavigationFailureType } from 'vue-router'
router.afterEach((to, from, failure) => {
if (failure) {
switch (failure.type) {
case NavigationFailureType.aborted:
console.log('导航被中止(守卫返回 false)')
break
case NavigationFailureType.cancelled:
console.log('导航被取消(新导航覆盖了旧导航)')
break
case NavigationFailureType.duplicated:
console.log('导航重复(已在目标路由)')
break
}
}
})
路由懒加载与异步组件
动态导入实现按需加载
const routes = [
{
path: '/dashboard',
// 动态导入 → 自动代码分割
component: () => import('@/views/Dashboard.vue')
}
]
Webpack / Vite 会将动态 import() 的组件拆分成单独的 chunk,首次访问时才加载。
路由级 loading
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
// 不推荐用这种方式(Vue Router 4 更好用 defineAsyncComponent)
}
]
// 更好的做法:组合 defineAsyncComponent
import { defineAsyncComponent } from 'vue'
const Dashboard = defineAsyncComponent({
loader: () => import('@/views/Dashboard.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorPage,
delay: 200,
timeout: 10000
})
Prefetch 预取
// 结合 webpackPrefetch 魔法注释
const routes = [
{
path: '/dashboard',
component: () => import(
/* webpackPrefetch: true */
/* webpackChunkName: "dashboard" */
'@/views/Dashboard.vue'
)
}
]
// Vite 中使用 <link rel="prefetch"> 手动预取
```vue
<link rel="prefetch" href="/assets/dashboard.js" />
## 动态路由与编程式导航
### 添加/删除路由
```typescript
// 动态添加路由(如基于用户权限)
router.addRoute({
path: '/admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, roles: ['admin'] }
})
// 添加子路由
router.addRoute('admin', {
path: 'users',
component: () => import('@/views/AdminUsers.vue')
})
// 删除路由
router.removeRoute('admin')
// 检查路由是否存在
if (router.hasRoute('admin')) {
// ...
}
编程式导航
// push:添加历史记录
router.push('/users/1')
router.push({ name: 'user', params: { id: '1' } })
router.push({ path: '/users/1' })
router.push({ path: '/search', query: { q: 'vue' } })
// → /search?q=vue
// replace:替换当前记录
router.replace({ path: '/login' })
// go:前进/后退
router.go(-1) // 后退
router.go(1) // 前进
// 检查是否能返回
if (window.history.state.back) {
router.back()
} else {
router.push('/') // 没有历史,回到首页
}
useRouter 与 useRoute
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// route 是响应式的,可以 watch
watch(
() => route.params.id,
(newId) => {
fetchArticle(newId)
}
)
// route 的关键属性
console.log(route.path) // '/users/123'
console.log(route.name) // 'user'
console.log(route.params) // { id: '123' }
console.log(route.query) // { tab: 'profile' }
console.log(route.hash) // '#section'
console.log(route.fullPath) // '/users/123?tab=profile#section'
console.log(route.meta) // { requiresAuth: true, title: '用户详情' }
console.log(route.matched) // 匹配的路由记录数组(含父级)
总结
Vue Router 的核心设计可以归纳为三层:
- 匹配层:路径模式编译 → 评分排序 → 正则匹配 → 参数提取
- 历史层:Hash / HTML5 / Memory 三种模式,统一接口,封装浏览器 History API
- 守卫层:洋葱模型导航守卫,支持全局/路由/组件三个层级,异步守卫
理解了这些原理,后续开发中遇到路由不匹配、守卫不执行、导航失败等问题时,都能从原理出发快速定位。
Comments | 0条评论