vue-router 源码解析(二)-创建路由匹配对象

基本使用

const routes = [
    {
        path:"/",
        component: Demo2,
        name:'root',
        beforeEnter: (to, from) => {
            console.log('ddd')
            return true;
        },
        beforeLeave:(to, from) => {
            console.log('aa')
            return true;
        },
    },
    { 
        path: '/d0/d01?p0=jeff', 
        component: App,
        name:'n0',
        alias:'/a0',
        beforeEnter: (to, from) => {
            return true;
        },
    },
    { 
        path: '/d1/:d11', 
        component: Demo1 ,
        name:'n1',
        beforeEnter:[ 
            (to, from) => {
                return true;
            },(to, from) => {
                return true;
            }
        ],
    },
    { path: '/d2/d21', component: Demo2,name:'n2',redirect:'/r2'},
    {
        path: '/d3',
        name: 'n3',
        component: Demo1,
        children: [{ path: 'd31', name: 'n31', component: Demo2,alias:'a31' }],
      },
  ]

const router = VueRouter.createRouter({
  history: VueRouter.createWebHashHistory(), // 创建对应的路由对象
  routes,
})

导语

  • 在上文中介绍了三种模式下路由对象的创建,而本文将深入createRouter,解析如何将传入的routes配置,转换成未来进行导航时对应的一个个matcher,当开发者通过push 等API进行导航时,会查找到对应path|name 的matcher记录,进而拿到需要的路由记录信息
  • matcher的创建过程,会根据routes中的信息,转换出一条条具有分数(或者理解为权重)的matcher,匹配时会根据分数优先匹配
    在这里插入图片描述

createRouterMatcher 创建匹配路由记录

//router.ts
export function createRouter(options: RouterOptions): Router { //options就为传入的配置
    // 这里返回的matcher是操作路由记录的API,非路由记录对应的匹配matcher
	const matcher = createRouterMatcher(options.routes, options)
	// ...
}
  • 会递归调用addRoutes方法,将配置的routes全部转换成matcher
export function createRouterMatcher(
  routes: Readonly<RouteRecordRaw[]>,
  globalOptions: PathParserOptions
): RouterMatcher {
  // 存储所有routes配置转换而成的路由记录
  const matchers: RouteRecordMatcher[] = []
  // 存储原始路由记录(非原始记录:设置了alias的别名路由会再创建一条记录,但该记录不会加入matcherMap中)
  const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
  
  // 合并开发者传递的strict、end和sensitive这些约定路由匹配模式的属性
  globalOptions = mergeOptions(
    { strict: false, end: true, sensitive: false } as PathParserOptions,
    globalOptions
  )
  function addRoute(route){ //将routes配置转换成matcher,过程会递归调用创建子路由matcher
   // ...
  }	
  // 创建一级路由记录
  routes.forEach(route => addRoute(route));
  
  return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher };
}

addRoute 递归添加matcher

  • 普通路由:
    • 会为每一个route创建一个matcher
  • 别名路由:
    • 如果存在别名路由时,会再创建一条matcher,matcher.path为别名设置的名称
    • 别名路由的matcher.aliasOf会指向原始路由记录的matcher
    • 原始路由的matcher.alias[],会存放对应的所有别名路由记录matcher
  • 子路由:
    • 当存在子路由时,会递归创建
    • 子路由matcher.path会拼接上父路由的路径
    • 子路由matcher.parent属性会指向父路由matcher
  • 最终所有的matcher会存入matchers,整个创建出来的matchers是一个平级结构(一维数组)
