Vue 是一个渐进式的框架,这意味着你能够只使用 Vue 的核心库来开发,可是当你在开发一个完整的业务项目时,路由是一个必不可少的部分javascript
在曾经的前端领域中,一直都使用的是服务端渲染的模式,即用户输入 url 后,浏览器向服务器请求这个 url 对应的HTML,服务器返回 HTML给前端,前端再展现,而后当须要浏览别的页面时,须要点击 a 标签再向服务器发送一个请求,服务器就会再发给你目标页面的 HTML前端
这样会暴露一些缺点:vue
每次跳转都向服务器请求,会增长服务器的压力java
每次跳转都会刷新页面致使跳转过程当中会有一瞬间的白屏,用户体验不是很是好nginx
因为是服务端渲染,受到 XSS 的攻击可能性也较高git
在 MVVM 框架兴起的同时,愈来愈多的开发者倾向于使用前端渲染的模式,服务端返回固定 JS 文件给前端,浏览器执行 JS 文件再渲染出整个页面,而在路由方面,前端会维护一个路由的层级表,当输入 url 后,再也不向后端请求 HTML,而是去这个层级表中找到对应页面的 JS 文件并执行,从而渲染出新的页面,整个过程是纯前端控制的,因此也被称为前端路由github
而 vue-router 做为 Vue 的路由库,它是怎么实现路由地址和组件之间的转换的呢,这篇文章中,我将会带你们深刻 vue- router 的源码,解密 vue-router API 背后的原理vue-router
文中的源码截图只保留核心逻辑 完整源码地址后端
有兴趣的朋友也能够看我学习源码时的详细注释源码地址api
须要了解一些 Vue 的公共函数(mixins,install,defineReactive)
vue-router 版本:3.0.2
咱们从 vue-router 的使用方法提及,当使用 vue-router 时,通常会分为3步
引入 vue-router,调用 Vue.use(Router)
实例化 router 对象,传入一个路由层级表 routes
在 main.js 中给根实例传入 router 对象
当咱们调用 Vue.use(Router)时会执行插件的注册流程
图1:
全部的 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 咱们以后详解,另外将一个 _route 对象变成一个响应式对象,这个和本章逻辑无关,我会放到之后章节讨论
除开根实例,其他全部的组件实例都会执行 false 的逻辑,它会给组件实例定义一个 _routerRoot 属性,由于 Vue 生成组件时是从上到下的,因此全部组件实例的 _routerRoot 属性都指向根实例
以后执行 registerInstance
这个也放到后面讨论
随后 Vue 在原型上定义了 $router,$route 2个对象,拦截 get 方法指向 _routerRoot.router,从上面一节能够发现,实质上指向的就是根实例的 router 对象,即平常开发中调用的 this.$router 最终都会指向根实例上的 router 对象,至于 $route 咱们放到后面来讲
最后经过 Vue.component 方法注册了2个全局组件,这样咱们能够在任何地方直接使用<router-view>和<router-link>组件
一般使用 vue-router 时,会在 router.js 中经过 new Router 的形式生成一个 router 的实例,并传入一个路由的层级表 routes 数组
图2:
在 new Router 时会传入一个对象,对象含有一个 routes 属性,值是一个数组 ,咱们称这个数组的每一个元素叫作一个路由配置项对象(源码中的类型名叫 RouteConfig),仔细观察能够发现它是一个树形的数据结构,即含有一个 children 数组,数组的元素也是一个路由配置项对象
了解路由配置项对象后,咱们找到源码中对应的 VueRouter 类
图3:
整个 vue-router 实例化的过程核心作了两件事
定义 matcher 属性:经过 createMatcher
建立了一个对象赋值给 matcher
定义 history 属性:根据传入的 mode 属性实例化不一样的 history 路由实例
先来看 matcher 属性,图中第四行会执行到 createMatcher
方法,返回一个 matcher 对象,包含 match
和 addRoutes
这 2 个方法,这 2 个方法是 vue-router 中比较重要的方法,以后咱们会分析它们的做用
在这以前先看一下 createMatcher
函数执行时触发的 createRouteMap
函数
图4:
而 createRouteMap
这个函数就是用来建立路由的映射表的,它是一个记录全部信息(路由记录)的对象,将参数 routes 数组(即上面提到的路由配置项数组)进行一系列处理,生成 pathList,pathMap,nameMap 3张路由映射表
图5:
createRouteMap
内部会遍历 routes 数组,执行 addRouteRecord
方法,将每一个路由配置项对象转为路由记录(RouteConfig类型 -> RouteRecord类型)
接着将获得的路由记录分别再作一些转换,而后储存在 pathList,pathMap,nameMap 3张路由映射表中,咱们先仔细分析 addRouteRecord 方法,再分析这三个映射表的区别
图6:
经过源代码发现,路由记录基于路由配置项对象扩展了一些额外属性,如下是对应的介绍
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:
例如图2中的 comp1Child 这个 route 对象,它的 path 最终会变成
"/comp1" + "/" + "comp1Child" => "/comp1/com1Child"
而最终会生成的路由记录是这样的
图8:
随后由于 routes 不只是一个数组,也是一个树形结构,因此须要进行递归的遍历,而后将路由 对象放入这3个路由映射表中,而这3个路由映射表的区别在于
pathList:数组,保存了 route 对象的路径
pathMap:对象,保存了全部 route 对象对应的 record 对象
nameMap:对象,保存了全部含有name属性的 route 对象对应的 record 对象
图2中的路由对应的3张路由映射表以下:
pathList:
pathMap:
nameMap:
能够看到 pathMap 和 nameMap 几乎是同样的,由于图2中的路由都有 name 属性,假设某个路由没有 name 属性,则只会在 pathMap 中存在
对比保存了路由配置项对象的 routes 数组和这3个路由映射表,咱们能够发现:routes 数组是一个树形结构,而路由映射表是一个扁平的一维结构,经过路由映射表里的 parent 属性来维护父子关系,只因此没有直接在 routes 数组中扩展是由于一维数组可以更方便的找到对应数据,反观树形结构只能递归查找,性能堪忧
根据 routes 生成三个路由映射表后,会向外暴露一个动态添加路由的 API addRoutes
图10:
这个 api 平常开发也遇到过,用于动态注册路由,它的原理其实很简单,就是接受一个 routes 数组,再次调用 createRouteMap
将数组每一个元素转换成路由记录 (RouteRecord) ,而后合并到以前生成的路由映射表中
createMatcher
返回的第二个函数是 match
,match
函数用于建立 $route 对象
图11:
以前说的 route 是针对 new Router 时传入的 routes 数组的每一个元素,也就是路由配置项对象,而 $route 是最终返回做为 Vue.prototype.$route 的对象,在类型定义中,route 的类型是 RouteConfig,而 $route 的类型是 Route,具体接口的定义能够查看源代码,虽然在源码中二者变量名都是 route,但我下文会使用 $route 来区分最终返回的 route 对象
图12:
route(路由配置项) :
$route(当前页面的路由对象) :
前者表示的是路由的一些基础配置项,然后者是真正通过 vue-router 处理后表示当前页面的路由对象
每次路由跳转的时候都会执行这个 match
函数从新生成一个 $route 对象,具体何时会触发 match
放到下篇中讲,这章先分析 match
函数是如何最终生成一个真正的 $route 对象的
match
函数首先会执行 normalizeLocation
函数,它是一个辅助函数,会将调用 router.push / router.replace 时跳转的路由地址转为一个 location 对象
this.$router.push("/login") // 路由地址即 "/login",还多是一个对象 {path: "/login"}
复制代码
那什么是 location 对象? MDN 上是这么解释的
Location
接口表示其连接到的对象的位置(URL)。所作的修改反映在与之相关的对象上。Document
和Window
接口都有这样一个连接的Location,分别经过Document.location
和Window.location
访问。
通俗的来讲就是用一个对象来描述当前 url 的一些信息。当咱们在地址栏中输入 www.baidu.com
,按 F12 打开控制台,输入 loaction 就能展现出当前地址的一些信息
图13:
vue-router 在 location 接口的基础上作了一些加强,添加了 name,path,hash 等 vue-router 特有的属性
举个例子,当调用 router.push({name:"comp1"})
使用 name 的形式进行路由跳转时,返回的 loaction 对象就会有一个 name 属性,当 name 存在时,会走到图11中的 true 逻辑,从以前 createMatcher
生成的 nameMap 路由映射表中找到对应 name 的路由记录对象,最终会执行 _createRoute
这个方法
而调用 router.push("/comp1")
使用路径的形式进行路由跳转,一样也会返回一个 location 对象,但不会有 name 属性,走图11的 false 逻辑,从 pathMap 和 pathList 中找到对应的路由记录对象,最终也会执行 _createRoute
这个方法
可见不管使用 name 跳转仍是使用 path 跳转,最终都会执行 _createRoute
,带下划线的 _createRoute
是一个私有方法,它最终会调用 createRoute
生成 $route 对象
图14:
通过对一些 query 参数的处理,最终返回 $route 对象,其中有一个 matched 属性值得注意,它经过 formatMatch
函数生成,有朋友打印过 $route 返回值的话应该知道,matched 是一个数组,每一个元素都是一个路由记录
图15:
还记得以前在生成路由记录的时定义的 parent 属性吗?它的其中一个用途就是经过不断的向上查找父级的路由记录,放入 matched 数组中,最终返回一个保存了当前路由记录和全部父级数组,顺序是 父 => 子
图16 $route 对象:
而这个 matched 数组最终会决定触发哪些路由组件的哪些路由守卫钩子,关于路由钩子部分咱们放到下篇来讲
路由配置项(RouteConfig),路由记录(RouteRecord),$route(Route)的区别在于
这里画了一张流程图来表达实例化 vue-router 时的 matcher 属性内部的依赖关系
再次回到图3,vue-router 根据传入参数的 mode 属性来实例化不一样的路由类(HTML5,hash,abstract),这也是官方提供给开发者的3种不一样的选择来生成路由
HTML5 路由是相对比较美观的一种路由,和正常的 url 显示没有什么区别,核心依靠 pushState
和 replaceState
来实现不向后端发送请求的路由跳转,可是这相似一层“假装”,当用户点击刷新按钮时就会暴露,最后仍是会发送请求,致使找不到页面的状况,因此须要配合 nginx 来实现找不到页面时返回主页的操做
hash 路由是默认使用的路由,在 url 中会存在一个 # 号,核心依靠这个 # 号也就是曾经做为路由的锚点来实现不向后端发送请求的路由跳转
abstract 路由是一种抽象路由,通常用在非浏览器端,维护一种抽象的路由结构,使得可以嫁接在客户端或者服务端等没有 history 路由的地方
vue-router 会以后根据 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 的全局组件,以及组件相关的视图更新