Vue-Router3源码拾遗【3】matcher匹配器

方法论:只要明白了某个模块的目的或者功能(或者函数签名、函数参数和返回),在多数情况下都不用关心其繁杂的具体实现。

以本文内容为例,只要掌握了核心模块createMatcher函数,及其返回的方法matcher.match 二者的作用和原理,就能理清相关模块的整体架构。

再后面就是枯燥的具体实现。如果你也想实现一个前端路由库并且想参考其源码,后半部分姑且可以当做字段字典来查阅。

该模块的架构总结:

从“函数”的视角入手,掌握关键的函数,用自己的话赋予之“函数签名”,对整个架构的设计的理解就豁然开朗!

【核心模块:】

  • createMatcher 接收 2 个参数,一个是 router,一个是 routes,它是用户定义的路由配置。内容就是根据路由表来创建各种映射表xxxMap, 返回(本身是matcher)参数有 match 和 addRouters。
  • matcher.match 会根据传入的位置和路径计算出新的位置,并匹配到对应的路由 record,然后根据新的位置和 record 创建新的路径并返回。

【还有周边的一些提供支持的模块:】

  • 数据结构模块:Loaction、Route
  • RouteRecord:抽象成数据结构,还是对路由表的数据做点增强,方便用于匹配
  • addRoutes: createMatcher的返回结果:addRoutes 方法的作用是动态添加路由配置,因为在实际开发中有些场景是不能提前把路由写死的,需要根据一些条件动态添加路由,所以 Vue-Router 也提供了这一接口。

后文就是枯燥的具体实现的解释了:

基础数据结构作为前置模块

Loaction 数据结构定义在 flow/declarations.js

declare type Location = {
  _normalized?: boolean;
  name?: string;
  path?: string;
  hash?: string;
  query?: Dictionary<string>;
  params?: Dictionary<string>;
  append?: boolean;
  replace?: boolean;
}

Vue-Router 中定义的 Location 数据结构和浏览器提供的 window.location 部分结构有点类似,它们都是对 url 的结构化描述。举个例子:/abc?foo=bar&baz=qux#hello,它的 path/abcquery{foo:'bar',baz:'qux'}Location 的其他属性我们之后会介绍。

Route 数据结构定义在 flow/declarations.js

declare type Route = {
  path: string;
  name: ?string;
  hash: string;
  query: Dictionary<string>;
  params: Dictionary<string>;
  fullPath: string;
  matched: Array<RouteRecord>;
  redirectedFrom?: string;
  meta?: any;
}

Route 表示的是路由中的一条线路,它除了描述了类似 Loctaionpathqueryhash 这些概念,还有 matched 表示匹配到的所有的 RouteRecordRoute 的其他属性我们之后会介绍。

RouteRecord:还是对路由表的数据做点增强,方便用于匹配

  • path 是规范化后的路径,它会根据 parentpath 做计算
  • regex 是一个正则表达式的扩展,它利用了path-to-regexp 这个工具库,把 path 解析成一个正则表达式的扩展
  • components 是一个对象,通常我们在配置中写的 component 实际上这里会被转换成 {components: route.component}
  • instances 表示组件的实例,也是一个对象类型;
  • parent 表示父的 RouteRecord,因为我们配置的时候有时候会配置子路由,所以整个 RouteRecord 也就是一个树型结构。
 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 }
}

createMatcher 返回的正是 matcher,是做匹配用的

