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

栏目: 编程语言 · 发布时间: 6年前

内容简介:Vue 是一个渐进式的框架,这意味着你可以只使用 Vue 的核心库来开发,但是当你在开发一个完整的业务项目时,路由是一个必不可少的部分在曾经的前端领域中,一直都使用的是服务端渲染的模式,即用户输入 url 后,浏览器向服务器请求这个 url 对应的HTML,服务器返回 HTML给前端,前端再展示,然后当需要浏览别的页面时,需要点击 a 标签再向服务器发送一个请求,服务器就会再发给你目标页面的 HTML这样会暴露一些缺点:

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(上)

所有的 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个全局组件,这样我们可以在任何地方直接使用<router-view>和<router-link>组件

实例化 vue-router

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

图2:

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

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

图3:

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

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

  1. 通过 createMatcher 创建了一个对象赋值给实例的 matcher 属性

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

创建路由的映射表

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

图4:

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

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

图5:

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

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

图6:

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

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(上)

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

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

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

图8:

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

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

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

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

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

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

pathList:

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

pathMap:

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

nameMap:

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

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

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

动态添加路由的 addRoutes 函数

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

图10:

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

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

返回 $route 对象的 match 函数

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

图11:

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

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

图12:

routes :

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

$route :

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

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

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

生成 loaction 对象

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

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

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

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

图13:

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

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(上)

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

图15:

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

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

图16:

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

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

生成 history 路由实例

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

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

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

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

流程图

这里画了一张流程图来表达实例化 vue-router 时的 matcher 属性内部的依赖关系

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

总结

  • 当调用 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 的全局组件,以及组件相关的视图更新


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Impractical Python Projects

Impractical Python Projects

Lee Vaughan / No Starch Press / 2018-11 / USD 29.95

Impractical Python Projects picks up where the complete beginner books leave off, expanding on existing concepts and introducing new tools that you’ll use every day. And to keep things interesting, ea......一起来看看 《Impractical Python Projects》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具