Matcher

createRouterMatcher是vue-router中的一个重要函数,它用于创建路由匹配器(route matcher)。

类型

export interface RouterMatcher {
  addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void
  removeRoute: {
    (matcher: RouteRecordMatcher): void
    (name: RouteRecordName): void
  }
  getRoutes: () => RouteRecordMatcher[]
  getRecordMatcher: (name: RouteRecordName) => RouteRecordMatcher | undefined

  /**
   * @param location - MatcherLocationRaw to resolve to a url
   * @param currentLocation - MatcherLocation of the current location
   */
  resolve: (
    location: MatcherLocationRaw,
    currentLocation: MatcherLocation
  ) => MatcherLocation
}

主要作用

其主要作用是:

  • 接收 routes 配置和 options 参数
  • 调用 addRoute 递归处理配置,生成路由记录的匹配器 matcher
  • 保存命名路由与 matcher 映射在 matcherMap 中
  • 按得分排序生成 matcher 数组 matchers
  • 创建路由解析方法 resolve
  • 返回一个对象,包含以下方法:
    • addRoute: 添加路由记录
    • resolve: 解析路由位置
    • removeRoute: 删除路由记录
    • getRoutes: 获取路由记录列表
    • getRecordMatcher: 获取单个路由记录的匹配器
需要注意的是
  • matcher 对象仅在 createRouter 内部被创建和使用
  • matcher 提供了后续路由匹配与解析的能力
  • router 实例内部调用 matcher 的逻辑主要通过闭包实现,并没有通过实例属性访问

matchers

这是一个 RouteRecordMatcher 数组,存储经过处理的路由记录匹配器。 通过调用 addRoute 方法递归处理路由配置后,生成的匹配器会插入这个数组,并按得分排序。 这使得后续可以通过数组顺序搜索快速匹配路由。

matcherMap

这是一个 Map 对象, key 是路由名称,value 是对应的 RouteRecordMatcher。 在添加拥有 name 的路由记录时,会同时保存到这个 Map 中。 这使得后续可以直接通过路由名称快速获取对应的匹配器。

所以:

  • matchers 数组支持通过顺序搜索快速匹配路由路径。
  • matcherMap 通过名称索引路由记录,用于快速获取命名路由的匹配器。

两者共同组成了路由匹配的索引系统,使得路由解析可以快速高效地完成。 它们充分利用了添加路由阶段构建的路由匹配元数据,为后续的匹配与导航提供了基础数据支持。

insertMatcher()

insertMatcher是createRouterMatcher中的一个辅助方法,主要作用是将生成的 RouteRecordMatcher 插入到 matchers 数组中。

主要逻辑:

  • 定义插入位置索引 i,初始为 0
  • 遍历 matchers,如果新 matcher 的得分更高,则 i++
  • 在 i 位置插入新 matcher
  • 如果路由记录有名称且不是别名记录,则将名称和 matcher 映射保存到 matcherMap 中

可以看出,insertMatcher 主要完成了以下工作:

  • 根据 matcher 的优先级计算插入位置
  • 将 matcher 按顺序插入 matchers 数组中
  • 维护 matcherMap 的映射关系

这样可以保证:

  • matchers 中的 matcher 顺序按得分排序
  • matcherMap 只包含原始记录的映射

insertMatcher 被 addRoute 在生成 matcher 后调用,按顺序将新 matcher 插入已有序列中。

它维护了 matchers 的顺序性,也通过 matcherMap 维护了命名路由的快速映射。 insertMatcher 是维护路由匹配元数据的重要辅助方法,使得后续的路由匹配可以高效进行。

addRoute()

addRoute是createRouterMatcher中的一个重要方法,主要用于处理路由配置,生成对应的路由记录(RouteRecord)以及路由匹配器( RouteRecordMatcher)。

主要逻辑:

  • 标准化路由记录
  • 生成对应的matcher
  • 处理嵌套子路由
  • 保存命名记录与matcher的映射
  • 插入matcher到有序数组

详细逻辑:

  • 接收原始路由记录record,父匹配器parent和原始匹配器originalRecord
  • 使用normalizeRouteRecord标准化record
  • 处理record的alias生成normalizedRecords
  • 遍历normalizedRecords生成matcher
  • 如果有parent,拼接子路由的路径
  • 使用createRouteRecordMatcher生成matcher
  • 如果是alias,关联到originalMatcher
  • 如果是顶级添加,保存映射到matcherMap
  • 递归处理子路由记录
  • 将非空matcher插入matchers数组
  • 返回移除路由的函数

源码如下:

/**
 * 添加路由记录
 * @param record 路由配置
 * @param parent 父级路由匹配器
 * @param originalRecord 原始匹配器
 */
