前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >vue router 4 源码篇:路由matcher的前世今生

vue router 4 源码篇:路由matcher的前世今生

原创
作者头像
南山种子外卖跑手
发布2022-10-11 14:44:07
1.9K0
发布2022-10-11 14:44:07
举报
文章被收录于专栏:南山种子外卖跑手的专栏

本文为原创文章,引用请注明出处,欢迎大家收藏和分享💐💐

源码专栏

感谢大家继续阅读《Vue Router 4 源码探索系列》专栏,你可以在下面找到往期文章: 《vue router 4 源码篇:路由诞生——createRouter原理探索》 《vue router 4 源码篇:路由matcher的前世今生》 《vue router 4 源码篇:router history的原生结合》 《vue router 4 源码篇:导航守卫该如何设计(一)》

开篇

哈喽大咖好,我是跑手,本次给大家继续讲解下vue-router@4.xrouter matcher的实现。

在上节讲到,createRouter方法的第一步就是根据传进来的路由配置列表,为每项创建matcher。这里的matcher可以理解成一个路由页面的匹配器,包含了路由常规方法。而创建matcher,调用了createRouterMatcher方法。

最终输出

createRouterMatcher执行完后,会返回的5个函数{ addRoute, resolve, removeRoute, getRoutes, getRecordMatcher },为后续的路由创建提供帮助。这些函数的作用,无非就是围绕着上面说到的matcher增删改查操作,例如,getRoutes用于返回所有matcher,removeRoute则是删除某个指定的matcher。。。

为了方便大家阅读,我们先看下创建的matcher最终长啥样?我们可以使用getRoutes()方法获取到的对象集,得到最终生成的matcher列表:

代码语言:typescript
复制
import {
  createRouterMatcher,
  createWebHistory,
} from 'vue-router'

export const routerHistory = createWebHistory()
const options = { 
    // your options... 
}
console.log('matchers:', createRouterMatcher(options.routes, options).getRoutes())

输出:

image.png
image.png

其中,record字段就是我们经常使用到的vue-router路由对象(即router.getRoute()得到的对象),这样理解方便多了吧 \手动狗头。。。

接下来,我们分别对**addRoute, resolve, removeRoute, getRoutes, getRecordMatcher**这5个方法解读,全面了解**vue router**是如何创建matcher的。

处理流程

讲了一大堆,还是回归到源码。createRouterMatcher函数一共286行,初始化matcher入口在代码340行,调用的方法是addRoute

image.png
image.png

addRoute

  • 定义:初始化matcher
  • 接收参数(3个):record(需要处理的路由)、parent(父matcher)、originalRecord(原始matcher),其中后两个是可选项,意思就是只传record则会认为是一个简单路由「无父无别名」并对其处理,假如带上第2、3参数,则还要结合父路由或者别名路由处理
  • 返回:单个matcher对象

扩展阅读:别名路由

addRoute关键步骤源码

image.png
image.png

addRoute的处理过程

image.png
image.png

流程拆分

标准化处理record和options合并

代码语言:typescript
复制
// used later on to remove by name
const isRootAdd = !originalRecord
const mainNormalizedRecord = normalizeRouteRecord(record)
if (__DEV__) {
  checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent)
}
// we might be the child of an alias
mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
const options: PathParserOptions = mergeOptions(globalOptions, record)
// generate an array of records to correctly handle aliases
const normalizedRecords: typeof mainNormalizedRecord[] = [
  mainNormalizedRecord,
]

在执行过程中,先对record调用normalizeRouteRecord进行标准化处理,再调用mergeOptions方法把自身options与全局options合并得到最终options,然后把结果放进normalizedRecords数组存储。

再讲解下normalizedRecords,它是一个存储标准化matcher的数组,数组每一项都包含是matcher所有信息:options、parent、compoment、alias等等。。。在接下来要对matcher进行完成初始化的流程中,只要遍历这个数组就行了。

处理alias

