• matcher
    • createMatcher
    • addRoutes
    • match
    • 总结

    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