function addRoute(record: RouteRecordRaw, parent?: RouteRecordMatcher, originalRecord?: RouteRecordMatcher) {
  const isRootAdd = !originalRecord
  // 调用 normalizeRouteRecord 标准化路由配置
  const mainNormalizedRecord = normalizeRouteRecord(record)
  if (__DEV__) {
    checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent)
  }
  mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
  const options: PathParserOptions = mergeOptions(globalOptions, record)

  const normalizedRecords: (typeof mainNormalizedRecord)[] = [mainNormalizedRecord]
  // 处理 record 的 alias 生成 normalizedRecords
  if ('alias' in record) {
    const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias!
    for (const alias of aliases) {
      normalizedRecords.push(assign({}, mainNormalizedRecord, {
          components: originalRecord ? originalRecord.record.components : mainNormalizedRecord.components,
          path: alias,
          aliasOf: originalRecord ? originalRecord.record : mainNormalizedRecord,
        }) as typeof mainNormalizedRecord
      )
    }
  }

  let matcher: RouteRecordMatcher
  let originalMatcher: RouteRecordMatcher | undefined

  // 遍历 normalizedRecords 生成 matcher
  for (const normalizedRecord of normalizedRecords) {
    const { path } = normalizedRecord
    // 如果有 parent, 拼接子路由的路径
    if (parent && path[0] !== '/') {
      const parentPath = parent.record.path
      const connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/'
      normalizedRecord.path = parent.record.path + (path && connectingSlash + path)
    }

    if (__DEV__ && normalizedRecord.path === '*') {
      throw new Error('Catch all routes ("*") must now be defined using a param with a custom regexp.\n' + 'See more at https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.')
    }

    // 使用 createRouteRecordMatcher 生成 matcher
    matcher = createRouteRecordMatcher(normalizedRecord, parent, options)

    if (__DEV__ && parent && path[0] === '/')
      checkMissingParamsInAbsolutePath(matcher, parent)

    if (originalRecord) {
      originalRecord.alias.push(matcher)
      if (__DEV__) {
        checkSameParams(originalRecord, matcher)
      }
    } else {
      originalMatcher = originalMatcher || matcher
      if (originalMatcher !== matcher)
        originalMatcher.alias.push(matcher)

      if (isRootAdd && record.name && !isAliasRecord(matcher))
        removeRoute(record.name)
    }

    // 递归处理子路由记录
    if (mainNormalizedRecord.children) {
      const children = mainNormalizedRecord.children
      for (let i = 0; i < children.length; i++) {
        addRoute(children[i], matcher, originalRecord && originalRecord.children[i])
      }
    }

    originalRecord = originalRecord || matcher

    if ((matcher.record.components && Object.keys(matcher.record.components).length) || matcher.record.name || matcher.record.redirect) {
      insertMatcher(matcher)
    }
  }

  // 返回移除路由的函数
  return originalMatcher ? () => {
    removeRoute(originalMatcher!)
  } : noop
}

resolve()

resolve方法是createRouterMatcher中非常重要的路由解析逻辑

主要逻辑:

  • 尝试从name、path、currentLocation匹配位置信息
  • 解析出对应matcher
  • 生成完整的匹配链表
  • 返回包含matcher及参数信息的location对象

详细逻辑:

  • 接收原始的location和当前位置currentLocation
  • 如果location有name,从matcherMap获取对应matcher
  • 合并参数,生成路径path
  • 如果只有path,用matchers匹配路径获取matcher
  • 如果仍未匹配则抛出MATCHER_NOT_FOUND的错误
  • 从matcher向上递归生成matched匹配链表
  • 返回解析后的location对象

源码如下:

/**
 * 路由解析的核心方法
 * @param location 原始的location
 * @param currentLocation 当前位置
 */
function resolve(location: Readonly<MatcherLocationRaw>, currentLocation: Readonly<MatcherLocation>): MatcherLocation {
  let matcher: RouteRecordMatcher | undefined
  let params: PathParams = {}
  let path: MatcherLocation['path']
  let name: MatcherLocation['name']

  // 如果location有name, 从matcherMap获取对应matcher
  if ('name' in location && location.name) {
    matcher = matcherMap.get(location.name)

    if (!matcher)
      throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, { location, })

    if (__DEV__) {
      const invalidParams: string[] = Object.keys(location.params || {}).filter(paramName => !matcher!.keys.find(k => k.name === paramName))

      if (invalidParams.length) {
        warn(`Discarded invalid param(s) "${invalidParams.join('", "')}" when navigating. See https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md#414-2022-08-22 for more details.`)
      }
    }

    name = matcher.record.name
    // 合并参数
    params = assign(paramsFromLocation(currentLocation.params, matcher.keys.filter(k => !k.optional).map(k => k.name)), location.params && paramsFromLocation(location.params, matcher.keys.map(k => k.name)))
    // 生成路径path
    path = matcher.stringify(params)
  } else if ('path' in location) {
    // 没有name,有path
    path = location.path

    if (__DEV__ && !path.startsWith('/')) {
      warn(`The Matcher cannot resolve relative paths but received "${path}". Unless you directly called \`matcher.resolve("${path}")\`, this is probably a bug in vue-router. Please open an issue at https://github.com/vuejs/router/issues/new/choose.`)
    }

    // 用matchers匹配路径获取matcher
    matcher = matchers.find(m => m.re.test(path))
    if (matcher) {
      params = matcher.parse(path)!
      name = matcher.record.name
    }
  } else {
    // 如果两者都没有,以currentLocation匹配
    matcher = currentLocation.name ? matcherMap.get(currentLocation.name) : matchers.find(m => m.re.test(currentLocation.path))

    // 如果仍未匹配则报错
    if (!matcher)
      throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, { location, currentLocation })
    name = matcher.record.name
    params = assign({}, currentLocation.params, location.params)
    path = matcher.stringify(params)
  }

  // 从matcher向上递归生成matched匹配链表
  const matched: MatcherLocation['matched'] = []
  let parentMatcher: RouteRecordMatcher | undefined = matcher
  while (parentMatcher) {
    matched.unshift(parentMatcher.record)
    parentMatcher = parentMatcher.parent
  }

  // 返回解析后的location对象
  return {
    name,
    path,
    params,
    matched,
    meta: mergeMetaFields(matched),
  }
}

