回顾
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
做了三件事情:
- 调用 createRouteMap 方法。
- 定义了一系列函数,例如 match、redirect、alias。
- 返回 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 主要做了四件事情:
- 调用 normalizeLocation 获取一个 location。
- 判断是否有 name,有的话通过 nameMap 来创建 route。
- 判断是否有 path,有的话通过 pathMap 来创建 route。
- 最后 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)
// ...
}
有没有一点眉目,原来 路由匹配 做的是这么一件事情,保存路由信息,然后在要用到时匹配出新的路由对象。