备案 控制台
学习
实践
活动
专区
工具
TVP
写文章
专栏首页 南山种子外卖跑手的专栏 vue router 4 源码篇:路由matcher的前世今生
5 0

海报分享

原创

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

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

源码专栏

感谢大家继续阅读 《Vue Router 4 源码探索系列》 专栏,你可以在下面找到往期文章:

《vue router 4 源码篇:路由诞生——createRouter原理探索》 《vue router 4 源码篇:路由matcher的前世今生》 《vue router 4 源码篇:router history的原生结合》 《vue router 4 源码篇:导航守卫该如何设计(一)》

开篇

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

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

最终输出

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

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

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

输出:

image.png

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

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

处理流程

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

image.png

addRoute

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

扩展阅读: 别名路由

addRoute关键步骤源码

image.png

addRoute的处理过程

image.png

流程拆分

标准化处理record和options合并

// 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

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 数组了。

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

// 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中;没有则认为第一个 record originalMatcher ,而其他则是当前路由的 aliases ,这里要注意点是当 originalMatcher matcher 不等时,说明此时matcher是由别名记录产生的,将matcher放到originalMatcher的 aliases 中。再往后就是为了避免嵌套调用而删掉不冗余路由。

遍历子路由

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

// 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 定义:

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]))
  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 ,这个在外层调用时已经处理好)
  • 返回:标准化的路由对象

举例

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

export const router = createRouter(options)
const matchers = createRouterMatcher(options.routes, options)
console.log('obj:', matchers)

输出:

image.png

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

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

关键源码

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)
  • 返回:无

源码

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)