removeRoute()

removeRoute方法是createRouterMatcher中的路由删除逻辑,它可以用于删除已添加的路由记录。

主要逻辑:

  • 根据名称或匹配器引用删除
  • 从matchers数组和matcherMap中删除
  • 递归删除相关联的匹配器

详细逻辑:

  • 判断输入是路由名称还是路由记录匹配器
  • 如果是名称,从matcherMap中取出对应匹配器
  • 从matchers数组中删除匹配器
  • 如果有名称,从matcherMap中删除键值对
  • 递归删除匹配器的子匹配器和别名匹配器

这样就可以完整删除之前addRoute添加的路由记录了。removeRoute与addRoute相反,在router动态删除路由时调用,用于在matcher内部同步删除不再使用的路由匹配器。 它会递归删除所有相关联的匹配器,保证matcher内部状态的精确一致。

源码如下:

/**
 * 删除已添加的路由记录
 * @param matcherRef
 */
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
  // 根据名称删除
  if (isRouteName(matcherRef)) {
    const matcher = matcherMap.get(matcherRef)
    if (matcher) {
      matcherMap.delete(matcherRef)
      matchers.splice(matchers.indexOf(matcher), 1)
      // 递归删除相关联的匹配器
      matcher.children.forEach(removeRoute)
      matcher.alias.forEach(removeRoute)
    }
  } else {
    // 根据匹配器索引删除
    const index = matchers.indexOf(matcherRef)
    if (index > -1) {
      matchers.splice(index, 1)
      if (matcherRef.record.name)
        matcherMap.delete(matcherRef.record.name)
      // 递归删除相关联的匹配器
      matcherRef.children.forEach(removeRoute)
      matcherRef.alias.forEach(removeRoute)
    }
  }
}

getRoutes()

getRoutes是createRouterMatcher中的一个简单方法,用于获取所有已添加的路由记录匹配器。

getRoutes方法的实现很简单:

function getRoutes() {
  return matchers
}

它直接返回了内部存储匹配器的matchers数组。 这样使用getRoutes就可以获取到所有的路由匹配器集合。

getRoutes的作用主要有:

  • 插件可以通过该方法获取所有的路由配置
  • 可以在开发环境用于输出路由状态做调试
  • 在服务端渲染时,可以获取路由状态串行化
  • 可以通过返回的匹配器重新调用match和resolve

所以 getRoutes 为外部提供了获取路由内部状态的途径。它将内部的 matchers 数组直接返回,暴露出所有的路由匹配器。

这可以帮助开发调试或者重新处理路由匹配。是 createRouterMatcher 对外提供状态的一个简单访问点。

getRecordMatcher()

getRecordMatcher是createRouterMatcher中获取单个路由记录匹配器的方法。

其定义为:

function getRecordMatcher(name: RouteRecordName) {
  return matcherMap.get(name)
}

它接受一个路由名称,从内部的matcherMap中获取对应记录的匹配器。 matcherMap在添加拥有name的路由记录时,会一并保存路由名称和匹配器的映射。

getRecordMatcher利用了这个索引表,可以直接通过名称查找对应路由的匹配器。 相比getRoutes返回所有的匹配器, getRecordMatcher只获取单一匹配器。

它提供了一种根据路由名称获取匹配器的途径。

主要用于:

  • 通过路由名称直接获取对应路由状态
  • 在 NavigationGuard 中匹配名称获取路由进行校验
  • 在服务端渲染时,只获取某一路由的匹配器

所以getRecordMatcher通过路由名称访问匹配器,是一种更精细的路由状态获取方式。它基于matcherMap提供了高效的命名路由匹配器查询。