function addRoute(
    record: RouteRecordRaw,  // 原始的route记录(开发者传入的)
    parent?: RouteRecordMatcher,// 当存在子路由时,parent才会有,第一次因为是创建一级路由,所以为空
    originalRecord?: RouteRecordMatcher // alais别名路由对应的原始记录
  ) {
    // 首次添加originalRecord为空,表明是添加第一层根路由
    const isRootAdd = !originalRecord
	
	// 将单个路由配置转换成规定格式,格式如下
	/**
	{
	    path: record.path,
	    redirect: record.redirect,
	    name: record.name,
	    meta: record.meta || {},
	    aliasOf: undefined, // 别名记录才会有值,执行原始记录
	    beforeEnter: record.beforeEnter,
	    props: normalizeRecordProps(record),
	    children: record.children || [],
	    instances: {},//路由组件实例,复用时使用
	    leaveGuards: new Set(), // setup中使用的守卫
	    updateGuards: new Set(),// setup中使用的守卫
	    enterCallbacks: {},
	    components:
	      'components' in record
	        ? record.components || null
	        : record.component && { default: record.component },
	  }
	*/
    const mainNormalizedRecord = normalizeRouteRecord(record)
	// 让个是别名路由,originalRecord会指向原始路由,为别名路由添加引用
	mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
	// 合并route记录中存在的strict、end和sensitive这些约定路由匹配模式的属性
	const options: PathParserOptions = mergeOptions(globalOptions, record)
	
	// 将转换后的路由变成数组格式,因为可能存在别名,将别名记录添加进去
    const normalizedRecords: typeof mainNormalizedRecord[] = [
      mainNormalizedRecord,
    ]
	// 处理别名情况
	if ('alias' in record) {
      // 别名可以是一个字符串或数组,统一变成数组格式
      const aliases =
        typeof record.alias === 'string' ? [record.alias] : record.alias!
        
      // 将alias变成path,有多少个alias就会添加多少条记录
      for (const alias of aliases) {
        normalizedRecords.push(
          assign({}, mainNormalizedRecord, {
            components: originalRecord
              ? originalRecord.record.components
              : mainNormalizedRecord.components,
            // 别名作为path
            path: alias,
            // 别名路由指向的原始路由记录
            aliasOf: originalRecord
              ? originalRecord.record
              : mainNormalizedRecord,
            // the aliases are always of the same kind as the original since they
            // are defined on the same record
          }) as typeof mainNormalizedRecord
        )
      }
    }
	
	// route对应的matcher
	let matcher: RouteRecordMatcher
	// 别名对应的原始matcher
    let originalMatcher: RouteRecordMatcher | undefined
	
	for (const normalizedRecord of normalizedRecords) {
      const { path } = normalizedRecord
	  // 当存在parent时,说明正在处理子路由
      // 添加子路由:处理alias没有加'/',且父路由没有以'/'结尾的情况,会拼接父路由路径作为path
      if (parent && path[0] !== '/') {
        const parentPath = parent.record.path
        const connectingSlash =
          parentPath[parentPath.length - 1] === '/' ? '' : '/'
        normalizedRecord.path =
          parent.record.path + (path && connectingSlash + path)
      }

      // 现在的版本必须用正则代替'*'匹配所有路由
      if (__DEV__ && normalizedRecord.path === '*') {
        throw new Error(
          'Catch all routes ("*") must now be defined using a param with a custom regexp.\n' +
          'See more at https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.'
        )
      }

      // 创建出matcher
      matcher = createRouteRecordMatcher(normalizedRecord, parent, options)

      if (__DEV__ && parent && path[0] === '/')
        checkMissingParamsInAbsolutePath(matcher, parent)

      // 当设置了别名alias,会再次生成一条别名对路由记录
      // 当第一次遍历原始路由记录后,originalRecord为上次原始记录,将别名路由记录放进原始记录的alias中
      if (originalRecord) {
        // 原始记录中添加别名记录
        originalRecord.alias.push(matcher)
        if (__DEV__) {
          checkSameParams(originalRecord, matcher)
        }
      } else {
        // 没有originalMatcher说明没有设置alias别名,或者正在处理原始路由记录
        originalMatcher = originalMatcher || matcher
        // 处理别名路由中,往alias中添加别名路由
        if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)

        // 检查删除掉之前添加的相同name路由
        if (isRootAdd && record.name && !isAliasRecord(matcher))
          removeRoute(record.name)
      }
	  // 如果存在子路由
      if (mainNormalizedRecord.children) {
        const children = mainNormalizedRecord.children
        for (let i = 0; i < children.length; i++) {
          addRoute(
            children[i],
            matcher,
            originalRecord && originalRecord.children[i]
          )
        }
      }

      originalRecord = originalRecord || matcher
      
      if (
        (matcher.record.components &&
          Object.keys(matcher.record.components).length) ||
        matcher.record.name ||
        matcher.record.redirect
      ) {
        // 将matcher根据分数排序,添加进matchers中
        insertMatcher(matcher)
      }
    }
    
     // 最终返回删除当前matcher的方法
     return originalMatcher
      ? () => {
        // since other matchers are aliases, they should be removed by the original matcher
        removeRoute(originalMatcher!)
      }
      : noop
}

