原文发布于个人 博客, 未经受权禁止转载html
在以前的一篇博文中主要阐述了前端权限控制的一种实现 —— 前端权限控制的基本实现。其中介绍了经过权限过滤实现动态地私有路由添加,那么在当前用户登出时,应该是要重置当前应用的用户数据的。那么全局的 vuex
状态可经过官方替换 store
的方法 replaceState 来实现。那么在没有官方实现的 feature
的状况下该如何删除(重置)经过 addRoutes 方法添加的动态路由?前端
起初,只有经过调用全局的原生 location.refresh 方法来实现整个页面的刷新,进而实现当前路由的重置。那么会有一种在不刷新当前页面的方法实现当前路由的重置功能么?在通过一系列的尝试,在官方源码存在这样一个 issue
—— feature request: replace routes dynamically,其中提到了一种 hack
的方法,经过替换路由实例的 matcher 对象来实现路由的 重置,即实现删除经过 addRoutes 添加的路由。vue
那么截至如今就有两种解决方案可实现路由的删除:git
经过调用全局的 location.refresh 方法来实现应用刷新来实现前端路由的重置。github
经过替换当前 vue-router
的 matcher
属性对象来实如今不刷新页面的状况下重置当前路由实例。vue-router
下文将着重从与 matcher
相关的 vue-router 源码解读为何替换 vue-router 的 matcher 对象可实现删除 addRoutes 添加的路由。另外截至本文写做日期,vue-router 的最新版本为 v3.0.6。后文所述的全部源码解读都是基于 此 v3.0.6 版本。vuex
首先在 src/index.js 中可见 vue-router
类,其中包含了一系列咱们熟知的 router API
。这里尤为要注意与本文相关联的 VueRouter
的 构造函数 constructor 和 原型方法 addRoutes。api
后文的内容都是基于这个两个点展开直至解决咱们的核心问题 —— 为何替换实例的 matcher
可实现 删除 由 addRoutes 添加的路由。数据结构
先问是什么,再问为何。ide
在源码的 src/index.js 入口文件中的 VueRouter
类的构造函数中可见,路由实例的 matcher
对象由 create-matcher
中的内部方法 createMatcher 建立 而来。
// ... other code
import { createMatcher } from './create-matcher'
// ... other code
export default class VueRouter {
// ...
constructor(options: RouterOptions = {}) {
// ...
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
// ... other code
}
// ... other code
addRoutes(routes: Array<RouteConfig>) {
this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
}
复制代码
经过对 VueRouter
类的代码抽象显而易见:
在实例化路由对象时,会建立一个与当前路由实例对应的 matcher
。并在实例化时,传入在实例化时的 routes 参数。这里 留心 这里传入的 routes 参数,后续的源码分析也会用到该 routes 参数。
在咱们以前在 前端权限控制的基本实现 中用到的 addRoutes 方法中,本质上是调用了 matcher
的 addRoutes 方法。
不管是 VueRouter
实例化仍是经过 addRoutes 方法,都绕不开 matcher
。那么再次深刻 create-matcher.js 源码文件中,可见如下代码:
// {projectRoot}/src/create-matcher.js
export function createMatcher( routes: Array<RouteConfig>, router: VueRouter ): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// ...
return {
match,
addRoutes
}
}
复制代码
这里暂时性省略了其余无关代码,关注咱们以前的问题 —— matcher
是什么。在这里能够很明显地看出,实例的 matcher
对象是由 match
属性和 addRoutes
属性组成。接下来咱们进一步探究两者的本质。
这里先提一点,探究源码的时候最好是带着一个具体的问题来看源码,来理解其中的代码逻辑,这样才不至于在源码中迷失,以保持本身的初心。
在上一章节,已经提到不管是 路由实例化 仍是调用动态添加路由的 API
—— addRoutes。都会调用到一个 createMatcher 函数。这里咱们将分步讨论如下 createMatcher 函数 到底具备什么样的职责。
export default class VueRouter {
constructor(options: RouterOptions = {}) {
// ...
this.matcher = createMatcher(options.routes || [], this)
// ...
}
// ...
match(raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
// ...
addRoutes(routes: Array<RouteConfig>) {
this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
// ...
}
复制代码
在查看 create-matcher.js 时,咱们可见第一句代码:
const { pathList, pathMap, nameMap } = createRouteMap(routes)
复制代码
这句代码咱们大体从调用的函数名称上 推测 一下,在执行 createMatcher 函数 时,首要任务时对当前开发者传入的 options.routes 进行路由 映射化处理,并获得了三个路由容器 pathList
、pathMap
、nameMap
。
在 create-matcher.js 的同级目录下咱们能够找到 createRouteMap 所在的文件 create-route-map.js,咱们一样可将 createRouteMap 的逻辑展现以下:
export function createRouteMap( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord> ): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// the path list is used to control path matching priority
const pathList: Array<string> = oldPathList || []
/** * @description Dictionary 泛型 * https://github.com/vuejs/vue-router/blob/v3.0.6/flow/declarations.js#L20 */
// $flow-disable-line
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// $flow-disable-line
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
/** * @description 将全部的 VueRouter 构造函数提供的 routes 分别存入 pathList, * pathMap, nameMap 三个路由容器中。 */
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
/** * @description 由于上文代码注释已经说明 pathList 是用于保证路由匹配的优先级 * 那么,如下代码用于保证路由通配符始终被最后匹配 */
// ensure wildcard routes are always at the end
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return {
pathList,
pathMap,
nameMap
}
}
复制代码
一样可知,createRouteMap 函数返回三个属性 —— pathList
、pathMap
、nameMap
。
pathList
,做者已经给出了注释说明,pathList
是用来保证进行非命名路由时的 path
匹配优先级的(具体可查看文档 —— 匹配优先级)。在进一步探究 addRouteRecord 时,咱们能够发现 addRouteRecord 就主要作了三件事,将全部以前实例化时传入的 options.routes 格式化
对其中每个 route
作映射,该映射集合被称为 pathMap
。同时在映射时,不断向 pathList
列表加入当前 path
记录以保证匹配路由时的优先级。
对提供了 name
字段的路由记录,加入到 nameMap
映射中。由于 name
具备惟一性,因此此时在 nameMap
中就不用考虑匹配优先级了。
递归全部路由的子路由,并进行映射化处理。
具体代码解析以下:
function addRouteRecord( pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) {
const { path, name } = route
// 断言传入的 route 中是否包含 path 属性
if (process.env.NODE_ENV !== 'production') {
assert(path != null, `"path" is required in a route configuration.`)
assert(
typeof route.component !== 'string',
`route config "component" for path: ${String( path || name )} cannot be a ` + `string id. Use an actual component instead.`
)
}
// 有没有提供客制化解析参数
// https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#高级匹配模式
const pathToRegexpOptions: PathToRegexpOptions =
route.pathToRegexpOptions || {}
// 格式化路由,如拼接子路由等
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
// 匹配时,是否对大小写敏感
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
// 路由记录对象
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
// 当前 route 存在子路由时
if (route.children) {
/** * @description 用来解决子路由为空字符串或为 '/' 时,将致使子路由没法渲染的 BUG * 具体可见:https://github.com/vuejs/vue-router/issues/629 */
// Warn if route is named, does not redirect and has a default child route.
// If users navigate to this route by name, the default child will
// not be rendered (GH Issue #629)
if (process.env.NODE_ENV !== 'production') {
if (
route.name &&
!route.redirect &&
route.children.some(child => /^\/?$/.test(child.path))
) {
warn(
false,
`Named Route '${route.name}' has a default child route. ` +
`When navigating to this named route (:to="{name: '${ route.name }'"), ` +
`the default child route will not be rendered. Remove the name from ` +
`this route and use the name of the default child route for named ` +
`links instead.`
)
}
}
/** * @description 递归映射化当前路由的全部子路由 */
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
/** * @description route.alias 表示路由别名。当存在 /a 别名为 /b 时,访问 /b 就像 * 访问 /a 同样,但路由仍是保持为 /b。 * https://router.vuejs.org/guide/essentials/redirect-and-alias.html#alias * https://router.vuejs.org/zh/guide/essentials/redirect-and-alias.html#别名 */
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
aliases.forEach(alias => {
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
})
}
/** * @description 若当前 pathMap 映射容器中不包含 path 路由记录,那么将该路由记录对 * 象添加到 pathMap 容器中。另外 pathList 用来保证匹配的优先级 */
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
/** * @description 若当前路由提供了 name 字段,即当前路由是命名路由时,将在 nameMap * 映射容器中添加该命名路由的路由记录。 */
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
`Duplicate named routes definition: ` +
`{ name: "${name}", path: "${record.path}" }`
)
}
}
}
复制代码
如今咱们能够大体 总结 一下,全部以前咱们经过 VueRouter
构造函数所传递的参数 最终 都在 addRouteRecord 函数中获得解析处理。
全部的路由信息 path
字段都存储在 pathMap
中,经过 pathList
列表实现 匹配优先级。
若存在路由有 name
字段时,该命名路由将被存储在 nameMap
映射容器中,由于文档中约定了 name
字段具备惟一性,那么命名路由没有专门的 nameList
来实现匹配优先级。
全部路由的每一项子路由都会递归进行解析并存储。
如今咱们理解了全部路由信息的最终归宿以后,回溯以前的解析能够发现:
// {ProjectRoot}/src/index.js
import { createMatcher } from './create-matcher'
// ...
export default VueRouter {
constructor (options: RouterOptions = {}) {
this.matcher = createMatcher(options.routes || [], this)
}
}
复制代码
// {ProjectRoot}/src/create-matcher.js
import { createRouteMap } from './create-route-map'
// ...
export function createMatcher( routes: Array<RouteConfig>, router: VueRouter ): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// ...
}
复制代码
// {ProjectRoot}/src/create-route-map.js
export function createRouteMap( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord> ): {
pathList: Array<string>
pathMap: Dictionary<RouteRecord>
nameMap: Dictionary<RouteRecord>
} {
// ...
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
// ...
}
复制代码
以上代码展现了传入的 options.routes 的格式化存储过程,最终全部的路由信息都在 addRouteRecord 被解析,并存储在 pathList
和 pathMap
中,如果命名路由,另外还会被存储在 nameMap
中。**在构造函数中可见,这一切的路由信息容器都是被挂载在路由实例的 matcher
对象上的。**这一点,对于后续的动态路由删除功能提供了契机。
以前咱们知道了 matcher
对象由 match
方法和 addRoutes
组成的。咱们大体看一下 match
属性是指什么,在 create-matcher.js 中的 26 - 72 行就是咱们找的 match
属性——它是一个函数。
function match( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route {
// 经过调用的函数名称可大体推断是格式化当前的路由对象
const location = normalizeLocation(raw, currentRoute, false, router)
// 抽离当前路由对象中的 name 字段,即命名路由中的名称
const { name } = location
// 若当前路由是命名路由时
if (name) {
// ... code
// 当前路由不是命名路由,那么直接进行路由的 path 字段路由匹配
} else if (location.path) {
// ... code
}
// no match
return _createRoute(null, location)
}
复制代码
这里直接将 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 函数本质上就是 向当前路由实例的路由容器动态地添加路由。
// {ProjectRoot}/src/index.js
import { createMatcher } from './create-matcher'
// ...
export default VueRouter {
// ...
addRoutes (routes: Array<RouteConfig>) {
this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
}
复制代码
// {ProjectRoot}/src/create-matcher.js
import { createRouteMap } from './create-route-map'
export function createMatcher( routes: Array<RouteConfig>, router: VueRouter ): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// ...
return {
match,
addRoutes
}
}
复制代码
由上源码可知,调用路由实例的 addRoutes 方法本质上是调用了路由实例的 addRoutes
方法,该方法在其内部完成了传入的 addRoutes 的路由解析并进行映射化处理。
在咱们明白了 addRoutes 是如何向当前路由实例 动态地添加路由 后,咱们再结合以前的路由实例化中的路由映射化处理流程可知:
替换当前路由实例的 matcher
之因此能实现删除动态添加的路由,是由于替换当前路由的 matcher
本质 上是 替换了现有的路由实例的路由映射容器。新的 matcher
始终 仅仅 包含路由实例化时的路由,而 不会包含 后期被 addRoutes 方法添加的路由,那么替换当前路由的 matcher
就可实现删除经过 addRoutes
添加的路由。