matcher

matcher 相关的实现都在 src/create-matcher.js 中,我们先来看一下 matcher 的数据结构:

  1. export type Matcher = {
  2. match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  3. addRoutes: (routes: Array<RouteConfig>) => void;
  4. };

Matcher 返回了 2 个方法,matchaddRoutes,在上一节我们接触到了 match 方法,顾名思义它是做匹配,那么匹配的是什么,在介绍之前,我们先了解路由中重要的 2 个概念,LoactionRoute,它们的数据结构定义在 flow/declarations.js 中。

  • Location
  1. declare type Location = {
  2. _normalized?: boolean;
  3. name?: string;
  4. path?: string;
  5. hash?: string;
  6. query?: Dictionary<string>;
  7. params?: Dictionary<string>;
  8. append?: boolean;
  9. replace?: boolean;
  10. }

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

  • Route
  1. declare type Route = {
  2. path: string;
  3. name: ?string;
  4. hash: string;
  5. query: Dictionary<string>;
  6. params: Dictionary<string>;
  7. fullPath: string;
  8. matched: Array<RouteRecord>;
  9. redirectedFrom?: string;
  10. meta?: any;
  11. }

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

createMatcher

在了解了 LocationRoute 后,我们来看一下 matcher 的创建过程:

  1. export function createMatcher (
  2. routes: Array<RouteConfig>,
  3. router: VueRouter
  4. ): Matcher {
  5. const { pathList, pathMap, nameMap } = createRouteMap(routes)
  6. function addRoutes (routes) {
  7. createRouteMap(routes, pathList, pathMap, nameMap)
  8. }
  9. function match (
  10. raw: RawLocation,
  11. currentRoute?: Route,
  12. redirectedFrom?: Location
  13. ): Route {
  14. const location = normalizeLocation(raw, currentRoute, false, router)
  15. const { name } = location
  16. if (name) {
  17. const record = nameMap[name]
  18. if (process.env.NODE_ENV !== 'production') {
  19. warn(record, `Route with name '${name}' does not exist`)
  20. }
  21. if (!record) return _createRoute(null, location)
  22. const paramNames = record.regex.keys
  23. .filter(key => !key.optional)
  24. .map(key => key.name)
  25. if (typeof location.params !== 'object') {
  26. location.params = {}
  27. }
  28. if (currentRoute && typeof currentRoute.params === 'object') {
  29. for (const key in currentRoute.params) {
  30. if (!(key in location.params) && paramNames.indexOf(key) > -1) {
  31. location.params[key] = currentRoute.params[key]
  32. }
  33. }
  34. }
  35. if (record) {
  36. location.path = fillParams(record.path, location.params, `named route "${name}"`)
  37. return _createRoute(record, location, redirectedFrom)
  38. }
  39. } else if (location.path) {
  40. location.params = {}
  41. for (let i = 0; i < pathList.length; i++) {
  42. const path = pathList[i]
  43. const record = pathMap[path]
  44. if (matchRoute(record.regex, location.path, location.params)) {
  45. return _createRoute(record, location, redirectedFrom)
  46. }
  47. }
  48. }
  49. return _createRoute(null, location)
  50. }
  51. // ...
  52. function _createRoute (
  53. record: ?RouteRecord,
  54. location: Location,
  55. redirectedFrom?: Location
  56. ): Route {
  57. if (record && record.redirect) {
  58. return redirect(record, redirectedFrom || location)
  59. }
  60. if (record && record.matchAs) {
  61. return alias(record, location, record.matchAs)
  62. }
  63. return createRoute(record, location, redirectedFrom, router)
  64. }
  65. return {
  66. match,
  67. addRoutes
  68. }
  69. }

createMatcher 接收 2 个参数,一个是 router,它是我们 new VueRouter 返回的实例,一个是 routes,它是用户定义的路由配置,来看一下我们之前举的例子中的配置:

  1. const Foo = { template: '<div>foo</div>' }
  2. const Bar = { template: '<div>bar</div>' }
  3. const routes = [
  4. { path: '/foo', component: Foo },
  5. { path: '/bar', component: Bar }
  6. ]