createRouteMap 的定义在src/create-route-map

  • pathList 是为了记录路由配置中的所有 `path
  • pathMapnameMap 都是为了通过 pathname 能快速查到对应的 RouteRecord
  • (由于 pathListpathMapnameMap 都是引用类型,所以在遍历整个 routes 过程中去执行 addRouteRecord 方法,会不断给他们添加数据。)

createMatcher 接收 2 个参数,一个是 router,一个是 routes,它是用户定义的路由配置。**内容就是根据路由表来创建各种映射表xxxMap,**返回(本身是matcher)参数有 match 和 addRouters。

  • createMathcer 把用户的路由配置转换成一张路由映射表
    • const { pathList, pathMap, nameMap } = createRouteMap(routes) 创建一个路由映射表
      • 递归遍历路由配置表,调用 addRouteRecord
        • normalizePath 略
        • (省略)
        • 如果配置了 children,那么递归执行 addRouteRecord 方法,并把当前的 record 作为 parent 传入,通过这样的深度遍历,我们就可以拿到一个 route 下的完整记录。
        • RouteRecord 添加到各种对应 xxxMap
        • alias的逻辑就不看了
      • 通配符*优先级问题
  • match相关的函数和逻辑,最终返回一条 Route 路径

addRoutes: createMatcher的返回结果

addRoutes 方法的作用是动态添加路由配置,因为在实际开发中有些场景是不能提前把路由写死的,需要根据一些条件动态添加路由,所以 Vue-Router 也提供了这一接口。

function addRoutes (routes) {
  createRouteMap(routes, pathList, pathMap, nameMap)
}

addRoutes 的逻辑十分简单:再次调用 createRouteMap 即可,传入新的 routes 配置。由于 pathListpathMapnameMap 都是引用类型,执行 addRoutes 后会修改它们的值。

match: createMatcher的返回结果

match 方法接收 3 个参数,其中 rawRawLocation 类型,它可以是一个 url 字符串,也可以是一个 Location 对象;currentRouteRoute 类型,它表示当前的路径;redirectedFrom 和重定向相关,这里先忽略。

match 方法返回的是一个路径,它的作用是根据传入的 raw 和当前的路径 currentRoute 计算出一个新的路径(并返回)。加之通过这个心的路径找到它对应的Record,然后返回_createRoute(...args) => createRoute(/*对应的Record*/, /*新的路径*/)

在_createRoute中,我们先不考虑 record.redirectrecord.matchAs 的情况。

**createRoute返回的是一条 Route 路径,这个对 Route 的切换,组件的渲染都有非常重要的指导意义。**在 Vue-Router 中,所有的 Route 最终都会通过 createRoute 函数创建,并且它最后是不可以被外部修改的。

【非常重要】Route.matched ,它通过 formatMatch(record) 计算而来:可以看它是通过 record 循环向上找 parent,直到找到最外层,并把所有的 record 都 push 到一个数组中,最终返回的就是 record 的数组,它记录了一条线路上的所有 recordmatched 属性非常有用,它为之后渲染组件提供了依据。

总结: match会根据传入的位置和路径计算出新的位置,并匹配到对应的路由 record,然后根据新的位置和 record 创建新的路径并返回。

  • match
    • normalizeLocation 【看源码的单元测试,大概知道它的使用场景, 计算出新的 location
      • (一些参数的处理)
      • parsePath
      • resolvePath、resolveQuery等等处理比较复杂的location
    • 根据 新location,找到对应Route,并且一起传入调用 _createRoute
重新计算 location #详解#可不看

它主要处理了 raw 的两种情况:

  • params 且没有 path
  • path

对于第一种情况,如果 currentname,则计算出的 location 也有 name

计算出新的 location后,怎么找到对应的record #详解#可不看

locationnamepath 的两种情况做了处理。

  • name

name 的情况下就根据 nameMap 匹配到 record,它就是一个 RouterRecord 对象,如果 record 不存在,则匹配失败,返回一个空路径;然后拿到 record 对应的 paramNames,再对比 currentRoute 中的 params,把交集部分的 params 添加到 location 中,然后在通过 fillParams 方法根据 record.pathlocation.path 计算出 location.path,最后调用 _createRoute(record, location, redirectedFrom) 去生成一条新路径,该方法我们之后会介绍。

  • path

通过 name 我们可以很快的找到 record,但是通过 path 并不能,因为我们计算后的 location.path 是一个真实路径,而 record 中的 path 可能会有 param,因此需要对所有的 pathList 做顺序遍历, 然后通过 matchRoute 方法根据 record.regexlocation.pathlocation.params 匹配,如果匹配到则也通过 _createRoute(record, location, redirectedFrom) 去生成一条新路径。因为是顺序遍历,所以我们书写路由配置要注意路径的顺序,因为写在前面的会优先尝试匹配。

文章目录
  1. 该模块的架构总结:
  2. 基础数据结构作为前置模块
    1. Loaction 数据结构定义在 flow/declarations.js 中
    2. Route 数据结构定义在 flow/declarations.js 中
  3. RouteRecord:还是对路由表的数据做点增强,方便用于匹配
  4. createMatcher 返回的正是 matcher,是做匹配用的
  5. addRoutes: createMatcher的返回结果
  6. match: createMatcher的返回结果
    1. 重新计算 location #详解#可不看
    2. 计算出新的 location后,怎么找到对应的record #详解#可不看