vue-router源码阅读-2.如何玩转route

388 阅读7分钟

什么是route

上一篇文章中介绍了#createRouter, 知道了在其返回的router中有一部分用于操作route的方法, 为了明白这些方法到底在做什么, 首先需要知道到底什么是route.

先来查看#addRoute的方法实现

// 添加一个新路由
function addRoute(
  parentOrRoute: RouteRecordName | RouteRecordRaw,
  route?: RouteRecordRaw
){
  let parent: Parameters<typeof matcher['addRoute']>[1] | undefined
  let record: RouteRecordRaw
  if (isRouteName(parentOrRoute)) {
    parent = matcher.getRecordMatcher(parentOrRoute)
    record = route!
  } else {
    record = parentOrRoute
  }
   return matcher.addRoute(record, parent)
}

其中最核心的功能代码为return matcher.addRoute(record, parent), 通过仔细阅读代码, 可以发现#addRoute中的route是一个RouterRecordRaw对象, 其实现添加逻辑的核心在于调用了matcher#addRouer. 我们已经抓住了理清route结构的线头, 接下来先来看下RouterRecordRaw的结构.

export type RouteRecordRaw =
  | RouteRecordSingleView
  | RouteRecordSingleViewWithChildren
  | RouteRecordMultipleViews
  | RouteRecordMultipleViewsWithChildren
  | RouteRecordRedirect

通过代码, 发现RouterRecordRaw是一个类型集合, 看起来十分复杂, 但只要仔细查看就会发现他们都拓展自同一个类型_RouteRecordBase. 然后在该类型的基础上控制一些字段的组合.

export interface _RouteRecordBase extends PathParserOptions {
  path: string
  redirect?: RouteRecordRedirectOption
  alias?: string | string[]
  name?: RouteRecordName
  beforeEnter?:
    | NavigationGuardWithThis<undefined>
    | NavigationGuardWithThis<undefined>[]
  meta?: RouteMeta
  children?: RouteRecordRaw[]
  props?: _RouteRecordProps | Record<string, _RouteRecordProps>
}
export interface RouteRecordSingleView extends _RouteRecordBase {
  component: RawRouteComponent
  components?: never
  children?: never
  redirect?: never
  props?: _RouteRecordProps
}

通过阅读以上代码, 我们基本可以确定, RouterRecordRaw就是我们定义的一个页面路由. 通常是这样的.

const addressRoute: RouteRecordRaw[] = [
  {
    path: "/address",
    name: "addressList",
    component: Layout,
    meta: { hidden: false },
    redirect: "/address/list",
    children: [
      {
        path: "list",
        component: () => import("@/views/address/index.vue"),
        meta: { hidden: false, title: "地址管理", keepAlive: false },
      },
    ],
  },
];

在知道了route是什么后, 我们需要解决另一个问题, matcher是什么? 为什么实现添加route需要调用他的方法.

什么又是matcher

在阅读了#addRoute的代码后, 发现其调用matcher#addRouter来实现添加路由逻辑. 因此可以大胆假设, router中所有对route的操作都是通过matcher进行的. 接下来我们进行小心求证. 已知router中关于route操作的方法共有4个, 分别是#addRoute, #removeRoute, #getRoute#hasRoute. 其中#addRoute方法已经确定了, 接下来依次检查其他三个方法的实现.

  • #removeRoute
// 移除路由
function removeRoute(name: RouteRecordName) {
  const recordMatcher = matcher.getRecordMatcher(name)
  if (recordMatcher) {
    matcher.removeRoute(recordMatcher)
  } else if (__DEV__) {
    warn(`Cannot remove non-existent route "${String(name)}"`)
  }
}
  • #getRoutes
// 获得所有路由
function getRoutes() {
  return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
}
  • #hasRoute
// 判断指定路由是否存在
function hasRoute(name: RouteRecordName): boolean {
  return !!matcher.getRecordMatcher(name)
}

通过查看代码, 我们并不惊讶的发现, 所有关于route的操作都是对matcher相关方法的二次封装. 那么matcher到底是何方神圣呢?

揭开matcher的面纱

createRouter的开头, matcher就暴露了自己.

// 创建路由匹配器
const matcher = createRouterMatcher(options.routes, options)

可以看到他是#createRouterMatcher接收options参数对象后的返回值. 那么我们就有必要看一下#createRouterMatcher又是什么了.

export function createRouterMatcher(
  routes: Readonly<RouteRecordRaw[]>,
  globalOptions: PathParserOptions
): RouterMatcher 