createMathcer 首先执行的逻辑是 const { pathList, pathMap, nameMap } = createRouteMap(routes) 创建一个路由映射表,createRouteMap 的定义在 src/create-route-map 中:

  1. export function createRouteMap (
  2. routes: Array<RouteConfig>,
  3. oldPathList?: Array<string>,
  4. oldPathMap?: Dictionary<RouteRecord>,
  5. oldNameMap?: Dictionary<RouteRecord>
  6. ): {
  7. pathList: Array<string>;
  8. pathMap: Dictionary<RouteRecord>;
  9. nameMap: Dictionary<RouteRecord>;
  10. } {
  11. const pathList: Array<string> = oldPathList || []
  12. const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  13. const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  14. routes.forEach(route => {
  15. addRouteRecord(pathList, pathMap, nameMap, route)
  16. })
  17. for (let i = 0, l = pathList.length; i < l; i++) {
  18. if (pathList[i] === '*') {
  19. pathList.push(pathList.splice(i, 1)[0])
  20. l--
  21. i--
  22. }
  23. }
  24. return {
  25. pathList,
  26. pathMap,
  27. nameMap
  28. }
  29. }

createRouteMap 函数的目标是把用户的路由配置转换成一张路由映射表,它包含 3 个部分,pathList 存储所有的 pathpathMap 表示一个 pathRouteRecord 的映射关系,而 nameMap 表示 nameRouteRecord 的映射关系。那么 RouteRecord 到底是什么,先来看一下它的数据结构:

  1. declare type RouteRecord = {
  2. path: string;
  3. regex: RouteRegExp;
  4. components: Dictionary<any>;
  5. instances: Dictionary<any>;
  6. name: ?string;
  7. parent: ?RouteRecord;
  8. redirect: ?RedirectOption;
  9. matchAs: ?string;
  10. beforeEnter: ?NavigationGuard;
  11. meta: any;
  12. props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
  13. }

它的创建是通过遍历 routes 为每一个 route 执行 addRouteRecord 方法生成一条记录,来看一下它的定义:

  1. function addRouteRecord (
  2. pathList: Array<string>,
  3. pathMap: Dictionary<RouteRecord>,
  4. nameMap: Dictionary<RouteRecord>,
  5. route: RouteConfig,
  6. parent?: RouteRecord,
  7. matchAs?: string
  8. ) {
  9. const { path, name } = route
  10. if (process.env.NODE_ENV !== 'production') {
  11. assert(path != null, `"path" is required in a route configuration.`)
  12. assert(
  13. typeof route.component !== 'string',
  14. `route config "component" for path: ${String(path || name)} cannot be a ` +
  15. `string id. Use an actual component instead.`
  16. )
  17. }
  18. const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
  19. const normalizedPath = normalizePath(
  20. path,
  21. parent,
  22. pathToRegexpOptions.strict
  23. )
  24. if (typeof route.caseSensitive === 'boolean') {
  25. pathToRegexpOptions.sensitive = route.caseSensitive
  26. }
  27. const record: RouteRecord = {
  28. path: normalizedPath,
  29. regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
  30. components: route.components || { default: route.component },
  31. instances: {},
  32. name,
  33. parent,
  34. matchAs,
  35. redirect: route.redirect,
  36. beforeEnter: route.beforeEnter,
  37. meta: route.meta || {},
  38. props: route.props == null
  39. ? {}
  40. : route.components
  41. ? route.props
  42. : { default: route.props }
  43. }
  44. if (route.children) {
  45. if (process.env.NODE_ENV !== 'production') {
  46. if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
  47. warn(
  48. false,
  49. `Named Route '${route.name}' has a default child route. ` +
  50. `When navigating to this named route (:to="{name: '${route.name}'"), ` +
  51. `the default child route will not be rendered. Remove the name from ` +
  52. `this route and use the name of the default child route for named ` +
  53. `links instead.`
  54. )
  55. }
  56. }
  57. route.children.forEach(child => {
  58. const childMatchAs = matchAs
  59. ? cleanPath(`${matchAs}/${child.path}`)
  60. : undefined
  61. addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
  62. })
  63. }
  64. if (route.alias !== undefined) {
  65. const aliases = Array.isArray(route.alias)
  66. ? route.alias
  67. : [route.alias]
  68. aliases.forEach(alias => {
  69. const aliasRoute = {
  70. path: alias,
  71. children: route.children
  72. }
  73. addRouteRecord(
  74. pathList,
  75. pathMap,
  76. nameMap,
  77. aliasRoute,
  78. parent,
  79. record.path || '/'
  80. )
  81. })
  82. }
  83. if (!pathMap[record.path]) {
  84. pathList.push(record.path)
  85. pathMap[record.path] = record
  86. }
  87. if (name) {
  88. if (!nameMap[name]) {
  89. nameMap[name] = record
  90. } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
  91. warn(
  92. false,
  93. `Duplicate named routes definition: ` +
  94. `{ name: "${name}", path: "${record.path}" }`
  95. )
  96. }
  97. }
  98. }

