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
}

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

相关推荐
m0_748240251 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar1 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人2 小时前
前端知识补充—CSS
前端·css
GISer_Jing2 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245522 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v2 小时前
webpack最基础的配置
前端·webpack·node.js
pubuzhixing2 小时前
开源白板新方案:Plait 同时支持 Angular 和 React 啦!
前端·开源·github
2401_857600952 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_857600952 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL2 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js