createRouteRecordMatcher 创建matcher

最主要的是tokenizePath和tokensToParser两个方法

  • tokenizePath:解析每一个片段(指按照’/'分割的路径)的类型和内容
  • tokensToParser:根据上一步结果进行打分和生成正则等
export function createRouteRecordMatcher(
  record: Readonly<RouteRecord>, // 路由记录
  parent: RouteRecordMatcher | undefined, // 父路由记录
  options?: PathParserOptions // strict、end和sensitive这些约定路由匹配模式的属性
): RouteRecordMatcher {
  // 通过tokenizePath解析每一个片段(指按照'/'分割的路径)类型和内容,tokensToParser根据上一步结果进行打分和生成正则等
  const parser = tokensToParser(tokenizePath(record.path), options)
  const matcher: RouteRecordMatcher = assign(parser, {
    record,
    parent,
    // these needs to be populated by the parent
    children: [],
    alias: [],
  })

  if (parent) {
    if (!matcher.record.aliasOf === !parent.record.aliasOf)
      parent.children.push(matcher)
  }
  
  return matcher

}

tokenizePath 解析path

  • 会遍历path的每一个字符,根据path中的‘/’进行分割,返回[[{type: 0, value: ‘’}],[{type: 0, value: ‘d01?p0=jeff’}]]类似结果
const enum TokenizerState {
  Static, // 静态路径
  Param, // 动态路径,比如:id
  ParamRegExp, // custom re for a param
  ParamRegExpEnd, // check if there is any ? + *
  EscapeNext,
}
export function tokenizePath(path: string): Array<Token[]> {
  //...
  while (i < path.length) {
    char = path[i++]

    if (char === '\\' && state !== TokenizerState.ParamRegExp) {
      previousState = state
      state = TokenizerState.EscapeNext
      continue
    }
   
    switch (state) {
      case TokenizerState.Static:  //如果匹配到的是静态路径
        if (char === '/') {
          if (buffer) {
            consumeBuffer()
          }
          finalizeSegment()
        } else if (char === ':') { // 解析到':',说明遇到了动态路径,走动态路径的解析分支
          consumeBuffer()
          state = TokenizerState.Param
        } else {
          addCharToBuffer()
        }
        break
      case TokenizerState.Param: // 动态路径的解析
        if (char === '(') {
          state = TokenizerState.ParamRegExp
        } else if (VALID_PARAM_RE.test(char)) { // 字母或数字
          addCharToBuffer()
        } else {
          consumeBuffer()
          state = TokenizerState.Static
          // go back one character if we were not modifying
          if (char !== '*' && char !== '?' && char !== '+') i--
        }
        break
    //...
}
  • 如果路径为:‘/d0/d01?p0=jeff’,那么将会返回 [ [ {type: 0, value: ‘d0’} ], [ { type: 0, value: ‘d01?p0=jeff’ } ]]
  • 如果路径为:‘/d1/:d11’,那么将会返回 [ [ {type: 0, value: ‘d1’} ], [ { type: 1, value: ‘d11’} ] ]

tokensToParser 记录打分

  • 根据上一步分割的片段,进行打分以及生成正则
// 打分规则
const enum PathScore {
  _multiplier = 10,
  Root = 9 * _multiplier, // just /
  Segment = 4 * _multiplier, // /a-segment
  SubSegment = 3 * _multiplier, // /multiple-:things-in-one-:segment
  Static = 4 * _multiplier, // /static
  Dynamic = 2 * _multiplier, // /:someId
  BonusCustomRegExp = 1 * _multiplier, // /:someId(\\d+)
  BonusWildcard = -4 * _multiplier - BonusCustomRegExp, // /:namedWildcard(.*) we remove the bonus added by the custom regexp
  BonusRepeatable = -2 * _multiplier, // /:w+ or /:w*
  BonusOptional = -0.8 * _multiplier, // /:w? or /:w*
  // these two have to be under 0.1 so a strict /:page is still lower than /:a-:b
  BonusStrict = 0.07 * _multiplier, // when options strict: true is passed, as the regex omits \/?
  BonusCaseSensitive = 0.025 * _multiplier, // when options strict: true is passed, as the regex omits \/?
}
export function tokensToParser(
  segments: Array<Token[]>,
  extraOptions?: _PathParserOptions
): PathParser {

	const score: Array<number[]> = []
	// 针对动态path等会抽取出来,用于后续判断是否重复出现相同的path,开发环境会提示
	const keys: PathParserParamKey[] = []
	
	for (const segment of segments) {
	  // ...
	  // 如果配置了sensitive的分数
	  let subSegmentScore: number =
        PathScore.Segment +
        (options.sensitive ? PathScore.BonusCaseSensitive : 0)
	  // 如果是静态路径的分数 
      if (token.type === TokenType.Static) {
        // prepend the slash if we are starting a new segment
        if (!tokenIndex) pattern += '/'
        pattern += token.value.replace(REGEX_CHARS_RE, '\\$&')
        subSegmentScore += PathScore.Static
      } 
	  // ... 根据path生成正则等
	}
}
  • 如果路径为’/d1/:d11’,最终会返回这样的结果
{
    "re": {},
    "score": [[80],[60]],
    "keys": [
        {
            "name": "d11",
            "repeatable": false,
            "optional": false
        }
    ],
    "record": {
	 // ...
    },
    "children": [],
    "alias": []
}
  • 最终返回的matcher格式为
    在这里插入图片描述

insertMatcher 将matcher排序

function addRoute(
    record: RouteRecordRaw,  // 原始的route记录(开发者传入的)
    parent?: RouteRecordMatcher,// 当存在子路由时,parent才会有,第一次因为是创建一级路由,所以为空
    originalRecord?: RouteRecordMatcher // alais别名路由对应的原始记录
  ) {
  
  // ...
  matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
  
  // ...
  if (
    (matcher.record.components &&
      Object.keys(matcher.record.components).length) ||
    matcher.record.name ||
    matcher.record.redirect
  ) {
    insertMatcher(matcher)
  }
}

insertMatcher

  function insertMatcher(matcher: RouteRecordMatcher) {
    let i = 0
    // 对路由记录根据score进行排序,i代表该路由记录排序后的位置
    while (
      i < matchers.length &&
      comparePathParserScore(matcher, matchers[i]) >= 0 && // comparePathParserScore 是个比较器
      // 子路由为空路径时,排序应该在父路由前面
      (matcher.record.path !== matchers[i].record.path ||
        !isRecordChildOf(matcher, matchers[i]))
    )
      i++
    // 将路由记录插入matchers中
    matchers.splice(i, 0, matcher)
    // matcherMap 中只存放原始记录
    if (matcher.record.name && !isAliasRecord(matcher))
      matcherMap.set(matcher.record.name, matcher)
  }

总结

  • vue-router会将开发者传入的routes配置转换成一条条matcher,而matcher会根据你path的类型进行打分排序,后续匹配时会优先匹配分数高的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值