代码语言:typescript
复制
if ('alias' in record) {
  const aliases =
    typeof record.alias === 'string' ? [record.alias] : record.alias!
  for (const alias of aliases) {
    normalizedRecords.push(
      assign({}, mainNormalizedRecord, {
        // this allows us to hold a copy of the `components` option
        // so that async components cache is hold on the original record
        components: originalRecord
          ? originalRecord.record.components
          : mainNormalizedRecord.components,
        path: alias,
        // we might be the child of an 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
    )
  }
}

然后就是处理别名路由,如果record设置了别名,则把原record(也就是传进来的第三个参数),当然这些信息也要塞进normalizedRecords数组保存,以便后续对原record处理。

扩展阅读:vue router alias

生成路由匹配器

万事俱备,接下来就要遍历normalizedRecords数组了。

代码语言:typescript
复制
const { path } = normalizedRecord
// Build up the path for nested routes if the child isn't an absolute
// route. Only add the / delimiter if the child path isn't empty and if the
// parent path doesn't have a trailing slash
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.'
  )
}

// create the object beforehand, so it can be passed to children
matcher = createRouteRecordMatcher(normalizedRecord, parent, options)

首先,生成普通路由和嵌套路由的path,然后调用createRouteRecordMatcher 方法生成一个路由匹配器,至于createRouteRecordMatcher内部逻辑这里就不细述了(以后有时间再补充),大概思路就是通过编码 | 解码将路由path变化到一个token数组的过程,让程序能准确辨认并处理子路由、动态路由、路由参数等情景。

处理originalRecord

代码语言:typescript
复制
// if we are an alias we must tell the original record that we exist,
// so we can be removed
if (originalRecord) {
  originalRecord.alias.push(matcher)
  if (__DEV__) {
    checkSameParams(originalRecord, matcher)
  }
} else {
  // otherwise, the first record is the original and others are aliases
  originalMatcher = originalMatcher || matcher
  if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)

  // remove the route if named and only for the top record (avoid in nested calls)
  // this works because the original record is the first one
  if (isRootAdd && record.name && !isAliasRecord(matcher))
    removeRoute(record.name)
}

完成上一步后,程序会对originalRecord做判断,如果有则将匹配器(matcher)放入alias中;没有则认为第一个recordoriginalMatcher,而其他则是当前路由的aliases,这里要注意点是当originalMatchermatcher不等时,说明此时matcher是由别名记录产生的,将matcher放到originalMatcher的aliases中。再往后就是为了避免嵌套调用而删掉不冗余路由。

遍历子路由

代码语言:typescript
复制
if (mainNormalizedRecord.children) {
  const children = mainNormalizedRecord.children
  for (let i = 0; i < children.length; i++) {
    addRoute(
      children[i],
      matcher,
      originalRecord && originalRecord.children[i]
    )
  }
}

再往下就是遍历当前matcher的children matcher做同样的初始化操作。

插入matcher

代码语言:typescript
复制
// if there was no original record, then the first one was not an alias and all
// other aliases (if any) need to reference this record when adding children
originalRecord = originalRecord || matcher

// TODO: add normalized records for more flexibility
// if (parent && isAliasRecord(originalRecord)) {
//   parent.children.push(originalRecord)
// }

insertMatcher(matcher)

再看看insertMatcher定义:

代码语言:typescript
复制
function insertMatcher(matcher: RouteRecordMatcher) {
  let i = 0
  while (
    i < matchers.length &&
    comparePathParserScore(matcher, matchers[i]) >= 0 &&
    // Adding children with empty path should still appear before the parent
    // https://github.com/vuejs/router/issues/1124
    (matcher.record.path !== matchers[i].record.path ||
      !isRecordChildOf(matcher, matchers[i]))
  )
    i++
  matchers.splice(i, 0, matcher)
  // only add the original record to the name map
  if (matcher.record.name && !isAliasRecord(matcher))
    matcherMap.set(matcher.record.name, matcher)
}

源码在添加matcher前还要对其判断,以便重复插入。当满足条件时,将matcher增加到matchers数组中;另外,假如matcher并非别名record时,也要将其记录到matcherMap中,matcherMap作用是通过名字快速检索到对应的record对象,在增加、删除、查询路由时都会用到。

至此addRoute逻辑基本完结了,最后返回original matcher集合,得到文中开头截图的matchers。

