vue-router 是怎么实现不同路由模式的?
前端路由是单页面应用(SPA)的核心功能之一,vue-router 作为 Vue 生态里管理路由的“大管家”,它的源码里藏着前端路由实现的诸多关键逻辑,不管是想搞懂路由模式差异、导航守卫怎么工作,还是想优化路由加载体验,看透 vue-router 源码里的设计思路都很有必要,下面通过几个关键问题,拆解 vue-router 源码中的核心逻辑~
前端路由得解决“URL 变了页面不刷新,但组件能切换”的问题,vue-router 搞了三种模式来适配不同场景,源码里每种模式的实现思路差别还挺大:先聊hash 模式(默认用这个):浏览器 URL 里的 后面那串(http://xxx.com/#/user
)叫 hash,它有个特点——变化时不会触发页面刷新,因为浏览器觉得这只是页面内的锚点跳转,vue-router 源码里专门搞了个 HashHistory
类来管这事儿:一方面监听 window.onhashchange
事件,只要 hash 变了,就去匹配对应的路由;当我们用 router.push
跳转时,它会通过 window.location.hash = 新路径
来改 URL,触发 hashchange 完成导航,这种模式兼容性好,老浏览器也能跑。
再看history 模式:想让 URL 更干净(没 ),就得用 HTML5 新增的 history.pushState
和 history.replaceState
这俩 API,源码里对应的 HTML5History
类,跳转时用这俩 API 改 URL(不会刷新页面),同时监听 window.onpopstate
事件(用户点浏览器前进/后退时触发),但有个坑:直接访问 /user
这类路径时,服务器得返回单页应用的入口 HTML,否则会 404,所以后端得配合配置。
abstract 模式:这是给非浏览器环境准备的,Node.js 做服务端渲染、Weex 这类框架,源码里的 AbstractHistory
类,内部用数组模拟浏览器的历史记录栈,导航时就操作这个数组的 push、pop,完全不用浏览器 API,纯内存里搞事儿。
初始化 vue-router 时,会根据配置的 mode
选对应的 History 实例,把 URL 变化、路由跳转这些细节都封装好,上层用的时候不用操心底层咋实现的。
路由匹配的核心逻辑在源码中是如何实现的?
当用户访问一个 URL 时,vue-router 得快速找到对应的组件,这个“匹配”过程在源码里分两步:路由配置解析和路径匹配。
第一步,路由配置解析:用户写的 routes
数组([{ path: '/user', component: User }, ...]
)会被源码里的 createRouteMap
函数处理,把每个路由配置转成路由记录(RouteRecord),这个过程中,不仅要处理普通路径,还要处理动态路由(如 /user/:id
)、嵌套路由(子路由配置在 children
数组里),对于动态路由,源码会把路径转成正则表达式(/user/:id
转成 /user/([^/]+)
),同时记录参数名(id
);对于嵌套路由,父路由的路径会和子路由路径拼接(比如父 /user
+ 子 /profile
变成 /user/profile
),确保层级关系正确。
第二步,路径匹配:当 URL 变化时(hashchange 或 popstate 触发),vue-router 会拿到当前的路径(如 /user/123
),然后遍历所有路由记录的正则表达式,找到第一个匹配的路由记录,如果是嵌套路由,还会递归匹配子路由,最终得到一个匹配列表(matched)——包含从顶层到当前层级的所有路由记录(比如访问 /user/123/profile
,matched 里可能有 /user
、/user/:id
、/user/:id/profile
对应的路由记录),有了这个 matched 列表,后续 <router-view>
就能知道该渲染哪个层级的组件了。
简单说,源码通过“预解析路由配置成正则和记录”+“运行时正则匹配路径”的方式,高效完成路由和组件的对应关系查找。
导航守卫的执行顺序和源码逻辑是怎样的?
导航守卫是控制路由跳转“生命周期”的关键,比如权限判断、数据预加载都靠它,vue-router 里的守卫分全局、路由独享、组件内三类,它们的执行顺序在源码里是严格的队列式流程:
假设现在从路由 A 跳转到路由 B,整个导航流程的守卫执行顺序是这样的:
- 离开当前组件的守卫:如果当前组件(路由 A 对应的组件)里有
beforeRouteLeave
守卫,会先执行,用来处理离开前的逻辑(比如弹窗确认)。 - 全局前置守卫(beforeEach):执行全局注册的
router.beforeEach
钩子,通常用来做权限拦截(比如判断用户是否登录,没登录就跳登录页)。 - 重用组件的更新守卫(beforeRouteUpdate):如果跳转时组件被复用(比如从
/user/1
跳到/user/2
,组件还是 User 组件),会执行组件内的beforeRouteUpdate
,处理参数变化的逻辑。 - 路由独享守卫(beforeEnter):如果目标路由(路由 B)的配置里有
beforeEnter
,执行这个守卫,它只对当前路由生效。 - 解析异步组件:如果目标路由对应的组件是异步加载的(
() => import('./User.vue')
),会在这里触发组件加载,等待加载完成后再继续。 - 进入目标组件的守卫(beforeRouteEnter):目标组件(路由 B 对应的组件)里的
beforeRouteEnter
执行,注意这时候组件实例还没创建,所以不能用this
,但可以通过next(vm => { ... })
拿到后续创建的实例。 - 全局解析守卫(beforeResolve):所有组件内守卫和异步组件都解析完成后,执行全局的
router.beforeResolve
,这是导航确认前的最后一个全局守卫。 - 导航确认,更新 URL:到这一步导航被确认,修改浏览器的 URL(hash 或 history 模式对应的操作)。
- 全局后置守卫(afterEach):导航完成后,执行
router.afterEach
,通常用来做页面埋点、滚动条重置等不影响导航的操作。 - 触发 DOM 更新,执行 beforeRouteEnter 的 next 回调:组件渲染到 DOM 后,执行
beforeRouteEnter
中next
里的回调,此时能拿到组件实例vm
。
源码里怎么管理这么多守卫的顺序?核心是用Promise 链和队列,每个守卫可以返回 Promise(比如异步请求判断权限),vue-router 会把这些守卫包装成 Promise,按顺序链式调用,确保前一个守卫完成(比如异步请求结束)后,再执行下一个,如果某个守卫调用 next(false)
,就会终止导航,回退到之前的路由;如果调用 next('/login')
,就会触发新的导航流程。
和 组件在源码中是如何工作的?
这两个组件是 vue-router 暴露给开发者的“界面入口”,一个负责渲染匹配的组件,一个负责生成跳转链接,它们的源码逻辑藏着很多细节:
:动态渲染匹配组件
<router-view>
是个函数式组件(没有自己的状态,只负责渲染),它的核心逻辑是“找到当前路由匹配的组件,然后渲染”,源码里它做了这些事:
- 获取当前路由匹配列表:通过 Vue 的
inject
拿到router
实例,然后取router.currentRoute.matched
(就是前面路由匹配得到的层级路由记录列表)。 - 处理嵌套层级:每个
<router-view>
有个“深度(depth)”,比如父组件里的<router-view>
深度是 0,渲染最顶层匹配的组件;子组件里的<router-view>
深度是 1,渲染下一层级的组件,源码里通过递归或父组件传递的 depth 来确定要渲染matched
数组中哪个索引的路由记录。 - 渲染组件:找到对应深度的路由记录后,取出它的
component
(可能是同步或异步组件),然后渲染到页面上,如果是异步组件,还要处理加载中的状态(比如显示 loading 组件)。
举个例子:如果路由配置是 { path: '/user', component: User, children: [{ path: 'profile', component: Profile }] }
,User
组件里的 <router-view>
(depth 0)会渲染 User
自己吗?不,User
是父路由的组件,父 <router-view>
(在 App 组件里)渲染 User
,而 User
里的 <router-view>
(depth 1)渲染 Profile
组件,这样就实现了嵌套路由的分层渲染。
:生成可跳转的链接
<router-link>
负责生成带跳转功能的链接,源码里它要处理 URL 生成、跳转逻辑、激活状态(active class)等:
- 解析目标路径:用户给
to
属性传值(可以是字符串路径,也可以是路由对象),router-link
会调用router.resolve
把它转成完整的 URL(考虑基路径、路由模式等)。 - 处理跳转:点击
router-link
时,默认是调用router.push
跳转(如果是replace
模式则调用router.replace
),源码里给a
标签绑定点击事件,阻止默认跳转(避免页面刷新),然后触发路由导航。 - 激活状态判断:当当前路由和
router-link
对应的路由匹配时,要给a
标签加active-class
(默认是router-link-active
),怎么判断匹配?源码里会对比当前路由的path
和router-link
的to
解析后的路径,支持“包含匹配”(/user
匹配/user/123
)和“精确匹配”(通过exact
属性控制)。 - 渲染成 a 标签:
router-link
会渲染成<a>
标签(也可以通过tag
属性改成其他标签),把解析好的href
赋值给a
标签,保证用户右键“在新标签页打开”时能正常跳转(这时候会触发浏览器默认的页面刷新,所以后端要配合处理 history 模式的情况)。
路由的响应式是怎么实现的?
在 Vue 项目里,我们能在组件中通过 this.$route
拿到当前路由,而且路由变化时组件会自动更新,这背后是 vue-router 结合 Vue 响应式系统实现的:
vue-router 源码中,currentRoute 是一个响应式对象,在 VueRouter 类的初始化过程中,会用 Vue 的 observable
(或更现代的 reactive
)把当前路由对象变成响应式数据,简单说,就是让 currentRoute
成为“依赖收集源”——当 currentRoute
的属性(path
、params
、query
等)变化时,所有依赖它的组件(比如用了 $route
的组件、<router-view>
)都会触发更新。
每次导航完成后(比如调用 router.push
并成功匹配到路由),vue-router 会创建一个新的 Route
对象(包含当前路径、参数、匹配记录等信息),然后把 this._currentRoute
赋值为这个新对象,因为 _currentRoute
是响应式的,所有依赖它的组件会感知到变化,从而重新渲染。
举个例子:当用户从 /user/1
跳到 /user/2
,currentRoute.params.id
从 1 变成 2,所有在模板中用了 $route.params.id
的组件,或者依赖 currentRoute
的 <router-view>
,都会因为响应式依赖的触发而重新渲染,展示新的内容。
异步路由和代码分割在vue-router 中是怎么支持的?
现在前端项目越来越大,“按需加载组件”是性能优化的关键,vue-router 天然支持异步路由,背后的源码逻辑和 Vue 的异步组件、打包工具(如 webpack)的代码分割机制紧密相关:
用户在路由配置中写异步组件时,通常用这样的语法:{ path: '/user', component: () => import('./User.vue') }
,这个 import()
是动态 import 语法,webpack 会把它当成一个代码分割点,打包时生成单独的 chunk 文件。
vue-router 源码在处理这种异步组件时,会在路由匹配和导航过程中加载组件:当导航到这个路由时,在执行到“解析异步组件”的步骤(参考导航守卫那部分的流程),会调用这个 import()
函数,它返回一个 Promise,vue-router 会等待这个 Promise resolve(即组件加载完成)后,再继续后续的导航流程,如果加载失败(比如网络错误),还能在全局错误处理中捕获,跳转到错误页面。
vue-router 还支持更细粒度的异步组件配置,比如结合 Vue 的 defineAsyncComponent
来处理加载状态、错误状态:component: defineAsyncComponent({ loader: () => import('./User.vue'), loadingComponent: Loading, errorComponent: Error })
,源码在处理这类异步组件时,会识别 loader
函数,同样在导航过程中触发加载,并用 loadingComponent
占位,加载失败则显示 errorComponent
。
这样一来,用户访问不同路由时,对应的组件代码才会被加载,减少了首屏加载的资源体积,提升了性能。
路由错误处理(如404、导航失败)在源码中有哪些机制?
路由过程中难免出现“找不到匹配路由”“导航被拦截”“异步组件加载失败”等情况,vue-router 源码里有一套错误处理机制:
404 路由匹配:用户可以在路由配置的最后加一个通配符路由({ path: '*', component: NotFound }
),当所有路由都匹配失败时,就会匹配到这个通配符路由,渲染对应的 404 组件,源码里的路由匹配逻辑是“按顺序匹配,直到找到第一个匹配项”,所以通配符路由要放在最后,确保前面的精确路由先匹配。
然后是导航失败处理:比如在全局前置守卫 beforeEach
中调用 next(false)
,会取消当前导航,回退到之前的路由;如果调用 next(new Error('权限不足'))
,可以在全局错误处理中捕获,源码里导航流程的 Promise 链如果遇到 reject,会触发对应的错误回调,开发者可以通过 router.onError
注册全局错误处理函数,捕获异步组件加载失败、导航守卫里的错误等。
还有异步组件加载错误:当 import()
加载组件失败时(比如网络超时、文件不存在),vue-router 会让这个 Promise reject,然后在导航流程中触发错误,此时可以用 router.onError
捕获,或者在 defineAsyncComponent
里配置 errorComponent
来展示错误页面,避免整个应用崩溃。
这些错误处理机制让路由系统更健壮,开发者能在不同环节拦截错误,给用户更友好的提示。
看透 vue-router 源码里的这些逻辑,不仅能明白“前端路由怎么让 URL 和组件联动”,还能学到框架设计里的分层思想(如不同路由模式的抽象封装)、响应式结合的技巧、异步流程的管理等,如果平时开发中遇到“路由跳转后页面没更新”“守卫执行顺序不对”这类问题,回到源码逻辑里找答案,往往能更快定位解决~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。