Code前端首页关于Code前端联系我们

vue-router 是怎么实现不同路由模式的?

terry 3周前 (09-05) 阅读数 48 #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.pushStatehistory.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,整个导航流程的守卫执行顺序是这样的:

  1. 离开当前组件的守卫:如果当前组件(路由 A 对应的组件)里有 beforeRouteLeave 守卫,会先执行,用来处理离开前的逻辑(比如弹窗确认)。
  2. 全局前置守卫(beforeEach):执行全局注册的 router.beforeEach 钩子,通常用来做权限拦截(比如判断用户是否登录,没登录就跳登录页)。
  3. 重用组件的更新守卫(beforeRouteUpdate):如果跳转时组件被复用(比如从 /user/1 跳到 /user/2,组件还是 User 组件),会执行组件内的 beforeRouteUpdate,处理参数变化的逻辑。
  4. 路由独享守卫(beforeEnter):如果目标路由(路由 B)的配置里有 beforeEnter,执行这个守卫,它只对当前路由生效。
  5. 解析异步组件:如果目标路由对应的组件是异步加载的(() => import('./User.vue')),会在这里触发组件加载,等待加载完成后再继续。
  6. 进入目标组件的守卫(beforeRouteEnter):目标组件(路由 B 对应的组件)里的 beforeRouteEnter 执行,注意这时候组件实例还没创建,所以不能用 this,但可以通过 next(vm => { ... }) 拿到后续创建的实例。
  7. 全局解析守卫(beforeResolve):所有组件内守卫和异步组件都解析完成后,执行全局的 router.beforeResolve,这是导航确认前的最后一个全局守卫。
  8. 导航确认,更新 URL:到这一步导航被确认,修改浏览器的 URL(hash 或 history 模式对应的操作)。
  9. 全局后置守卫(afterEach):导航完成后,执行 router.afterEach,通常用来做页面埋点、滚动条重置等不影响导航的操作。
  10. 触发 DOM 更新,执行 beforeRouteEnter 的 next 回调:组件渲染到 DOM 后,执行 beforeRouteEnternext 里的回调,此时能拿到组件实例 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),怎么判断匹配?源码里会对比当前路由的 pathrouter-linkto 解析后的路径,支持“包含匹配”(/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 的属性(pathparamsquery 等)变化时,所有依赖它的组件(比如用了 $route 的组件、<router-view>)都会触发更新。

每次导航完成后(比如调用 router.push 并成功匹配到路由),vue-router 会创建一个新的 Route 对象(包含当前路径、参数、匹配记录等信息),然后把 this._currentRoute 赋值为这个新对象,因为 _currentRoute 是响应式的,所有依赖它的组件会感知到变化,从而重新渲染。

举个例子:当用户从 /user/1 跳到 /user/2currentRoute.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前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门