[Vue.js进阶]从源码角度剖析vue-router(上)

[Vue.js进阶]从源码角度剖析vue-router(上)_第1张图片
image

前言

Vue 是一个渐进式的框架,这意味着你可以只使用 Vue 的核心库来开发,但是当你在开发一个完整的业务项目时,路由是一个必不可少的部分

在曾经的前端领域中,一直都使用的是服务端渲染的模式,即用户输入 url 后,浏览器向服务器请求这个 url 对应的HTML,服务器返回 HTML给前端,前端再展示,然后当需要浏览别的页面时,需要点击 a 标签再向服务器发送一个请求,服务器就会再发给你目标页面的 HTML

这样会暴露一些缺点:

  • 每次跳转都向服务器请求,会增加服务器的压力

  • 每次跳转都会刷新页面导致跳转过程中会有一瞬间的白屏,用户体验不是非常好

  • 由于是服务端渲染,受到 XSS 的攻击可能性也较高

在 MVVM 框架兴起的同时,越来越多的开发者倾向于使用前端渲染的模式,服务端返回固定 JS 文件给前端,浏览器执行 JS 文件再渲染出整个页面,而在路由方面,前端会维护一个路由的层级树,当输入 url 后,不再向后端请求 HTML,而是去这个层级树中找到对应页面的 JS 文件并执行,从而渲染出新的页面,整个过程是纯前端控制的,所以也被称为前端路由

而 vue-router 作为 Vue 的路由库,它是怎么实现路由地址和组件之间的转换的呢,这篇文章中,我将会带大家深入 vue- router 的源码,解密 vue-router API 背后的原理

文中的源码截图只保留核心逻辑 完整源码地址

需要了解一些 Vue 的公共函数(mixins,install,defineReactive)

vue-router 版本:3.0.2

vue-router的使用方法

我们从 vue-router 的使用方法说起,当使用 vue-router 时,一般会分为3步

  1. 引入 vue-router,调用 Vue.use(Router)

  2. 实例化 router 对象,传入一个路由层级表 routes

  3. 在 main.js 中给根实例传入 router 对象

注册 vue-router 插件

当我们调用 Vue.use(Router)时会执行插件的注册流程

图1:

[Vue.js进阶]从源码角度剖析vue-router(上)_第2张图片
image

(删除了部分和入口无关的逻辑)

所有的 Vue 插件都会暴露一个 install 方法,当执行 Vue.use 时,实质上 Vue 会执行插件的 install 方法

混入全局钩子

了解过 Vue 响应式原理的朋友可以发现,vue-router 会通过 Vue.mixin 的方法全局混入 beforeCreate,destroyed 2个钩子,因为是全局混入的,所以之后所有的根实例和组件实例都会有这2个生命周期钩子

当根实例被实例化时,混入的 beforeCreate 第一次被执行,因为我们在 new Vue 时传入了 router 对象,它会被 Vue 作为 $options 的属性,所以会执行到 true 的逻辑,这里的核心在于 init 方法,它会初始化整个 vue-router 我们之后详解,另外将传入的 router 对象变成一个响应式对象,这个我们也之后讨论

除开根实例,其余所有的组件实例都会执行 false 的逻辑,它会给组件实例定义一个 _routerRoot 属性,因为 Vue 生成组件时是从上到下的,所以所有组件实例的 _routerRoot 属性都指向根实例

之后执行 registerInstance 这个也放到后面讨论

定义 $router,$route 属性

随后 Vue 在原型上定义了 $router,$route 2个对象,拦截 get 方法指向 _routerRoot.router,从上面一章可以发现,实质上指向的就是根实例的 router 对象,即日常开发中调用的 this.$router 最终都会指向根实例上的 router 对象

定义全局组件

最后通过 Vue.component 方法注册了2个全局组件,这样我们可以在任何地方直接使用组件

实例化 vue-router

通常使用 vue-router 时,会在 router.js 中通过 new Router 的形式生成一个 router 的实例,并传入一个路由的层级表 routes 数组

图2:

[Vue.js进阶]从源码角度剖析vue-router(上)_第3张图片

随后我们找到源码中的 vue-router 类

图3:

[Vue.js进阶]从源码角度剖析vue-router(上)_第4张图片
image

整个 vue-router 实例化的过程核心就做了2件事

  1. 创建路由的映射表

  2. 根据传入的 mode 属性实例化不同的 history 路由实例

创建路由的映射表

图中第四行会执行到 createMatcher 方法,返回一个对象,包含 matchaddRoutes 这2个方法,这2个方法是 vue-router 中比较重要的函数,之后我们会分析它们的作用,在这之前先看一下 createMatcher 函数中的 createRouteMap 函数