我们只看几个关键逻辑,首先创建 RouteRecord 的代码如下:

  1. const record: RouteRecord = {
  2. path: normalizedPath,
  3. regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
  4. components: route.components || { default: route.component },
  5. instances: {},
  6. name,
  7. parent,
  8. matchAs,
  9. redirect: route.redirect,
  10. beforeEnter: route.beforeEnter,
  11. meta: route.meta || {},
  12. props: route.props == null
  13. ? {}
  14. : route.components
  15. ? route.props
  16. : { default: route.props }
  17. }

这里要注意几个点,path 是规范化后的路径,它会根据 parentpath 做计算;regex 是一个正则表达式的扩展,它利用了path-to-regexp 这个工具库,把 path 解析成一个正则表达式的扩展,举个例子:

  1. var keys = []
  2. var re = pathToRegexp('/foo/:bar', keys)
  3. // re = /^\/foo\/([^\/]+?)\/?$/i
  4. // keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]

components 是一个对象,通常我们在配置中写的 component 实际上这里会被转换成 {components: route.component}instances 表示组件的实例,也是一个对象类型;parent 表示父的 RouteRecord,因为我们配置的时候有时候会配置子路由,所以整个 RouteRecord 也就是一个树型结构。

  1. if (route.children) {
  2. // ...
  3. route.children.forEach(child => {
  4. const childMatchAs = matchAs
  5. ? cleanPath(`${matchAs}/${child.path}`)
  6. : undefined
  7. addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
  8. })
  9. }

如果配置了 children,那么递归执行 addRouteRecord 方法,并把当前的 record 作为 parent 传入,通过这样的深度遍历,我们就可以拿到一个 route 下的完整记录。

  1. if (!pathMap[record.path]) {
  2. pathList.push(record.path)
  3. pathMap[record.path] = record
  4. }

pathListpathMap 各添加一条记录。

  1. if (name) {
  2. if (!nameMap[name]) {
  3. nameMap[name] = record
  4. }
  5. // ...
  6. }

如果我们在路由配置中配置了 name,则给 nameMap 添加一条记录。

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

再回到 createMather 函数,接下来就定义了一系列方法,最后返回了一个对象。

  1. return {
  2. match,
  3. addRoutes
  4. }

也就是说,matcher 是一个对象,它对外暴露了 matchaddRoutes 方法。

addRoutes

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

  1. function addRoutes (routes) {
  2. createRouteMap(routes, pathList, pathMap, nameMap)
  3. }

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

match

  1. function match (
  2. raw: RawLocation,
  3. currentRoute?: Route,
  4. redirectedFrom?: Location
  5. ): Route {
  6. const location = normalizeLocation(raw, currentRoute, false, router)
  7. const { name } = location
  8. if (name) {
  9. const record = nameMap[name]
  10. if (process.env.NODE_ENV !== 'production') {
  11. warn(record, `Route with name '${name}' does not exist`)
  12. }
  13. if (!record) return _createRoute(null, location)
  14. const paramNames = record.regex.keys
  15. .filter(key => !key.optional)
  16. .map(key => key.name)
  17. if (typeof location.params !== 'object') {
  18. location.params = {}
  19. }
  20. if (currentRoute && typeof currentRoute.params === 'object') {
  21. for (const key in currentRoute.params) {
  22. if (!(key in location.params) && paramNames.indexOf(key) > -1) {
  23. location.params[key] = currentRoute.params[key]
  24. }
  25. }
  26. }
  27. if (record) {
  28. location.path = fillParams(record.path, location.params, `named route "${name}"`)
  29. return _createRoute(record, location, redirectedFrom)
  30. }
  31. } else if (location.path) {
  32. location.params = {}
  33. for (let i = 0; i < pathList.length; i++) {
  34. const path = pathList[i]
  35. const record = pathMap[path]
  36. if (matchRoute(record.regex, location.path, location.params)) {
  37. return _createRoute(record, location, redirectedFrom)
  38. }
  39. }
  40. }
  41. return _createRoute(null, location)
  42. }

match 方法接收 3 个参数,其中 rawRawLocation 类型,它可以是一个 url 字符串,也可以是一个 Location 对象;currentRouteRoute 类型,它表示当前的路径;redirectedFrom 和重定向相关,这里先忽略。match 方法返回的是一个路径,它的作用是根据传入的 raw 和当前的路径 currentRoute 计算出一个新的路径并返回。

