三、matcher
在组件根部vue实例,会在beforeCreate生命周期中执行router的init方法,在init方法中通过history.transitionTo进行路由的操作,transitionTo函数首先会通过this.router.match(location, this.current)拿到路由,那么match方法对应的是this.matcher.match方法,this.matcher在new VueRouter的时候通过createMatcher(options.routes || [], this)初始化,第一个参数传入用户写的routes,第二个参数传入当前的router实例,createMatcher函数返回一个Matcher对象。createMatcher函数首先会执行createRouteMap(routes)拿到pathList,pathMap,nameMap这三个变量。createRouteMap函数首先会定义这三个变量,然后通过forEach遍历用户传入的routes,对每一项执 行addRouteRecord(pathList, pathMap, nameMap, route)函数,并传入,第一次执行pathList是一个空数组,pathMap和nameMap是空对象,最后一个参数route是用户定义的routes,比如
{
path: xxx,
name: xxx,
component: xxx,
meta: xxx,
children: xxx
}
addRouteRecord函数首先拿到route中的path和name,然后执行normalizePath函数。normalizePath函数接受三个参数,path传入route的path属性,第二个参数为parent(如果是嵌套路由的话,parent为父级),第三个参数是strict。首先判断strict如果是false,那么会通过正则,判断path的最后一位是否是/符号,如果是则去掉斜线。接着判断如果path的第0项是/或者parent为null那么会直接返回path,否则返回cleanPath(`${parent.path}/${path}`),cleanPath的目的是把连续的两个斜线替换为一个斜线,把当前的路径和parent.path进行拼接也就是说,在写子的path路径的时候,如果直接写detail,那么会对detail进行一个拼接,如果parent.path是category/或category,那么就会拼接为category/detail。把处理过的路径赋值给normalizedPath变量后,定义了一个record对象,record.path就是刚才通过normalizePath函数计算出来的normalizedPath,record.components是route定义的components,或{ default: route.component },record.name是route定义的name,还有meta,props等。定义record对象后,会判断当前route是否有children,如果有的话,对route.children进行遍历,把.children的每一项,作为route传入addRouteRecord函数,这样如果当前route有children就会递归的调用addRouteRecord函数,去把routes的每一个节点都会通过addRouteRecord进行处理。函数最后会判断if (!pathMap[record.path])也就是path是否被添加到了pathMap中,如果没有,那么会把record.pathpush到pathList中,并以path作为key,以record作为value,定义在pathMap中。name的处理和path相同,由于path是必须定义的,name可以不定义,所以name的处理,不会往pathList中添加。那么最终createRouteMap函数中的这三个参数,就会被赋值,执行完addRouteRecord函数,createRouteMap函数最后,会对pathList数组做一个处理,如果发现有通配符*那么会把他放到最后一位,最终createRouteMap函数返回pathList,pathMap,nameMap这三个变量。createMatcher函数最终返回了值,其中一个是addRoutes方法,也就是createRouteMap(routes, pathList, pathMap, nameMap),这个提供给之后如果想要动态的修改routes,这样把之前表示路由映射关系的对象传入,也就是对旧的映射关系做修改
// src/create-matcher.js
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
...
return {
match,
addRoutes
}
}
// src/create-route-map.js
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// the path list is used to control path matching priority
const pathList: Array<string> = oldPathList || []
// $flow-disable-line
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// $flow-disable-line
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
// ensure wildcard routes are always at the end
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
...
return {
pathList,
pathMap,
nameMap
}
}
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
...
const pathToRegexpOptions: PathToRegexpOptions =
route.pathToRegexpOptions || {}
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
enteredCbs: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
if (route.children) {
// Warn if route is named, does not redirect and has a default child route.
// If users navigate to this route by name, the default child will
// not be rendered (GH Issue #629)
...
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
...
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
...
}
}
}
另一个返回的是match函数,match函数的第一个参数raw的类型是RawLocation在flow文件夹下的declarations.js中declare type RawLocation = string | Location,也就是说raw的类型可以是Location类型的数据,他可以拥有name,path,hash,query,params等属性,也可以是一个字符串,match函数最终返回一个Route类型的数据,相比Location类型,他多出了fullPath,matched,meta等属性。首先他会执行 normalizeLocation(raw, currentRoute, false, router)来定义location。normalizeLocation函数首先定义next变量,next的赋值会首先判断传入的raw的类型是否是string,如果是string那么把raw作为空对象中path的值,然后赋值给next,否则把raw直接赋值给next。接着判断如果next._normalized那么直接返回next本身,这也是做了一层缓存处理,否则的话如果next有name属性,那么会对raw做一层浅拷贝,如果next有params并且params的类型是object,那么会对next.params进行一层拷贝,重新赋值给next.params,这样就完成了对next的一个深层拷贝,然后返回next。之后会通过parsePath函数对next.path进行处理,并赋值给parsedPath变量,parsePath函数接受path,query,hash三个参数,经过一些逻辑处理之后再返回。然后定义变量basePath为current.path或/,current.path是调用match方法传入的第二个参数。之后通过调用resolveQuery定义了变量query,match函数最后会返回一个对象,其中包含_normalized为true,还有path,query,hash。match函数首先拿到loaction之后,从其中再获取name,如果有name,那么会到之前定义的nameMap,取到对应的record,如果没有取到,会调用 _createRoute(null, location)。如果取到了,那么会去对他的params做一些处理最后调用_createRoute(record, location, redirectedFrom),否则如果有path,那么会去pathMap取到对应的record,然后会判断matchRoute(record.regex, location.path, location.params),matchRoute是根据传入的record.regex正则,来匹配location.path,如果匹配到返回true,否则false,如果匹配到,那么 return _createRoute(record, location, redirectedFrom),如果没有匹配到,或者没有找到location的path和name,那么会执行 return _createRoute(record, location, redirectedFrom)
// src/create-matcher.js
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
...
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
const record = nameMap[name]
...
if (!record) return _createRoute(null, location)
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
if (typeof location.params !== 'object') {
location.params = {}
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
location.path = fillParams(record.path, location.params, `named route "${name}"`)
return _createRoute(record, location, redirectedFrom)
} else if (location.path) {
location.params = {}
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
// no match
return _createRoute(null, location)
}
...
return {
match,
addRoutes
}
}
可以看到match函数最终都会return一个调用_createRoute函数的结果,如果通过path或者name匹配到了正确的路径,那么会调用_createRoute(record, location, redirectedFrom),否则会调用_createRoute(null, location)。_createRoute函数中,如果传入的record有redirect,那么会调用redirect函数,redirect函数就是路由的重定向,他最终也会调用match方法或调用_createRoute(null, location),如果传入的record有matchAs他会执行alias函数也就是路由别名功能,最终他也会调用_createRoute方法,也就是说最终都会调用 return createRoute(record, location, redirectedFrom, router),createRoute函数最终返回一个route类型的数据,形成路由的对应关系
function _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, router)
}
在看源码的过程中会遇到很多辅助函数,他会帮助你对参数进行一些处理,其中有的逻辑很复杂,在看源码的过程中,最重要的是对整体逻辑的把控,之后可以再去细致的看具体的函数是如何实现的,在很多项目中,会有一些单元测试的脚本,比如在vue-router中,test/unit/specs文件夹下就有很多辅助函数的测试,比如normalizeLocation函数,他会在单元测试中传入不同的参数,并通过expect函数来拿到相应的结果,遇到一些辅助函数,可以通过看单元测试的方式,快速了解一个函数的大概目的(传入什么参数,执行之后返回什么参数)
// test/unit/specs/location.spec.js
import { normalizeLocation } from '../../../src/util/location'
describe('Location utils', () => {
describe('normalizeLocation', () => {
it('string', () => {
const loc = normalizeLocation('/abc?foo=bar&baz=qux#hello')
expect(loc._normalized).toBe(true)
expect(loc.path).toBe('/abc')
expect(loc.hash).toBe('#hello')
expect(JSON.stringify(loc.query)).toBe(JSON.stringify({
foo: 'bar',
baz: 'qux'
}))
})
it('empty string', function () {
const loc = normalizeLocation('', { path: '/abc' })
expect(loc._normalized).toBe(true)
expect(loc.path).toBe('/abc')
expect(loc.hash).toBe('')
expect(JSON.stringify(loc.query)).toBe(JSON.stringify({}))
})
...
})