Vue Router源码分析(二)-- createMatcher

Vue Router源码分析(二)-- createMatcher

果然还是很在意这个matcher,这个显眼包在上一篇阅读createRouter时,时不时在我眼前晃一下。算了咯,再不乐意看也得看看咯。

1. RouterMatcher

照例先看类型接口,这样才晓得这家伙有几斤几两。

先是增删查与解析呗,Router几个方法addRouteremoveRoutegetRoutesresolve都是会调用RouterMatcher对应的方法。喔哦,这还有个getRecordMatcher,看签名是通过路由名称来查找matcher,还挺会玩儿。

typescript 复制代码
export interface RouterMatcher {
  addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void
  removeRoute: {
    (matcher: RouteRecordMatcher): void
    (name: RouteRecordName): void
  }
  getRoutes: () => RouteRecordMatcher[]
  // 通过路由名称来查找`matcher`
  getRecordMatcher: (name: RouteRecordName) => RouteRecordMatcher | undefined
​
  /**
   * Resolves a location. Gives access to the route record that corresponds to the actual path as well as filling the corresponding params objects
   *
   * @param location - MatcherLocationRaw to resolve to a url
   * @param currentLocation - MatcherLocation of the current location
   */
  resolve: (
    location: MatcherLocationRaw,
    currentLocation: MatcherLocation
  ) => MatcherLocation
}

2. createRouterMatcher

这才是今天的主角呀,还好加上注释也才300行,不然我真的会吐。

入参:

  • routes:就是初始的路由列表呗;
  • globalOptions:天晓得这个全局选项是干啥子的嘞。看了眼上一篇,哦,原来是createRouter的选项参数options

接下来嘛,还是熟悉的配方,还是原来的味道。先把类型接口需要的方法都定义出来,然后组装在对象里返回。这套路不能说和createRouter毫无瓜葛,简直就是一模一样。那就排好队,一个一个来呗。

整了几个常量,虽然不明白为什么弄个matchers数组的同时,还要来个matcherMap,但是感觉似乎有点东西,后文应该会有答案吧。

php 复制代码
/**
 * Creates a Router Matcher.
 *
 * @internal
 * @param routes - array of initial routes
 * @param globalOptions - global route options
 */
export function createRouterMatcher(
  routes: Readonly<RouteRecordRaw[]>,
  globalOptions: PathParserOptions
): RouterMatcher {
  // normalized ordered array of matchers
  const matchers: RouteRecordMatcher[] = []
  const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
  globalOptions = mergeOptions(
    { strict: false, end: true, sensitive: false } as PathParserOptions,
    globalOptions
  )
    
  // 省略中间的函数体 ...
​
  // add initial routes
  // 添加初始路由
  routes.forEach(route => addRoute(route))
  return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}

下面就是函数体的内容咯。

2.1 getRecordMatcher

getRecordMatcher就是以路由名称为keymatcherMap中取出matcher

csharp 复制代码
​
​
  function getRecordMatcher(name: RouteRecordName) {
    return matcherMap.get(name)
  }

2.2 addRoute

router.addRoute只接收两个参数,而matcher.addRoute却有三个参数,那就是说matcher.addRoute还和别的函数有一腿咯。呸,海王!而且这个addRoute内容是真不少啊,那就挑重点了。

  • 开局一个常量isRootAdd,标记是否是添加在根路由下,后续会根据它来判断是否需要移除路由以防止多余的嵌套;

  • 将参数record处理为标准化的路由记录mainNormalizedRecord

  • 合并全局选项globalOptions和当前记录,得到新的选项options

  • 生成数组normalizedRecords来处理record的别名;

  • 处理normalizedRecords各成员的path,如果某个成员的path为通配符*,则会在dev环境下进行报错提示;

  • 调用createRouteRecordMatcher创建matcher

  • 如果存在原始的记录originalRecord,则当前为别名记录,需要放入原始记录的别名数组中;

  • 否则,normalizedRecords中的第一个成员就是原始记录,其余成员为别名;

  • 遇到顶层有名称的原始记录,则根据记录的名称来移除;

  • 递归mainNormalizedRecord.children

  • 通过insertMatcher来实际添加路由,只有matcher.record同时满足以下两个条件才会被添加:

    1. components中至少有一个component
    2. name或者redirect
csharp 复制代码
function addRoute(
    record: RouteRecordRaw,
    parent?: RouteRecordMatcher,
    originalRecord?: RouteRecordMatcher
  ) {
    // used later on to remove by name
    const isRootAdd = !originalRecord
    const mainNormalizedRecord = normalizeRouteRecord(record)
    if (__DEV__) {
      checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent)
    }
    // we might be the child of an alias
    mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
    const options: PathParserOptions = mergeOptions(globalOptions, record)
    // generate an array of records to correctly handle aliases
    const normalizedRecords: (typeof mainNormalizedRecord)[] = [
      mainNormalizedRecord,
    ]
    if ('alias' in record) {
      const aliases =
        typeof record.alias === 'string' ? [record.alias] : record.alias!
      for (const alias of aliases) {
        normalizedRecords.push(
          assign({}, mainNormalizedRecord, {
            // this allows us to hold a copy of the `components` option
            // so that async components cache is hold on the original record
            components: originalRecord
              ? originalRecord.record.components
              : mainNormalizedRecord.components,
            path: alias,
            // we might be the child of an alias
            aliasOf: originalRecord
              ? originalRecord.record
              : mainNormalizedRecord,
            // the aliases are always of the same kind as the original since they
            // are defined on the same record
          }) as typeof mainNormalizedRecord
        )
      }
    }