从函数签名中可以发现, #createRouterMatcher返回一个RouterMatcher.

export interface RouterMatcher {
  addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void
  removeRoute: {
    (matcher: RouteRecordMatcher): void
    (name: RouteRecordName): void
  }
  getRoutes: () => RouteRecordMatcher[]
  getRecordMatcher: (name: RouteRecordName) => RouteRecordMatcher | undefined

  resolve: (
    location: MatcherLocationRaw,
    currentLocation: MatcherLocation
  ) => MatcherLocation
}

RouterMathcer是一个接口, 定义了对route的操作. 继续阅读#createRouterMatcher代码如下

// matchers 存放所有路由记录
const matchers: RouteRecordMatcher[] = []
// matcherMap 建立路由标识和对应路由的匹配
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()

#createRouterMatcher里, 首先创建了一个RouteRecordMatcher数组和一个键为RouteRecordName, 值为RouteRecordMatchermap. 这两个类型如同其名字一样, 分别是路由记录标识和路由记录匹配器. RouteRecordName是一个stringsymbol, RouteRecordMatcher的具体结构暂且不表.

我们可以看到, #createRouterMatcher并没有暴露matchersmatcherMap, 而是形成了一个闭包. 接着我们继续阅读暴露出去的方法.

  • 首先阅读两个简单的, #getRouters#hasRoute
// 获得路由数组matchers
function getRoutes() {
  return matchers
}

// 判断指定路由是否存在
function hasRoute(name: RouteRecordName): boolean {
  return !!matcher.getRecordMatcher(name)
}

function getRecordMatcher(name: RouteRecordName) {
  return matcherMap.get(name)
}

可以看到#getRouters只是简单的返回了matchers, 而#hasRoute则调用了matcher#getRecordMatcher来判断路由是否存在, matcher#getRecordMatcher则是通过matcherMap#get来进行判断的. 这样我们就可以清楚的明白matchers和matcherMap的作用了, 其中matchers是存放所有路由记录的数组, matcherMap是通过标识来快速索引路由记录的一个map.

  • 然后是#removeRoute
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
  // 判断matcherRef的类型
  if (isRouteName(matcherRef)) {
    // 如果是RouteRecordName需要先get获取对应的RouteRecordMatcher
    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)
    }
  }
}

#removeRoute实现的功能就是将和matcherRef匹配的RouteRecordMatchermatchersmatcherMap中删除, 同时如果其有子路由或别名路由, 也要一并删除.

  • #addRoute, 相对比较复杂
function addRoute(
  record: RouteRecordRaw,
  parent?: RouteRecordMatcher,
  originalRecord?: RouteRecordMatcher
) {
  // 如果传入了originalRecord, 即该路由是别名路由时, isRootAdd为假, 否则为真
  const isRootAdd = !originalRecord;
  // 标准化路由记录
  const mainNormalizedRecord = normalizeRouteRecord(record);

  // 开发环境下, 检查路由是否是一个没有name属性的空路径子路由, 如果是, 报错
  if (__DEV__) {
    checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent);
  }

  // aliasOf 表示此记录是否是其他路由记录的别名, 如果不是, 其值为 undefined
  mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record;
  const options: PathParserOptions = mergeOptions(globalOptions, record);
  // 生成一个数组用于处理别名, 初始值为待添加的标准化路由
  const normalizedRecords: typeof mainNormalizedRecord[] = [
    mainNormalizedRecord,
  ];

  // 当要添加的record拥有别名时
  if ("alias" in record) {
    const aliases =
      typeof record.alias === "string" ? [record.alias] : record.alias!;
    // 遍历别名数组, 根据别名记录创建记录存储到 normalizedRecords 中
    for (const alias of aliases) {
      normalizedRecords.push(
        assign({}, mainNormalizedRecord, {
          components: originalRecord
            ? originalRecord.record.components
            : mainNormalizedRecord.components,
          path: alias,
          // 如果有原始记录, aliasOf 为原始记录, 否则为其本身
          aliasOf: originalRecord
            ? originalRecord.record
            : mainNormalizedRecord,
        }) as typeof mainNormalizedRecord
      );
    }
  }

  let matcher: RouteRecordMatcher;
  let originalMatcher: RouteRecordMatcher | undefined;

  // 遍历 normalizedRecords
  for (const normalizedRecord of normalizedRecords) {
    const { path } = normalizedRecord;
    // 当父路由存在且子路由不以 '/' 开头, 当父路由不以 '/' 结尾时需要在父路由末尾添加一个 '/'
    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 = createRouteRecordMatcher(normalizedRecord, parent, options);

    // 检查是否有声明了却没传递的参数
    if (__DEV__ && parent && path[0] === "/")
      checkMissingParamsInAbsolutePath(matcher, parent);

    // 当该路由为别名路由时, 需要将其添加进源路由的别名中用于之后的路由移除
    if (originalRecord) {
      originalRecord.alias.push(matcher);
      if (__DEV__) {
        checkSameParams(originalRecord, matcher);
      }
    } else {
      // 否则, 第一个record就是源路由, 而其他路由是别名路由(如果存在)
      // 仅当originalMatcher为空时进行第一次赋值, 之后originalMatcher均不为空
      // 达到了将normalizedRecords数组的第一个路由赋值给originalMatcher的目的
      originalMatcher = originalMatcher || matcher;
      // 将其他路由添加进源路由的别名路由数组中
      if (originalMatcher !== matcher) originalMatcher.alias.push(matcher);

      // 移除已存在的命名路由
      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;

    // 将新路由插入到matcers和matcherMap中
    insertMatcher(matcher);
  }

  // 如果没有源routerMatcher, 那么返回一个空函数
  // 返回一个可以将源路由 matcher 删除的方法
  return originalMatcher
    ? () => {
        removeRoute(originalMatcher!);
      }
    : noop;
}