首先执行了 normalizeLocation,它的定义在 src/util/location.js 中:

  1. export function normalizeLocation (
  2. raw: RawLocation,
  3. current: ?Route,
  4. append: ?boolean,
  5. router: ?VueRouter
  6. ): Location {
  7. let next: Location = typeof raw === 'string' ? { path: raw } : raw
  8. if (next.name || next._normalized) {
  9. return next
  10. }
  11. if (!next.path && next.params && current) {
  12. next = assign({}, next)
  13. next._normalized = true
  14. const params: any = assign(assign({}, current.params), next.params)
  15. if (current.name) {
  16. next.name = current.name
  17. next.params = params
  18. } else if (current.matched.length) {
  19. const rawPath = current.matched[current.matched.length - 1].path
  20. next.path = fillParams(rawPath, params, `path ${current.path}`)
  21. } else if (process.env.NODE_ENV !== 'production') {
  22. warn(false, `relative params navigation requires a current route.`)
  23. }
  24. return next
  25. }
  26. const parsedPath = parsePath(next.path || '')
  27. const basePath = (current && current.path) || '/'
  28. const path = parsedPath.path
  29. ? resolvePath(parsedPath.path, basePath, append || next.append)
  30. : basePath
  31. const query = resolveQuery(
  32. parsedPath.query,
  33. next.query,
  34. router && router.options.parseQuery
  35. )
  36. let hash = next.hash || parsedPath.hash
  37. if (hash && hash.charAt(0) !== '#') {
  38. hash = `#${hash}`
  39. }
  40. return {
  41. _normalized: true,
  42. path,
  43. query,
  44. hash
  45. }
  46. }

normalizeLocation 方法的作用是根据 rawcurrent 计算出新的 location,它主要处理了 raw 的两种情况,一种是有 params 且没有 path,一种是有 path 的,对于第一种情况,如果 currentname,则计算出的 location 也有 name

计算出新的 location 后,对 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) 去生成一条新路径。因为是顺序遍历,所以我们书写路由配置要注意路径的顺序,因为写在前面的会优先尝试匹配。

最后我们来看一下 _createRoute 的实现:

  1. function _createRoute (
  2. record: ?RouteRecord,
  3. location: Location,
  4. redirectedFrom?: Location
  5. ): Route {
  6. if (record && record.redirect) {
  7. return redirect(record, redirectedFrom || location)
  8. }
  9. if (record && record.matchAs) {
  10. return alias(record, location, record.matchAs)
  11. }
  12. return createRoute(record, location, redirectedFrom, router)
  13. }

我们先不考虑 record.redirectrecord.matchAs 的情况,最终会调用 createRoute 方法,它的定义在 src/uitl/route.js 中:

  1. export function createRoute (
  2. record: ?RouteRecord,
  3. location: Location,
  4. redirectedFrom?: ?Location,
  5. router?: VueRouter
  6. ): Route {
  7. const stringifyQuery = router && router.options.stringifyQuery
  8. let query: any = location.query || {}
  9. try {
  10. query = clone(query)
  11. } catch (e) {}
  12. const route: Route = {
  13. name: location.name || (record && record.name),
  14. meta: (record && record.meta) || {},
  15. path: location.path || '/',
  16. hash: location.hash || '',
  17. query,
  18. params: location.params || {},
  19. fullPath: getFullPath(location, stringifyQuery),
  20. matched: record ? formatMatch(record) : []
  21. }
  22. if (redirectedFrom) {
  23. route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  24. }
  25. return Object.freeze(route)
  26. }

createRoute 可以根据 recordlocation 创建出来,最终返回的是一条 Route 路径,我们之前也介绍过它的数据结构。在 Vue-Router 中,所有的 Route 最终都会通过 createRoute 函数创建,并且它最后是不可以被外部修改的。Route 对象中有一个非常重要属性是 matched,它通过 formatMatch(record) 计算而来:

  1. function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  2. const res = []
  3. while (record) {
  4. res.unshift(record)
  5. record = record.parent
  6. }
  7. return res
  8. }

可以看它是通过 record 循环向上找 parent,只到找到最外层,并把所有的 record 都 push 到一个数组中,最终返回的就是 record 的数组,它记录了一条线路上的所有 recordmatched 属性非常有用,它为之后渲染组件提供了依据。

总结

那么到此,matcher 相关的主流程的分析就结束了,我们了解了 LocationRouteRouteRecord 等概念。并通过 matchermatch 方法,我们会找到匹配的路径 Route,这个对 Route 的切换,组件的渲染都有非常重要的指导意义。下一节我们会回到 transitionTo 方法,看一看路径的切换都做了哪些事情。

原文: https://ustbhuangyi.github.io/vue-analysis/vue-router/matcher.html