vue-router 源码探究——路由重置实现

原文发布于个人 博客, 未经受权禁止转载html

在以前的一篇博文中主要阐述了前端权限控制的一种实现 —— 前端权限控制的基本实现。其中介绍了经过权限过滤实现动态地私有路由添加,那么在当前用户登出时,应该是要重置当前应用的用户数据的。那么全局的 vuex 状态可经过官方替换 store 的方法 replaceState 来实现。那么在没有官方实现的 feature 的状况下该如何删除(重置)经过 addRoutes 方法添加的动态路由?前端

TL;DR

起初,只有经过调用全局的原生 location.refresh 方法来实现整个页面的刷新,进而实现当前路由的重置。那么会有一种在不刷新当前页面的方法实现当前路由的重置功能么?在通过一系列的尝试,在官方源码存在这样一个 issue —— feature request: replace routes dynamically,其中提到了一种 hack 的方法,经过替换路由实例的 matcher 对象来实现路由的 重置,即实现删除经过 addRoutes 添加的路由。vue

那么截至如今就有两种解决方案可实现路由的删除:git

  • 经过调用全局的 location.refresh 方法来实现应用刷新来实现前端路由的重置。github

  • 经过替换当前 vue-routermatcher 属性对象来实如今不刷新页面的状况下重置当前路由实例。vue-router

下文将着重从与 matcher 相关的 vue-router 源码解读为何替换 vue-routermatcher 对象可实现删除 addRoutes 添加的路由。另外截至本文写做日期,vue-router 的最新版本为 v3.0.6。后文所述的全部源码解读都是基于 此 v3.0.6 版本vuex

首先在 src/index.js 中可见 vue-router 类,其中包含了一系列咱们熟知的 router API。这里尤为要注意与本文相关联的 VueRouter构造函数 constructor原型方法 addRoutesapi

后文的内容都是基于这个两个点展开直至解决咱们的核心问题 —— 为何替换实例的 matcher 可实现 删除addRoutes 添加的路由。数据结构

先问是什么,再问为何。ide

matcher 是什么

在源码的 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 方法中,本质上是调用了 matcheraddRoutes 方法。

不管是 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 进行路由 映射化处理,并获得了三个路由容器 pathListpathMapnameMap

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 函数返回三个属性 —— pathListpathMapnameMap

  • pathList,做者已经给出了注释说明,pathList 是用来保证进行非命名路由时的 path 匹配优先级的(具体可查看文档 —— 匹配优先级)。

在进一步探究 addRouteRecord 时,咱们能够发现 addRouteRecord 就主要作了三件事,将全部以前实例化时传入的 options.routes 格式化

  1. 对其中每个 route 作映射,该映射集合被称为 pathMap。同时在映射时,不断向 pathList 列表加入当前 path 记录以保证匹配路由时的优先级。

  2. 对提供了 name 字段的路由记录,加入到 nameMap 映射中。由于 name 具备惟一性,因此此时在 nameMap 中就不用考虑匹配优先级了。

  3. 递归全部路由的子路由,并进行映射化处理。

具体代码解析以下:

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 被解析,并存储在 pathListpathMap 中,如果命名路由,另外还会被存储在 nameMap 中。**在构造函数中可见,这一切的路由信息容器都是被挂载在路由实例的 matcher 对象上的。**这一点,对于后续的动态路由删除功能提供了契机。

matcher.match 函数

以前咱们知道了 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 添加的路由。

相关文章
相关标签/搜索