虽然addRouter看起来一大坨代码很复杂, 但可以对其核心逻辑进行简化

  1. 将传入的record进行格式化, 将格式化后的路由记录添加到normalizedRecords中.
  2. 处理record的别名情况, 将别名路由也添加到normalizedRecords中.
  3. 遍历normalizedRecords, 使用#createRouteRecordMatcher为每一个格式化后的路由记录normalizedRecord创建一个路由记录匹配器matcher.
  4. 记录别名路由, 用于之后的删除
  5. 递归添加normalizedRecord的子路由
  6. 调用#insertMatchermatcher插入到matchersmatcherMap中.

整个过程中有三个关键的方法, 分别是

  1. normalizeRouteRecord
  2. createRouteRecordMatcher
  3. insertMatcher

其中#insertMatcher是在#createRouterMatcher中定义的函数, 先阅读其代码

function insertMatcher(matcher: RouteRecordMatcher) {
  let i = 0;
  while (
    i < matchers.length &&
    // mather和mathers[i]进行比较
    comparePathParserScore(matcher, matchers[i]) >= 0 &&
    // mather的path与matcher[i]不同或mather不是matcher[i]的孩子
    (matcher.record.path !== matchers[i].record.path ||
      !isRecordChildOf(matcher, matchers[i]))
  )
    i++;
  matchers.splice(i, 0, matcher);
  // 仅当matcher不是一个别名路由时才添加进matcherMap中
  if (matcher.record.name && !isAliasRecord(matcher))
    matcherMap.set(matcher.record.name, matcher);
}

其逻辑即为通过比较将传入的matcher插入到mathersmatcherMap中. 剩下的#createRouteRecordMatcher作用为将路由记录的path属性格式化, 用于比较不同路由记录间的优先度. #normalizeRouteRecord的作用为将传入的路由记录格式化, 为其缺失的属性添加默认值. #createRouteRecordMatcher涉及到路径的格式化, 因此留到之后讲解.

export function normalizeRouteRecord(
  record: RouteRecordRaw
): RouteRecordNormalized {
  return {
    path: record.path,
    redirect: record.redirect,
    name: record.name,
    meta: record.meta || {},
    aliasOf: undefined,
    beforeEnter: record.beforeEnter,
    props: normalizeRecordProps(record),
    children: record.children || [],
    insveGuards: new Set(),
    updtances: {},
    leaateGuards: new Set(),
    enterCallbacks: {},
    components:
      'components' in record
        ? record.components || null
        : record.component && { default: record.component },
  }
}

在定义好这些操作route的方法后, #createRouterMatcher对传入的routes进行遍历, 调用#addRoute将每个传入的route添加到matchersmatcherMap中. 然后将这些方法暴露出去供router进行调用.

// 添加所有接收的route到 matchers 和 matcherMap 中
routes.forEach(route => addRoute(route))
// 返回定义的操作
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }

总结

本篇文章介绍了vue-router中route的存储方式和基本的操作方法实现逻辑.