图4:

[Vue.js进阶]从源码角度剖析vue-router(上)_第5张图片
image

createRouteMap 这个函数就是用来创建路由的映射表的,它是一个记录所有信息(路由记录)的对象,将传入的 routes 数组进行一系列处理,生成 pathList,pathMap,nameMap 3张路由映射表

图5:

[Vue.js进阶]从源码角度剖析vue-router(上)_第6张图片
image

createRouteMap 内部会遍历 routes 数组,执行 addRouteRecord 方法来为**每一个数组的每个元素(route 对象)创建记录,并储存在这3个路由映射表中

图6:

[Vue.js进阶]从源码角度剖析vue-router(上)_第7张图片
image

addRouteRecord 会将每个 route 对象转换为一个路由记录并保存在之前声明的3个路由映射表中,通过源代码发现,路由记录(record 对象)非常详细的记录了 route 对象的很多属性

  • path:路由的完整路径

  • regex:匹配到当前 route 对象的正则

  • components:route 对象的组件(因为 vue-router 中有命名视图,所以会默认放在 default 属性下,instances 同理)

  • instances: route 对象对应的 vm 实例

  • name:route 对象的名字

  • parent:route 对象的父级路由记录

  • matchAs:路由别名

  • redirect:路由重定向

  • beforeEnter:组件级别的路由钩子

  • meta:路由元信息

  • props:路由跳转时的传参

在创建路由记录前,会使用 normalizedPath 规范化 route 对象的路径,如果传入的 route 对象含有父级 route 对象,会将父级 route 对象的 path 拼上当前的 path

图7:

[Vue.js进阶]从源码角度剖析vue-router(上)_第8张图片
image

例如图2中的 comp1Child 这个 route 对象,它的 path 最终会变成

"/comp1" + "comp1Child" => "/comp1/com1Child"

而最终会生成的路由记录是这样的

图8:

[Vue.js进阶]从源码角度剖析vue-router(上)_第9张图片

随后因为 route 可能含有 children 属性,即含有子的 route 对象组成的数组,所以需要进行递归的遍历,然后将 record 对象放入这3个路由映射表中,而这3个路由映射表的区别在于

  • pathList:数组,保存了 route 对象的路径

  • pathMap:对象,保存了所有 route 对象对应的 record 对象

  • nameMap:对象,保存了所有含有name属性的 route 对象对应的 record 对象

图2中的路由对应的3张路由映射表如下:

pathList:

[Vue.js进阶]从源码角度剖析vue-router(上)_第10张图片

pathMap:

[Vue.js进阶]从源码角度剖析vue-router(上)_第11张图片

nameMap:

[Vue.js进阶]从源码角度剖析vue-router(上)_第12张图片

可以看到 pathMap 和 nameMap 是一样的,因为图2中的路由都有 name 属性,如果某个路由没有 name 属性,则只会在 pathMap 中存在

对比保存了所有 route 对象的 routes 数组和这3个路由映射表,我们可以发现:routes 对象是一个递归的树形结构,而路由映射表是一个扁平的一维结构,通过路由映射表里的 parent 属性来维护父子关系

动态添加路由的 addRoutes 函数

在创建完路由映射表后,会向外暴露一个动态添加路由的 API addRoutes

图10:

[Vue.js进阶]从源码角度剖析vue-router(上)_第13张图片
image

它的原理其实很简单,就是接受一个 route 对象,并且把它转换成 record 对象,然后合并到之前生成的路由映射表中,所以我们可以在外部调用 router.addRoutes 动态注册路由

返回 $route 对象的 match 函数

createMatcher 返回的第二个函数是 matchmatch 函数会返回一个 route 对象

图11:

[Vue.js进阶]从源码角度剖析vue-router(上)_第14张图片
image

之前说的 route 是针对 new Router 时传入的 routes 数组的每个元素,而 $route 是最终返回作为 Vue.prototype.$route 使用的对象,在 flow 语言中,route 的类型是 RouteConfig,而 $route 的类型是 Route,具体接口的定义可以查看源代码,虽然在源码中两者变量名都是 route,但我下文会使用 $route 来区分通过 this.$route 返回 route 对象

图12:

routes :

[Vue.js进阶]从源码角度剖析vue-router(上)_第15张图片

$route :

[Vue.js进阶]从源码角度剖析vue-router(上)_第16张图片

前者表示的是路由的一些基础配置项,而后者是真正经过 vue-router 处理后表示当前路由的对象

每次路由跳转的时候都会执行这个 match 函数生成一个 $route 对象,具体什么时候会触发 match 放到下篇中讲,这章先分析 match 函数是如何最终生成一个真正的 $route 对象的

