matcher 相关的实现都在 src/create-matcher.js 中

  1. export type Matcher = {
  2. match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  3. addRoutes: (routes: Array<RouteConfig>) => void;
  4. addRoute: (parentNameOrRoute: string | RouteConfig, route?: RouteConfig) => void;
  5. getRoutes: () => Array<RouteRecord>;
  6. };

路由中重要的 2 个概念

Loaction 和 Route的数据结构定义在 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. }

和浏览器提供的 window.location 部分结构有点类似,它们都是对 url 的结构化描述

例子

  1. /abc?foo=bar&baz=qux#hello
  2. path /abc
  3. query {foo:'bar',baz:'qux'}

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 表示的是路由中的一条线路,它除了描述了类似 Loctaion 的 path、query、hash 这些概念,还有 matched 表示匹配到的所有的 RouteRecord

createMatcher

  1. export function createMatcher (
  2. routes: Array<RouteConfig>, // 用户定义的路由配置
  3. router: VueRouter // new VueRouter返回的实例
  4. ): Matcher {
  5. // createRouteMap创建路由映射表
  6. const { pathList, pathMap, nameMap } = createRouteMap(routes)
  7. function addRoutes (routes) {
  8. createRouteMap(routes, pathList, pathMap, nameMap)
  9. }
  10. function addRoute (parentOrRoute, route) {
  11. const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined
  12. // $flow-disable-line
  13. createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)
  14. // add aliases of parent
  15. if (parent && parent.alias.length) {
  16. createRouteMap(
  17. // $flow-disable-line route is defined if parent is
  18. parent.alias.map(alias => ({ path: alias, children: [route] })),
  19. pathList,
  20. pathMap,
  21. nameMap,
  22. parent
  23. )
  24. }
  25. }
  26. function getRoutes () {
  27. return pathList.map(path => pathMap[path])
  28. }
  29. function match (
  30. raw: RawLocation,
  31. currentRoute?: Route,
  32. redirectedFrom?: Location
  33. ): Route {
  34. const location = normalizeLocation(raw, currentRoute, false, router)
  35. const { name } = location
  36. if (name) {
  37. const record = nameMap[name]
  38. if (process.env.NODE_ENV !== 'production') {
  39. warn(record, `Route with name '${name}' does not exist`)
  40. }
  41. if (!record) return _createRoute(null, location)
  42. const paramNames = record.regex.keys
  43. .filter(key => !key.optional)
  44. .map(key => key.name)
  45. if (typeof location.params !== 'object') {
  46. location.params = {}
  47. }
  48. if (currentRoute && typeof currentRoute.params === 'object') {
  49. for (const key in currentRoute.params) {
  50. if (!(key in location.params) && paramNames.indexOf(key) > -1) {
  51. location.params[key] = currentRoute.params[key]
  52. }
  53. }
  54. }
  55. location.path = fillParams(record.path, location.params, `named route "${name}"`)
  56. return _createRoute(record, location, redirectedFrom)
  57. } else if (location.path) {
  58. location.params = {}
  59. for (let i = 0; i < pathList.length; i++) {
  60. const path = pathList[i]
  61. const record = pathMap[path]
  62. if (matchRoute(record.regex, location.path, location.params)) {
  63. return _createRoute(record, location, redirectedFrom)
  64. }
  65. }
  66. }
  67. // no match
  68. return _createRoute(null, location)
  69. }
  70. function redirect (
  71. record: RouteRecord,
  72. location: Location
  73. ): Route {
  74. const originalRedirect = record.redirect
  75. let redirect = typeof originalRedirect === 'function'
  76. ? originalRedirect(createRoute(record, location, null, router))
  77. : originalRedirect
  78. if (typeof redirect === 'string') {
  79. redirect = { path: redirect }
  80. }
  81. if (!redirect || typeof redirect !== 'object') {
  82. if (process.env.NODE_ENV !== 'production') {
  83. warn(
  84. false, `invalid redirect option: ${JSON.stringify(redirect)}`
  85. )
  86. }
  87. return _createRoute(null, location)
  88. }
  89. const re: Object = redirect
  90. const { name, path } = re
  91. let { query, hash, params } = location
  92. query = re.hasOwnProperty('query') ? re.query : query
  93. hash = re.hasOwnProperty('hash') ? re.hash : hash
  94. params = re.hasOwnProperty('params') ? re.params : params
  95. if (name) {
  96. // resolved named direct
  97. const targetRecord = nameMap[name]
  98. if (process.env.NODE_ENV !== 'production') {
  99. assert(targetRecord, `redirect failed: named route "${name}" not found.`)
  100. }
  101. return match({
  102. _normalized: true,
  103. name,
  104. query,
  105. hash,
  106. params
  107. }, undefined, location)
  108. } else if (path) {
  109. // 1. resolve relative redirect
  110. const rawPath = resolveRecordPath(path, record)
  111. // 2. resolve params
  112. const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`)
  113. // 3. rematch with existing query and hash
  114. return match({
  115. _normalized: true,
  116. path: resolvedPath,
  117. query,
  118. hash
  119. }, undefined, location)
  120. } else {
  121. if (process.env.NODE_ENV !== 'production') {
  122. warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)
  123. }
  124. return _createRoute(null, location)
  125. }
  126. }
  127. function alias (
  128. record: RouteRecord,
  129. location: Location,
  130. matchAs: string
  131. ): Route {
  132. const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`)
  133. const aliasedMatch = match({
  134. _normalized: true,
  135. path: aliasedPath
  136. })
  137. if (aliasedMatch) {
  138. const matched = aliasedMatch.matched
  139. const aliasedRecord = matched[matched.length - 1]
  140. location.params = aliasedMatch.params
  141. return _createRoute(aliasedRecord, location)
  142. }
  143. return _createRoute(null, location)
  144. }
  145. function _createRoute (
  146. record: ?RouteRecord,
  147. location: Location,
  148. redirectedFrom?: Location
  149. ): Route {
  150. if (record && record.redirect) {
  151. return redirect(record, redirectedFrom || location)
  152. }
  153. if (record && record.matchAs) {
  154. return alias(record, location, record.matchAs)
  155. }
  156. return createRoute(record, location, redirectedFrom, router)
  157. }
  158. // matcher是一个对象,对外暴露了match和addRoutes方法
  159. return {
  160. match,
  161. addRoute,
  162. getRoutes,
  163. addRoutes
  164. }
  165. }

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

  1. // 把用户的路由配置转换成一张路由映射表
  2. export function createRouteMap (
  3. routes: Array<RouteConfig>,
  4. oldPathList?: Array<string>,
  5. oldPathMap?: Dictionary<RouteRecord>,
  6. oldNameMap?: Dictionary<RouteRecord>,
  7. parentRoute?: RouteRecord
  8. ): {
  9. pathList: Array<string>,
  10. pathMap: Dictionary<RouteRecord>,
  11. nameMap: Dictionary<RouteRecord>
  12. } {
  13. // the path list is used to control path matching priority
  14. // pathList存储所有的path
  15. const pathList: Array<string> = oldPathList || []
  16. // $flow-disable-line
  17. // pathMap表示一个path到RouteRecord的映射关系
  18. const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  19. // $flow-disable-line
  20. // nameMap表示name到RouteRecord的映射关系
  21. const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  22. // 遍历routes为每一个route执行addRouteRecord方法生成一条记录
  23. routes.forEach(route => {
  24. addRouteRecord(pathList, pathMap, nameMap, route, parentRoute) // 不断给pathList, pathMap, nameMap添加数据
  25. })
  26. // pathList是为了记录路由配置中的所有path
  27. // pathMap, nameMap都是为了通过path和name能快速查找到对应的RouteRecord
  28. // ensure wildcard routes are always at the end
  29. for (let i = 0, l = pathList.length; i < l; i++) {
  30. if (pathList[i] === '*') {
  31. pathList.push(pathList.splice(i, 1)[0])
  32. l--
  33. i--
  34. }
  35. }
  36. if (process.env.NODE_ENV === 'development') {
  37. // warn if routes do not include leading slashes
  38. const found = pathList
  39. // check for missing leading slash
  40. .filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/')
  41. if (found.length > 0) {
  42. const pathNames = found.map(path => `- ${path}`).join('\n')
  43. warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`)
  44. }
  45. }
  46. return {
  47. pathList,
  48. pathMap,
  49. nameMap
  50. }
  51. }

RouteRecord的数据结构

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

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(
  15. path || name
  16. )} cannot be a ` + `string id. Use an actual component instead.`
  17. )
  18. warn(
  19. // eslint-disable-next-line no-control-regex
  20. !/[^\u0000-\u007F]+/.test(path),
  21. `Route with path "${path}" contains unencoded characters, make sure ` +
  22. `your path is correctly encoded before passing it to the router. Use ` +
  23. `encodeURI to encode static segments of your path.`
  24. )
  25. }
  26. const pathToRegexpOptions: PathToRegexpOptions =
  27. route.pathToRegexpOptions || {}
  28. const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
  29. if (typeof route.caseSensitive === 'boolean') {
  30. pathToRegexpOptions.sensitive = route.caseSensitive
  31. }
  32. // 1.创建RouteRecord
  33. const record: RouteRecord = {
  34. path: normalizedPath, // path是规范化后的路径 - 会根据parent的path做计算
  35. regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 一个正则表达式的扩展 - 利用了path-to-regexp这个工具库,把path解析成一个正则表达式的扩展
  36. components: route.components || { default: route.component }, // components是一个对象,通常在配置中写的component实际上会被转换成{components: route.component}
  37. alias: route.alias
  38. ? typeof route.alias === 'string'
  39. ? [route.alias]
  40. : route.alias
  41. : [],
  42. instances: {}, // 表示组件的实例,也是一个对象类型
  43. enteredCbs: {},
  44. name,
  45. parent, // 表示父的RouteRecord,配置时会配置子路由,所以整个RouteRecord也就是一个树型结构
  46. matchAs,
  47. redirect: route.redirect,
  48. beforeEnter: route.beforeEnter,
  49. meta: route.meta || {},
  50. props:
  51. route.props == null
  52. ? {}
  53. : route.components
  54. ? route.props
  55. : { default: route.props }
  56. }
  57. // 如果配置了children
  58. if (route.children) {
  59. // Warn if route is named, does not redirect and has a default child route.
  60. // If users navigate to this route by name, the default child will
  61. // not be rendered (GH Issue #629)
  62. if (process.env.NODE_ENV !== 'production') {
  63. if (
  64. route.name &&
  65. !route.redirect &&
  66. route.children.some(child => /^\/?$/.test(child.path))
  67. ) {
  68. warn(
  69. false,
  70. `Named Route '${route.name}' has a default child route. ` +
  71. `When navigating to this named route (:to="{name: '${
  72. route.name
  73. }'"), ` +
  74. `the default child route will not be rendered. Remove the name from ` +
  75. `this route and use the name of the default child route for named ` +
  76. `links instead.`
  77. )
  78. }
  79. }
  80. // 递归执行addRouteRecord方法,并把当前的record作为parent传入,通过这样的深度遍历就可以拿到一个route下的完整记录
  81. route.children.forEach(child => {
  82. const childMatchAs = matchAs
  83. ? cleanPath(`${matchAs}/${child.path}`)
  84. : undefined
  85. addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
  86. })
  87. }
  88. // 为pathList和pathMap各添加一条记录
  89. if (!pathMap[record.path]) {
  90. pathList.push(record.path)
  91. pathMap[record.path] = record
  92. }
  93. if (route.alias !== undefined) {
  94. const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
  95. for (let i = 0; i < aliases.length; ++i) {
  96. const alias = aliases[i]
  97. if (process.env.NODE_ENV !== 'production' && alias === path) {
  98. warn(
  99. false,
  100. `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
  101. )
  102. // skip in dev to make it work
  103. continue
  104. }
  105. const aliasRoute = {
  106. path: alias,
  107. children: route.children
  108. }
  109. addRouteRecord(
  110. pathList,
  111. pathMap,
  112. nameMap,
  113. aliasRoute,
  114. parent,
  115. record.path || '/' // matchAs
  116. )
  117. }
  118. }
  119. // 如果在路由配置中配置了name,则给nameMap添加一条记录
  120. if (name) {
  121. if (!nameMap[name]) {
  122. nameMap[name] = record
  123. } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
  124. warn(
  125. false,
  126. `Duplicate named routes definition: ` +
  127. `{ name: "${name}", path: "${record.path}" }`
  128. )
  129. }
  130. }
  131. }

path-to-regexp例子

  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: '[^\\/]+?' }]

addRoutes

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

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

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

match

  1. function match (
  2. raw: RawLocation, // RawLocation类型,可以是url字符串,也可以是Location对象
  3. currentRoute?: Route, // Route类型,表示当前的路径
  4. redirectedFrom?: Location // 和重定向相关
  5. ): Route {
  6. //
  7. const location = normalizeLocation(raw, currentRoute, false, router)
  8. const { name } = location
  9. if (name) {
  10. const record = nameMap[name]
  11. if (process.env.NODE_ENV !== 'production') {
  12. warn(record, `Route with name '${name}' does not exist`)
  13. }
  14. if (!record) return _createRoute(null, location)
  15. const paramNames = record.regex.keys
  16. .filter(key => !key.optional)
  17. .map(key => key.name)
  18. if (typeof location.params !== 'object') {
  19. location.params = {}
  20. }
  21. if (currentRoute && typeof currentRoute.params === 'object') {
  22. for (const key in currentRoute.params) {
  23. if (!(key in location.params) && paramNames.indexOf(key) > -1) {
  24. location.params[key] = currentRoute.params[key]
  25. }
  26. }
  27. }
  28. location.path = fillParams(record.path, location.params, `named route "${name}"`)
  29. return _createRoute(record, location, redirectedFrom)
  30. } else if (location.path) {
  31. location.params = {}
  32. for (let i = 0; i < pathList.length; i++) {
  33. const path = pathList[i]
  34. const record = pathMap[path]
  35. if (matchRoute(record.regex, location.path, location.params)) {
  36. return _createRoute(record, location, redirectedFrom)
  37. }
  38. }
  39. }
  40. // no match
  41. // 返回的是一个路径,根据传入的raw和当前的路径currentRoute计算出一个新的路径并返回
  42. return _createRoute(null, location)
  43. }

normalizeLocation定义在 src/util/location.js 中

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

normalizeLocation主要处理2种情况,一种是有 params 且没有 path,一种是有 path 的
对于第一种情况,如果 current 有 name,则计算出的 location 也有 name
计算出新的 location 后,对 location 的 name 和 path 的两种情况做了处理

name

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

path

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

  1. // 先不考虑 record.redirect 和 record.matchAs 的情况,最终会调用 createRoute 方法
  2. function _createRoute (
  3. record: ?RouteRecord,
  4. location: Location,
  5. redirectedFrom?: Location
  6. ): Route {
  7. if (record && record.redirect) {
  8. return redirect(record, redirectedFrom || location)
  9. }
  10. if (record && record.matchAs) {
  11. return alias(record, location, record.matchAs)
  12. }
  13. return createRoute(record, location, redirectedFrom, router)
  14. }
  15. return {
  16. match,
  17. addRoute,
  18. getRoutes,
  19. addRoutes
  20. }

createRoute定义在 src/uitl/route.js 中

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

在 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 的数组,它记录了一条线路上的所有 record
matched 属性非常有用,它为之后渲染组件提供了依据

了解了 Location、Route、RouteRecord 等概念,并通过 matcher 的 match 方法,找到匹配的路径 Route,这个对 Route 的切换,组件的渲染都有非常重要的指导意义