vue-router 源码:路由匹配

953 阅读5分钟

回顾

Router 调用构造函数时,保存了一个 match 属性。

constructor (options: RouterOptions = {}) {
  this.match = createMatcher(options.routes || [])
  
  // ...
}

然后在执行 transitionTo 时,就用到了这个属性(函数)。

transitionTo (location: RawLocation, cb?: Function) {
  const route = this.router.match(location, this.current)
  
  // ...
}

猜测

match 保存的是 createMatcher 执行后返回的一个函数,传入的是 options.routes。

// options.routes
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

执行 match 函数,需传入目标地址和当前的路由对象,得到的是即将跳转的 route 对象。

所以初步猜测,createMatcher 是做了一个映射(路径对应到 route 对象)保存起来,当要用的时候,通过返回的函数就可以使用这个映射。

看起来不难,实际上实现的代码并不简单,做好心理准备吧。

createMatcher

我们先从 createMatcher 入手吧,打开它的代码,发现好长啊,先忽略一些细节,看个大概吧。

export function createMatcher (routes: Array<RouteConfig>): Matcher {
  // path 和 name 的路由映射
  const { pathMap, nameMap } = createRouteMap(routes)

  function match (): Route {
    // ...
  }

  function redirect (): Route {
    // ...
  }

  function alias (): Route {
    // ...
  }

  function _createRoute (): Route {
    // ...
  }

  return match
}

可以看到 createMatcher 做了三件事情:

  1. 调用 createRouteMap 方法。
  2. 定义了一系列函数,例如 match、redirect、alias。
  3. 返回 match 方法。

createRouteMap

createRouteMap 返回了 pathMap 和 nameMap,打开它的代码实现:

export function createRouteMap (routes: Array<RouteConfig>): {
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  const pathMap: Dictionary<RouteRecord> = Object.create(null)
  const nameMap: Dictionary<RouteRecord> = Object.create(null)

  routes.forEach(route => {
    addRouteRecord(pathMap, nameMap, route)
  })

  return {
    pathMap,
    nameMap
  }
}

核心代码是:

routes.forEach(route => {
  addRouteRecord(pathMap, nameMap, route)
})

addRouteRecord

遍历 options 的 routes,通过调用 addRouteRecord 来处理映射,addRouteRecord 代码实现如下:

哦不,addRouteRecord 的代码也挺长的,删减一下。