​
    let matcher: RouteRecordMatcher
    let originalMatcher: RouteRecordMatcher | undefined
​
    for (const normalizedRecord of normalizedRecords) {
      const { path } = normalizedRecord
      // Build up the path for nested routes if the child isn't an absolute
      // route. Only add the / delimiter if the child path isn't empty and if the
      // parent path doesn't have a trailing slash
      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.'
        )
      }
​
      // create the object beforehand, so it can be passed to children
      matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
​
      if (__DEV__ && parent && path[0] === '/')
        checkMissingParamsInAbsolutePath(matcher, parent)
​
      // if we are an alias we must tell the original record that we exist,
      // so we can be removed
      if (originalRecord) {
        originalRecord.alias.push(matcher)
        if (__DEV__) {
          checkSameParams(originalRecord, matcher)
        }
      } else {
        // otherwise, the first record is the original and others are aliases
        originalMatcher = originalMatcher || matcher
        if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
​
        // remove the route if named and only for the top record (avoid in nested calls)
        // this works because the original record is the first one
        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]
          )
        }
      }
​
      // if there was no original record, then the first one was not an alias and all
      // other aliases (if any) need to reference this record when adding children
      originalRecord = originalRecord || matcher
​
      // TODO: add normalized records for more flexibility
      // if (parent && isAliasRecord(originalRecord)) {
      //   parent.children.push(originalRecord)
      // }
​
      // Avoid adding a record that doesn't display anything. This allows passing through records without a component to
      // not be reached and pass through the catch all route
      if (
        (matcher.record.components &&
          Object.keys(matcher.record.components).length) ||
        matcher.record.name ||
        matcher.record.redirect
      ) {
        insertMatcher(matcher)
      }
    }
​
    return originalMatcher
      ? () => {
          // since other matchers are aliases, they should be removed by the original matcher
          removeRoute(originalMatcher!)
        }
      : noop
  }

2.3 removeRoute

这个就比addRoute简单多了嘛。接收的参数如果是name,就用matcherMap去找,如果是matcher,就从matchers里去找下标。

不仅要移除matchersmatcherMap中存储的matcher,还要从二者中递归移除matcher.childrenmatcher.alias中的所有matcher,这不是犯天条了要株连九族嘛。

scss 复制代码
  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)
      }
    }
  }

2.4 getRoutes

就返回matchers数组呗。好,原来这么简单,我这就去告诉别人我学会Vue Router的源码了!

csharp 复制代码
  function getRoutes() {
    return matchers
  }

2.5 insertMatcher

看过addRoute的人都知道,addRoute中调用createRouteRecordMatcher生成matcher,而实际将matcher存起来的是insertMatcher

这下就解决开头的疑问了,matchers数组什么记录都能存,但是matcherMap只存原始记录。

csharp 复制代码
  function insertMatcher(matcher: RouteRecordMatcher) {
    let i = 0
    while (
      i < matchers.length &&
      comparePathParserScore(matcher, matchers[i]) >= 0 &&
      // Adding children with empty path should still appear before the parent
      // https://github.com/vuejs/router/issues/1124
      (matcher.record.path !== matchers[i].record.path ||
        !isRecordChildOf(matcher, matchers[i]))
    )
      i++
    matchers.splice(i, 0, matcher)
    // only add the original record to the name map
    // matcherMap只存储原始记录
    if (matcher.record.name && !isAliasRecord(matcher))
      matcherMap.set(matcher.record.name, matcher)
  }

2.6 resolve

又是个飞流直下三千尺的函数,这么长我可以不看吗?周深再怎么一往情深,也远不及我此刻眉头皱得深。

存在有效的location.name时:

  • 根据参数location.name获取matcher,获取不到则抛出路由错误,且dev环境下会根据不合法的loaction.params来进行告警提示;
  • locationcurrentLocation中得到路由参数params,并字符串化为path

不存在有效的location.name但是存在有效的location.path时:

  • path取自location.path
  • 利用pathmatcher中拿到paramsname

最后,根据parent属性递归matcher,将得到的所有matcher倒序放进matched数组,使得父级位于前面;matched数组会与pathparamsname等属性在同一个对象中被返回。