resolve

  • 定义:获取路由的标准化版本
  • 入参2个:location路由路径对象,可以是path 或 name与params的组合;currentLocation当前路由matcher location,这个在外层调用时已经处理好)
  • 返回:标准化的路由对象

举例

方便大家理解,这里还是先举个例子:

代码语言:typescript
复制
export const router = createRouter(options)
const matchers = createRouterMatcher(options.routes, options)
console.log('obj:', matchers)

输出:

image.png
image.png

这里大家可能会有个疑问,假如2个参数的路由不一致会以哪个为准?

其实这是个伪命题,matcher内部的resolve方法和平时我们外部调用的router resolve方法不一样,内部这个resolve的2入参数默认指向同一个路由而不管外部的业务逻辑如何,在外部router resolve已经把第二个参数处理好,所以才有上面截图的效果。

关键源码

代码语言:typescript
复制
function resolve(
  location: Readonly<MatcherLocationRaw>,
  currentLocation: Readonly<MatcherLocation>
): MatcherLocation {
  let matcher: RouteRecordMatcher | undefined
  let params: PathParams = {}
  let path: MatcherLocation['path']
  let name: MatcherLocation['name']

  if ('name' in location && location.name) {
    // match by name
  } else if ('path' in location) {
    // match by path
  } else {
    // match by name or path of current route...
  }

  const matched: MatcherLocation['matched'] = []
  let parentMatcher: RouteRecordMatcher | undefined = matcher
  while (parentMatcher) {
    // reversed order so parents are at the beginning

    matched.unshift(parentMatcher.record)
    parentMatcher = parentMatcher.parent
  }

  return {
    name,
    path,
    params,
    matched,
    meta: mergeMetaFields(matched),
  }
}

上面为省略源码,无非就是通过3种方式(通过name、path、当前路由的name或path)查找matcher,最后返回一个完整的信息对象。

removeRoute

  • 定义:删除某个路由matcher
  • 入参:matcherRef(路由标识,可以是字符串或object)
  • 返回:无

源码

代码语言:typescript
复制
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
  if (isRouteName(matcherRef)) {
    const matcher = matcherMap.get(matcherRef)
    if (matcher) {
      matcherMap.delete(matcherRef)
      matchers.splice(matchers.indexOf(matcher), 1)
      matcher.children.forEach(removeRoute)
      matcher.alias.forEach(removeRoute)
    }
  } else {
    const index = matchers.indexOf(matcherRef)
    if (index > -1) {
      matchers.splice(index, 1)
      if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name)
      matcherRef.children.forEach(removeRoute)
      matcherRef.alias.forEach(removeRoute)
    }
  }
}

删除路由matcher逻辑也不复杂,先干掉本路由matcher,然后再递归干掉其子路由和别名路由。

getRoutes

  • 定义:获取所有的matchers
  • 入参:无
  • 返回:matchers

源码

代码语言:typescript
复制
function getRoutes() {
  return matchers
}

getRecordMatcher

  • 定义:获取某个matcher
  • 返回:matcher

源码

代码语言:typescript
复制
function getRecordMatcher(name: RouteRecordName) {
  return matcherMap.get(name)
}

上面说过,matcherMap是一个map结构的内存变量,能通过name快速检索到指定的matcher。

落幕

好了,相信小伙伴们都对vue router 4matcher有总体的认识和理解,这节先到这里,下节我们会聊下vue router 4中核心能力之一:源码中有关Web History API能力的部分,看看它是如何把原生能力完美结合起来的。

最后感谢大家阅览并欢迎纠错,欢迎大家关注本人公众号「似马非马」,一起玩耍起来!🌹🌹

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 源码专栏
  • 开篇
  • 最终输出
  • 处理流程
  • addRoute
    • addRoute关键步骤源码
      • addRoute的处理过程
        • 流程拆分
          • 标准化处理record和options合并
          • 处理alias
          • 生成路由匹配器
          • 处理originalRecord
          • 遍历子路由
          • 插入matcher
      • resolve
        • 举例
          • 关键源码
          • removeRoute
            • 源码
            • getRoutes
              • 源码
              • getRecordMatcher
                • 源码
                • 落幕
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档