function addRouteRecord (
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string  // alias 用到了
) {
  const { path, name } = route

  const record: RouteRecord = { // ... }

  // 递归添加子组件的信息
  if (route.children) {
  }

  // 别名
  if (route.alias) {
  }

  // 形成一个路径映射
  pathMap[record.path] = record
  if (name) nameMap[name] = record
}

从代码可以看到,pathMap 存储着 path 路径与 record 的映射,nameMap 存储着 name 与 record 的映射。即通过 path 或 name 都能找到对应的 record。

record 保存着一个路由里所需的信息,比如我们可以通过路径来找到对应要渲染的组件。

const record: RouteRecord = {
  path: normalizePath(path, parent), // 路径
  components: route.components || { default: route.component }, // 对应组件
  name, // 别名
  parent, // 父亲路由
  matchAs, // alias 会用到
  redirect: route.redirect, // 重定向
  beforeEnter: route.beforeEnter, // 钩子
  // ...
}

如果有 嵌套路由 的话,即当前 route 有 children 属性的话,那就需要通过递归的方式将它们也加入映射之中。

addRouteRecord 的第四个参数是 parent,用于保存当前路由的父路由。

if (route.children) {
  route.children.forEach(child => {
    addRouteRecord(pathMap, nameMap, child, record)
  })
}

还要一种情况就是有 别名

const routes = [
  { path: '/a', component: A, alias: '/b' }
]

有别名的情况下,alias 并不会复制一份跟 path 一样的 record,而是使用了 addRouteRecord 的第五个参数 matchAs 来保存 path。

if (route.alias) {
  if (Array.isArray(route.alias)) {
    route.alias.forEach(alias => {
      addRouteRecord(pathMap, nameMap, { path: alias }, parent, record.path)
    })
  } else {
    addRouteRecord(pathMap, nameMap, { path: route.alias }, parent, record.path)
  }
}

这样,通过 alias 别名就能找到 path 路径,再通过 path 就能找到对应的 record,从而找到对应的路由信息。

match

了解完了 createRouteMap,知道通过它能得到 pathMap 和 nameMap 两个映射:

const { pathMap, nameMap } = createRouteMap(routes)

接下来再返回来 createMatcher 看看 match 函数:

export function createMatcher (routes: Array<RouteConfig>): Matcher {
  const { pathMap, nameMap } = createRouteMap(routes)

  function match (): Route {
    // ...
  }

  return match
}

刚开始的时候也提到了,存储起来的 match 会再调用 transitionTo 的时候用到:

transitionTo (location: RawLocation, cb?: Function) {
  const route = this.router.match(location, this.current)
  
  // ...
}

match 的代码如下:

function match (
  raw: RawLocation,
  currentRoute?: Route,
  redirectedFrom?: Location
): Route {
  const location = normalizeLocation(raw, currentRoute)
  const { name } = location

  // 省略的代码,接下来会提到
  
  // no match
  return _createRoute(null, location)
}

match 主要做了四件事情:

  1. 调用 normalizeLocation 获取一个 location。
  2. 判断是否有 name,有的话通过 nameMap 来创建 route。
  3. 判断是否有 path,有的话通过 pathMap 来创建 route。
  4. 最后 name 和 path 都没有的情况,直接用 null 创建 roue。

normalizeLocation

normalizeLocation 的实现就不细看了,传入的第二个对象 current,是为了处理其中一种情况,即将跳转的路由是当前路由的子路由,需要基于它来跳转。

返回的结果主要分两种情况。一种是有 name,直接返回以下对象:

{ path, params }

另一种则是通过路径的处理返回以下对象:

{ path, query, hash }

接下来无论 name 或 path 的处理都会用到 path-to-regexp 来做动态路由匹配。这里只要知道通过这个库可以做类似以下的事情即可:

// 官方文档例子
const userId = 123
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123

无论是处理 name 时的 fillParams 函数还是处理 path 时的 matchRoute 函数都是做动态路由匹配的事情。

fillParams 实现的是将 path 和 params 拼成完整路径。

if (name) {
  const record = nameMap[name]
  if (record) {
    location.path = fillParams(record.path, location.params, `named route "${name}"`)
    return _createRoute(record, location, redirectedFrom)
  }
}

而 path 判断时,需要遍历 pathMap 来找到匹配的到的路由,matchRoute 做的就是拼接路径和匹配路由。

else if (location.path) {
  location.params = {}
  for (const path in pathMap) {
    if (matchRoute(path, location.params, location.path)) {
      return _createRoute(pathMap[path], location, redirectedFrom)
    }
  }
}

最后,name、path 或 null 的条件下都会返回一个 _createRoute 函数来创建路由。

_createRoute

既然有了保存着路由信息的 record,也有了动态路由匹配完成后的 location 路径信息。

当然还有一个 redirectedFrom 的信息,这里我们忽略吧。

通过将 record 和 location 传递给 _createRoute,本以为里面就会做创建路由对象的操作,结果发现里面只是判断。

  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: Location
): Route {
  if (record && record.redirect) {
    return redirect(record, redirectedFrom || location)
  }
  if (record && record.matchAs) {
    return alias(record, location, record.matchAs)
  }
  return createRoute(record, location, redirectedFrom)
}

里面判断了 redirect 重定向和 matchAs 别名,但这里不打算深究这两个,因为代码还真不少,感兴趣的打开源码看即可。

来看默认的创建路由吧:

return createRoute(record, location, redirectedFrom)

好吧,咱们乖乖去找 createRoute 函数吧。

createRoute

打开 createRoute 有木有一种熟悉的感觉。还记得在看导航守卫时遇到的 START 吗,不记得回去看看吧。

createRoute 创建的就是一个真正的 Route 对象了。

const route: Route = {
  name: location.name || (record && record.name),
  meta: (record && record.meta) || {},
  path: location.path || '/',
  hash: location.hash || '',
  query: location.query || {},
  params: location.params || {},
  fullPath: getFullPath(location),
  matched: record ? formatMatch(record) : []
}

至此,知道了 match 的大致实现,我们就能得出,只要有一个即将跳转的路由信息 location,和保存着当前的路由对象 current,就能通过 match 来得到一个新的路由 Route 对象,以便后面的路由跳转和导航守卫。

transitionTo (location: RawLocation, cb?: Function) {
  const route = this.router.match(location, this.current)
  
  // ...
}

最后

再来回顾一下 createMatcher 的创建。

constructor (options: RouterOptions = {}) {
  this.match = createMatcher(options.routes || [])
  
  // ...
}

match 的使用。

transitionTo (location: RawLocation, cb?: Function) {
  const route = this.router.match(location, this.current)
  
  // ...
}

有没有一点眉目,原来 路由匹配 做的是这么一件事情,保存路由信息,然后在要用到时匹配出新的路由对象。