javascript 复制代码
function resolve(
    location: Readonly<MatcherLocationRaw>,
    currentLocation: Readonly<MatcherLocation>
  ): MatcherLocation {
    let matcher: RouteRecordMatcher | undefined
    let params: PathParams = {}
    let path: MatcherLocation['path']
    let name: MatcherLocation['name']
​
    if ('name' in location && location.name) {
      matcher = matcherMap.get(location.name)
​
      if (!matcher)
        throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
          location,
        })
​
      // warn if the user is passing invalid params so they can debug it better when they get removed
      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 is a new object
        paramsFromLocation(
          currentLocation.params,
          // only keep params that exist in the resolved location
          // only keep optional params coming from a parent record
          matcher.keys
            .filter(k => !k.optional)
            .concat(
              matcher.parent ? matcher.parent.keys.filter(k => k.optional) : []
            )
            .map(k => k.name)
        ),
        // discard any existing params in the current location that do not exist here
        // #1497 this ensures better active/exact matching
        location.params &&
          paramsFromLocation(
            location.params,
            matcher.keys.map(k => k.name)
          )
      )
      // throws if cannot be stringified
      path = matcher.stringify(params)
    } else if (location.path != null) {
      // no need to resolve the path with the matcher as it was provided
      // this also allows the user to control the encoding
      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.`
        )
      }
​
      matcher = matchers.find(m => m.re.test(path))
      // matcher should have a value after the loop
​
      if (matcher) {
        // we know the matcher works because we tested the regexp
        params = matcher.parse(path)!
        name = matcher.record.name
      }
      // location is a relative path
    } else {
      // match by name or path of current route
      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
      // since we are navigating to the same location, we don't need to pick the
      // params like when `name` is provided
      params = assign({}, currentLocation.params, location.params)
      path = matcher.stringify(params)
    }
​
    const matched: MatcherLocation['matched'] = []
    let parentMatcher: RouteRecordMatcher | undefined = matcher
    while (parentMatcher) {
      // reversed order so parents are at the beginning
​
      matched.unshift(parentMatcher.record)
      parentMatcher = parentMatcher.parent
    }
​
    return {
      name,
      path,
      params,
      matched,
      meta: mergeMetaFields(matched),
    }
  }

createMatcher的函数内容到此就走向了终点,剩下的一个未解之谜是matcher的创建过程,而这都藏在createRouteRecordMatcher中。

createRouteRecordMatcher

在观摩createRouteRecordMatcher之前,最好是先了解一下类型接口RouteRecordMatcher,知己知彼。类型接口RouteRecordMatcher继承了PathParser

php 复制代码
export interface PathParser {
  /**
   * The regexp used to match a url
   */
  re: RegExp
​
  /**
   * The score of the parser
   */
  score: Array<number[]>
​
  /**
   * Keys that appeared in the path
   */
  keys: PathParserParamKey[]
  /**
   * Parses a url and returns the matched params or null if it doesn't match. An
   * optional param that isn't preset will be an empty string. A repeatable
   * param will be an array if there is at least one value.
   *
   * @param path - url to parse
   * @returns a Params object, empty if there are no params. `null` if there is
   * no match
   */
  parse(path: string): PathParams | null
​
  /**
   * Creates a string version of the url
   *
   * @param params - object of params
   * @returns a url
   */
  stringify(params: PathParams): string
}
​
export interface RouteRecordMatcher extends PathParser {
  record: RouteRecord
  parent: RouteRecordMatcher | undefined
  children: RouteRecordMatcher[]
  // aliases that must be removed when removing this record
  alias: RouteRecordMatcher[]
}

那么现在可以看一看createRouteRecordMatcher。不看不知道,一看真下头。matcher的一大部分内容都来自于tokensToParser创建的parser,毕竟RouteRecordMatcher继承了PathParser。那这部分就不在本文深究了,不然过长了会让人既渴望又害怕。

csharp 复制代码
export function createRouteRecordMatcher(
  record: Readonly<RouteRecord>,
  parent: RouteRecordMatcher | undefined,
  options?: PathParserOptions
): RouteRecordMatcher {
  const parser = tokensToParser(tokenizePath(record.path), options)
​
  // warn against params with the same name
  if (__DEV__) {
    const existingKeys = new Set<string>()
    for (const key of parser.keys) {
      if (existingKeys.has(key.name))
        warn(
          `Found duplicated params with name "${key.name}" for path "${record.path}". Only the last one will be available on "$route.params".`
        )
      existingKeys.add(key.name)
    }
  }
​
  const matcher: RouteRecordMatcher = assign(parser, {
    record,
    parent,
    // these needs to be populated by the parent
    children: [],
    alias: [],
  })
​
  if (parent) {
    // both are aliases or both are not aliases
    // we don't want to mix them because the order is used when
    // passing originalRecord in Matcher.addRoute
    if (!matcher.record.aliasOf === !parent.record.aliasOf)
      parent.children.push(matcher)
  }
​
  return matcher
}

今天这个代码就看到这里了,剩下的时间要去陪别的知识咯。

相关推荐
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
吹牛不交税3 小时前
admin.net-v2 框架使用笔记-netcore8.0/10.0版
vue.js·.netcore