生成 loaction 对象

match函数首先会执行 normalizeLocation 函数,它是一个辅助函数,会将调用 router.push / router.replace 时跳转的路由地址转为一个 location 对象

那什么是 location 对象? MDN 上是这么解释的

Location接口表示其链接到的对象的位置(URL)。所做的修改反映在与之相关的对象上。 DocumentWindow 接口都有这样一个链接的Location,分别通过 Document.locationWindow.location 访问。

通俗的来说就是用一个对象来描述当前 url 的一些信息。当我们在地址栏中输入 www.baidu.com ,按 F12 打开控制台,输入 loaction 就能展示出当前地址的一些信息

图13:

[Vue.js进阶]从源码角度剖析vue-router(上)_第17张图片
image

vue-router 在 location 接口的基础上做了一些增强,添加了 name,path,hash 等 vue-router 特有的属性

举个例子,当调用 router.push({name:"comp1"}) 使用 name 的形式进行路由跳转时,返回的 loaction 对象就会有一个 name 属性,当 name 存在时,会走到图11中的 true 逻辑,从之前 createMatcher 生成的 nameMap 路由映射表中找到对应 name 的路由记录 record 对象,最终会执行 _createRoute 这个方法

而调用 router.push("/comp1") 使用路径的形式进行路由跳转,同样也会返回一个 location 对象,但不会有 name 属性,走图11的 false 逻辑,从另外2个路由映射表 pathMap,pathList 中找到对应的路由记录,最终也会执行 _createRoute 这个方法

可见无论使用 name 跳转还是使用 path 跳转,最终都会执行 _createRoute ,带下划线的 _createRoute 是一个私有方法,它最终会调用 createRoute 生成 $route 对象

生成 $route 对象

图14:
[Vue.js进阶]从源码角度剖析vue-router(上)_第18张图片
image

经过对一些 query 参数的处理,最终返回 $route 对象,其中有一个 matched 属性值得注意,它通过 formatMatch 函数生成,查看过 this.$route 返回值的朋友应该知道,matched 是一个数组,每个元素都是一个路由记录(record)

图15:

[Vue.js进阶]从源码角度剖析vue-router(上)_第19张图片
image

还记得之前在生成路由记录的时定义的 parent 属性吗?它的其中一个用途就是通过不断的向上查找父级的路由记录,放入 matched 数组中,最终返回一个保存了当前路由记录和所有父级数组,顺序是 父 => 子

图16:

[Vue.js进阶]从源码角度剖析vue-router(上)_第20张图片

而这个 matched 数组最终会决定触发哪些路由组件的哪些路由守卫钩子,关于路由钩子部分我们放到下篇来说

生成 history 路由实例

再次回到图3,vue-router 根据传入参数的 mode 属性来实例化不同的路由类(HTML5,hash,abstract),这也是官方提供给开发者的3种不同的选择来生成路由

  • HTML5 路由是相对比较美观的一种路由,和正常的 url 显示没有什么区别,核心依靠 pushStatereplaceState 来实现不向后端发送请求的路由跳转,但是当用户点击刷新按钮时会存在找不到页面的情况,需要配合 nginx 来做一层转发

  • hash 路由是默认使用的路由,在 url 中会存在一个 # 号,核心依靠这个 # 号也就是曾经作为路由的锚点来实现不向后端发送请求的路由跳转

  • abstract 路由是一种抽象路由,一般用在非浏览器端,维护一种抽象的路由结构,使得能够嫁接在客户端或者服务端等没有 history 路由的地方

总结

  • 当调用 Vue.use(Router) 时,会给全局的 beforeCreate,destroyed 混入2个钩子,使得在组件初始化时能够通过 this.$router / this.$route 访问到根实例的 router / route 对象,同时还定义了全局组件 router-view / router-link

  • 在实例化 vue-router 时,通过 createRouteMap 创建3个路由映射表,保存了所有路由的记录,另外创建了 match 函数用来创建 $route 对象,addRoutes 函数用来动态生成路由,这2个函数都是需要依赖路由映射表生成的

  • vue-router 还给开发者提供了3种不同的路由模式,每个模式下的跳转逻辑都有所差异

vue-router 定义了 match 方法用来生成 $route 对象,而什么时候会调用 match 方法还没有分析过,另外文章开头的 registerInstance 又是做什么的,在下篇中我会分析 vue-router 中的跳转逻辑,包括路由守卫,vue-router 的全局组件,以及组件相关的视图更新

参考资料

Vue.js 技术揭秘

你可能感兴趣的:([Vue.js进阶]从源码角度剖析vue-router(上))