在之前的一篇博文中主要阐述了前端权限控制的一种实现 —— 前端权限控制的基本实现。其中介绍了通过权限过滤实现动态地私有路由添加,那么在当前用户登出时,应该是要重置当前应用的用户数据的。那么全局的 vuex
状态可通过官方替换 store
的方法 replaceState 来实现。那么在没有官方实现的 feature
的情况下该如何删除(重置)通过 addRoutes 方法添加的动态路由?
TL;DR
起初,只有通过调用全局的原生 location.refresh 方法来实现整个页面的刷新,进而实现当前路由的重置。那么会有一种在不刷新当前页面的方法实现当前路由的重置功能么?在经过一系列的尝试,在官方源码存在这样一个 issue
—— feature request: replace routes dynamically,其中提到了一种
hack
的方法,通过替换路由实例的 matcher 对象来实现路由的 重置,即实现删除通过 addRoutes 添加的路由。
那么截至现在就有两种解决方案可实现路由的删除:
-
通过调用全局的 location.refresh 方法来实现应用刷新来实现前端路由的重置。
-
通过替换当前
vue-router
的matcher
属性对象来实现在不刷新页面的情况下重置当前路由实例。
下文将着重从与 matcher
相关的 vue-router 源码解读为什么替换 vue-router 的 matcher 对象可实现删除 addRoutes 添加的路由。另外截至本文写作日期,vue-router 的最新版本为 v3.0.6。后文所述的所有源码解读都是基于 此 v3.0.6 版本。
首先在 src/index.js 中可见 vue-router
类,其中包含了一系列我们熟知的 router API
。这里尤其要注意与本文相关联的 VueRouter
的 构造函数 constructor 和 原型方法 addRoutes。
后文的内容都是基于这个两个点展开直至解决我们的核心问题 —— 为什么替换实例的 matcher
可实现 删除 由 addRoutes 添加的路由。
先问是什么,再问为什么。
matcher 是什么
在源码的 src/index.js 入口文件中的 VueRouter
类的构造函数中可见,路由实例的 matcher
对象由 create-matcher
中的内部方法 createMatcher 创建 而来。
通过对 VueRouter
类的代码抽象显而易见:
-
在实例化路由对象时,会创建一个与当前路由实例对应的
matcher
。并在实例化时,传入在实例化时的 routes 参数。这里 留心 这里传入的 routes 参数,后续的源码分析也会用到该 routes 参数。 -
在我们之前在 前端权限控制的基本实现 中用到的 addRoutes 方法中,本质上是调用了
matcher
的 addRoutes 方法。
不论是 VueRouter
实例化还是通过 addRoutes 方法,都绕不开 matcher
。那么再次深入 create-matcher.js 源码文件中,可见以下代码:
这里暂时性省略了其他无关代码,关注我们之前的问题 —— matcher
是什么。在这里可以很明显地看出,实例的 matcher
对象是由 match
属性和 addRoutes
属性组成。接下来我们进一步探究二者的本质。
这里先提一点,探究源码的时候最好是带着一个具体的问题来看源码,来理解其中的代码逻辑,这样才不至于在源码中迷失,以保持自己的初心。
全局路由的存储
在上一章节,已经提到不论是 路由实例化 还是调用动态添加路由的 API
—— addRoutes。都会调用到一个 createMatcher 函数。这里我们将分步讨论以下
createMatcher 函数 到底具有什么样的职责。
在查看 create-matcher.js 时,我们可见第一句代码:
这句代码我们大致从调用的函数名称上 推测 一下,在执行 createMatcher 函数 时,首要任务时对当前开发者传入的 options.routes 进行路由
映射化处理,并得到了三个路由容器 pathList
、pathMap
、nameMap
。
在 create-matcher.js 的同级目录下我们可以找到 createRouteMap 所在的文件 create-route-map.js,我们同样可将 createRouteMap 的逻辑展示如下:
同样可知,createRouteMap 函数返回三个属性 —— pathList
、pathMap
、nameMap
。
pathList
,作者已经给出了注释说明,pathList
是用来保证进行非命名路由时的path
匹配优先级的(具体可查看文档 —— 匹配优先级)。
在进一步探究 addRouteRecord 时,我们可以发现 addRouteRecord 就主要做了三件事,将所有之前实例化时传入的 options.routes 格式化
-
对其中每一个
route
做映射,该映射集合被称为pathMap
。同时在映射时,不断向pathList
列表加入当前path
记录以保证匹配路由时的优先级。 -
对提供了
name
字段的路由记录,加入到nameMap
映射中。因为name
具有唯一性,所以此时在nameMap
中就不用考虑匹配优先级了。 -
递归所有路由的子路由,并进行映射化处理。
具体代码解析如下:
现在我们可以大致 总结 一下,所有之前我们通过 VueRouter
构造函数所传递的参数 最终 都在 addRouteRecord 函数中得到解析处理。
-
所有的路由信息
path
字段都存储在pathMap
中,通过pathList
列表实现 匹配优先级。 -
若存在路由有
name
字段时,该命名路由将被存储在nameMap
映射容器中,因为文档中约定了name
字段具有唯一性,那么命名路由没有专门的nameList
来实现匹配优先级。 -
所有路由的每一项子路由都会递归进行解析并存储。
现在我们理解了所有路由信息的最终归宿之后,回溯之前的解析可以发现:
以上代码展示了传入的 options.routes 的格式化存储过程,最终所有的路由信息都在 addRouteRecord 被解析,并存储在 pathList
和
pathMap
中,若是命名路由,另外还会被存储在 nameMap
中。在构造函数中可见,这一切的路由信息容器都是被挂载在路由实例的 matcher
对象上的。这一点,对于后续的动态路由删除功能提供了契机。
matcher.match 函数
之前我们知道了 matcher
对象由 match
方法和 addRoutes
组成的。我们大致看一下 match
属性是指什么,在 create-matcher.js 中的 26 - 72 行就是我们找的 match
属性——它是一个函数。
这里直接将 match
函数的大致脉络抽象为以上结构,经过抽象后的代码可轻易看出 match
函数的主要定位是 vue-router
的 路由匹配模块。一切路由的匹配都是依赖于实例的 matcher.match
函数。这里我们主要是要探究替换 matcher
为什么可以实现路由重置,将不对 路由匹配模块 做深入探究,如果读者感兴趣的话,可以从这个函数开始开起,自己可以尝试着探究 vue-router
的路由匹配模块。稍微提示一下,整个 vue-router
实例都是基于命名路由的 name
字段的 nameMap
映射 用于命名路由的路由匹配,使用非命名路由的 pathList
列表 保证路由匹配的优先级和使用 pathMap
映射 来实现路由的匹配的。这里的匹配搜索原理和作者之前的 前端权限控制的基本实现 数据搜索原理都是基于 映射 这种数据结构。
本文所述的映射是指的一种抽象化的 key-value 数据结构。每一个唯一个 key 都有一个唯一的 value 值与之对应。在 JS 中,一个朴素对象或一个 Map 实例对象都可称为映射。
动态添加路由的实现
在前文中已经提到我们外部调用路由实例的 addRoutes 方法 本质 上就是调用了 match.addRoutes 方法实现 路由的动态添加。
回到之前的 create-matcher.js 中的 create-matcher
函数,在 22 - 24 行可看到 addRoutes
同样是一个函数,并且我们还知道了 addRoutes
的本质就是调用了前文所述的 createRouteMap
函数,我们对之前全局静态路由的解析存储流程有了理解之后就不难理解,调用 createRouteMap 函数本质上就是 向当前路由实例的路由容器动态地添加路由。
由上源码可知,调用路由实例的 addRoutes 方法本质上是调用了路由实例的 addRoutes
方法,该方法在其内部完成了传入的 addRoutes 的路由解析并进行映射化处理。
结论
在我们明白了 addRoutes 是如何向当前路由实例 动态地添加路由 后,我们再结合之前的路由实例化中的路由映射化处理流程可知:
替换当前路由实例的 matcher
之所以能实现删除动态添加的路由,是因为替换当前路由的 matcher
本质 上是 替换了现有的路由实例的路由映射容器。新的 matcher
始终 仅仅 包含路由实例化时的路由,而 不会包含 后期被 addRoutes 方法添加的路由,那么替换当前路由的 matcher
就可实现删除通过 addRoutes
添加的路由。