React router源码解析(二)

376 阅读15分钟

React Router基于monorepo的架包(指在一个项目仓库(repo)中管理多个模块/包(package))。

  • react-router:React Router的核心基本功能,为react-router-dom和react-router-native服务;
  • react-router-dom:在web应用使用React Router的方法;
  • react-router-native:在RN中使用React Router的方法;
  • react-router-dom-v5-compat:V5迁移至V6的垫片;

这里主要总结react-router核心库

1. react-router

与运行环境无关,几乎所有运行平台无关的方法、组件和hooks都是在这里定义的

  • index.ts:入口文件,且标识了三个不安全的API,要使用的话,不要单独从lib/context.ts引入,要从react-router的入口文件引入(虽然一般开发中用不到 )。
/** @internal */
export {
  NavigationContext as UNSAFE_NavigationContext,
  LocationContext as UNSAFE_LocationContext,
  RouteContext as UNSAFE_RouteContext,
};

1.1 router

Router在react-router内部主要用于提供全局的路由导航对象(一般由history库提供)以及当前的路由导航状态,在项目中使用时一般是必须并且唯一的,不过一般不会直接使用,更多会使用已经封装好的路由导航对象的BrowserRouter(react-router-dom包引入)、HashRouter(react-router-dom包引入)和MemoryRouter(react-router包引入)

  • router context
import React from 'react'
import type {
  History,
  Location,
} from "history";
import {
  Action as NavigationType,
} from "history";

// 只包含,go、push、replace、createHref 四个方法的 History 对象,用于在 react-router 中进行路由跳转
export type Navigator = Pick<History, "go" | "push" | "replace" | "createHref">;

interface NavigationContextObject {
  basename: string;
  navigator: Navigator;
  static: boolean;
}

/**
 * 内部含有 navigator 对象的全局上下文,官方不推荐在外直接使用
 */
const NavigationContext = React.createContext<NavigationContextObject>(null!);


interface LocationContextObject {
  location: Location;
  navigationType: NavigationType;
}
/**
 * 内部含有当前 location 与 action 的 type,一般用于在内部获取当前 location,官方不推荐在外直接使用
 */
const LocationContext = React.createContext<LocationContextObject>(null!);

// 这是官方对于上面两个 context 的导出,可以看到都是被定义为不安全的,并且可能会有着重大更改,强烈不建议使用
/** @internal */
export {
  NavigationContext as UNSAFE_NavigationContext,
  LocationContext as UNSAFE_LocationContext,
};
  • Hooks:基于LocationContext的三个 hooks
    • useInRouterContext
    • useNavigationType
    • useLocation
/**
* 断言方法
*/
function invariant(cond: any, message: string): asserts cond {
  if (!cond) throw new Error(message);
}

/**
* 判断当前组件是否在一个 Router 中
*/
export function useInRouterContext(): boolean {
  return React.useContext(LocationContext) != null;
}
/**
* 获取当前的跳转的 action type
*/
export function useNavigationType(): NavigationType {
  return React.useContext(LocationContext).navigationType;
}
/**
* 获取当前跳转的 location
*/
export function useLocation(): Location {
  // useLocation 必须在 Router 提供的上下文中使用
  invariant(
    useInRouterContext(),
    // TODO: This error is probably because they somehow have 2 versions of the
    // router loaded. We can help them understand how to avoid that.
    `useLocation() may be used only in the context of a <Router> component.`
  );
  
  return React.useContext(LocationContext).location;
}
  • 定义Router组件

传入Context与外部传入的location

// 接上面,这里额外还从 history 中引入了 parsePath 方法
import {
  parsePath
} from "history";

export interface RouterProps {
  // 路由前缀
  basename?: string;
  children?: React.ReactNode;
  // 必传,当前 location
  /*
      interface Location {
            pathname: string;
            search: string;
            hash: string;
            state: any;
            key: string;
      }
  */
  location: Partial<Location> | string;
  // 当前路由跳转的类型,有 POP,PUSH 与 REPLACE 三种
  navigationType?: NavigationType;
  // 必传,history 中的导航对象,我们可以在这里传入统一外部的 history
  navigator: Navigator;
  // 是否为静态路由(ssr)
  static?: boolean;
}

/**
 * 提供渲染 Route 的上下文,但是一般不直接使用这个组件,会包装在 BrowserRouter 等二次封装的路由中
 * 整个应用程序应该只有一个 Router
 * Router 的作用就是格式化传入的原始 location 并渲染全局上下文 NavigationContext、LocationContext
 */
export function Router({
  basename: basenameProp = "/",
  children = null,
  location: locationProp,
  navigationType = NavigationType.Pop,
  navigator,
  static: staticProp = false
}: RouterProps): React.ReactElement | null {
  // 断言,Router 不能在其余 Router 内部,否则抛出错误
  invariant(
    !useInRouterContext(),
    `You cannot render a <Router> inside another <Router>.` +
      ` You should never have more than one in your app.`
  );
  // 格式化 basename,去掉 url 中多余的 /,比如 /a//b 改为 /a/b
  let basename = normalizePathname(basenameProp);
  // 全局的导航上下文信息,包括路由前缀,导航对象等
  let navigationContext = React.useMemo(
    () => ({ basename, navigator, static: staticProp }),
    [basename, navigator, staticProp]
  );

  // 转换 location,传入 string 将转换为对象
  if (typeof locationProp === "string") {
    // parsePath 用于将 locationProp 转换为 Path 对象,都是 history 库引入的
    /*
        interface Path {
              pathname: string;
              search: string;
              hash: string;
        }
    */
    locationProp = parsePath(locationProp);
  }

  let {
    pathname = "/",
    search = "",
    hash = "",
    state = null,
    key = "default"
  } = locationProp;

  // 经过抽离 base 后的真正的 location,如果抽离 base 失败返回 null
  let location = React.useMemo(() => {
    // stripBasename 用于去除 pathname 前面 basename 部分
    let trailingPathname = stripBasename(pathname, basename);

    if (trailingPathname == null) {
      return null;
    }

    return {
      pathname: trailingPathname,
      search,
      hash,
      state,
      key
    };
  }, [basename, pathname, search, hash, state, key]);

  if (location == null) {
    return null;
  }

  return (
    // 唯一传入 location 的地方
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider
        children={children}
        value={{ location, navigationType }}
      />
    </NavigationContext.Provider>
  );
}
  
// 格式化方法
/**
 * 格式化 pathname
 * @param pathname
 * @returns
 */
const normalizePathname = (pathname: string): string =>
  pathname.replace(//+$/, "").replace(/^/*/, "/");

/**
 *
 * 抽离 basename,获取纯粹的 path,如果没有匹配到则返回 null
 * @param pathname
 * @param basename
 * @returns
 */
function stripBasename(pathname: string, basename: string): string | null {
  if (basename === "/") return pathname;

  // 如果 basename 与 pathname 不匹配,返回 null
  if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
    return null;
  }

  // 上面只验证了是否 pathname 包含 basename,这里还需要验证包含 basename 后第一个字母是否为 /,不为 / 证明并不是该 basename 下的路径,返回 null
  let nextChar = pathname.charAt(basename.length);
  if (nextChar && nextChar !== "/") {
    return null;
  }

  // 返回去除掉 basename 的 path
  return pathname.slice(basename.length) || "/";
}
  • memory router封装

其实就是将history库与我们声明的Router组件绑定起来,当history.listen 监听到路由改变后重新设置当前的location 与 action。

import type { InitialEntry, MemoryHistory } from 'history';
import { createMemoryHistory } from 'history';

export interface MemoryRouterProps {
  // 路由前缀
  basename?: string;
  children?: React.ReactNode;
  // 与 createMemoryHistory 返回的 history 对象参数相对应,代表的是自定义的页面栈与索引
  initialEntries?: InitialEntry[];
  initialIndex?: number;
}

/**
 * react-router 里面只有 MemoryRouter,其余的 router 在 react-router-dom 里
 */
export function MemoryRouter({
  basename,
  children,
  initialEntries,
  initialIndex
}: MemoryRouterProps): React.ReactElement {
  // history 对象的引用
  let historyRef = React.useRef<MemoryHistory>();
  if (historyRef.current == null) {
    // 创建 memoryHistory
    historyRef.current = createMemoryHistory({ initialEntries, initialIndex });
  }

  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location
  });

  // 监听 history 改变,改变后重新 setState
  React.useLayoutEffect(() => history.listen(setState), [history]);

  // 简单的初始化并将相应状态与 React 绑定
  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  );
}
  • 总结:
    • Router组件是react-router应用中必不可少的,一般直接写在应用最外层,它提供了一系列关于路由跳转和状态的上下文属性和方法;
    • 一般不会直接使用Router组件,而是使用react-router内部提供的高阶Router组件,而这些高阶组件实际上就是将history库中提供的导航对象与Router组件连接起来,进而控制应用的导航状态;

1.2 route

举个例子:

import { render } from "react-dom";
import {
  BrowserRouter,
  Routes,
  Route
} from "react-router-dom";
// 这几个页面不用管它
import App from "./App";
import Expenses from "./routes/expenses";
import Invoices from "./routes/invoices";

const rootElement = document.getElementById("root");
render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<App />} />
      <Route path="/expenses" element={<Expenses />} />
      <Route path="/invoices" element={<Invoices />} />
    </Routes>
  </BrowserRouter>,
  rootElement
);
  • props

route在react-router中只是提供命令式的路由配置的方式

// Route 有三种 props 类型,这里先了解内部参数的含义,下面会细讲
export interface PathRouteProps {
  caseSensitive?: boolean;
  // children 代表子路由
  children?: React.ReactNode;
  element?: React.ReactNode | null;
  index?: false;
  path: string;
}

export interface LayoutRouteProps {
  children?: React.ReactNode;
  element?: React.ReactNode | null;
}

export interface IndexRouteProps {
  element?: React.ReactNode | null;
  index: true;
}

/**
 * Route 组件内部没有进行任何操作,仅仅只是定义 props,而我们就是为了使用它的 props
 */
export function Route(
  _props: PathRouteProps | LayoutRouteProps | IndexRouteProps
): React.ReactElement | null {
  // 这里可以看出 Route 不能够被渲染出来,渲染会直接抛出错误,证明 Router 拿到 Route 后也不会在内部操作
  invariant(
    false,
    `A <Route> is only ever to be used as the child of <Routes> element, ` +
      `never rendered directly. Please wrap your <Route> in a <Routes>.`
  );
}
  • 总结
    • Route可以被看作一个挂载用户传入参数的对象,它不会在页面中渲染,而是会被Routes接受并解析;

1.3 routes

export interface RoutesProps {
  children?: React.ReactNode;
  // 用户传入的 location 对象,一般不传,默认用当前浏览器的 location
  location?: Partial<Location> | string;
}

/**
 * 所有的 Route 都需要 Routes 包裹,用于渲染 Route(拿到 Route 的 props 的值,不渲染真实的 DOM 节点)
 */
export function Routes({
  children,
  location
}: RoutesProps): React.ReactElement | null {
  return useRoutes(createRoutesFromChildren(children), location);
}
  • createRoutesFromChildren
// 路由配置对象
export interface RouteObject {
  // 路由 path 是否匹配大小写
  caseSensitive?: boolean;
  // 子路由
  children?: RouteObject[];
  // 要渲染的组件
  element?: React.ReactNode;
  // 是否是索引路由
  index?: boolean;
  path?: string;
}

/**
 * 将 Route 组件转换为 route 对象,提供给 useRoutes 使用
 */
export function createRoutesFromChildren(
  children: React.ReactNode
): RouteObject[] {
  let routes: RouteObject[] = [];

  // 内部逻辑很简单,就是递归遍历 children,获取 <Route /> props 上的所有信息,然后格式化后推入 routes 数组中
  React.Children.forEach(children, element => {
    if (!React.isValidElement(element)) {
      // Ignore non-elements. This allows people to more easily inline
      // conditionals in their route config.
      return;
    }

    // 空节点,忽略掉继续往下遍历
    if (element.type === React.Fragment) {
      // Transparently support React.Fragment and its children.
      routes.push.apply(
        routes,
        createRoutesFromChildren(element.props.children)
      );
      return;
    }

    // 不要传入其它组件,只能传 Route
    invariant(
      element.type === Route,
      `[${
        typeof element.type === "string" ? element.type : element.type.name
      }] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>`
    );

    let route: RouteObject = {
      caseSensitive: element.props.caseSensitive,
      element: element.props.element,
      index: element.props.index,
      path: element.props.path
    };

    // 递归
    if (element.props.children) {
      route.children = createRoutesFromChildren(element.props.children);
    }

    routes.push(route);
  });

  return routes;
}
  • useRoutes:声明式配置路由,下面详细介绍
  • 总结:
  • react-router在路由定义时包含两种方式
    • 指令式:<routes><route /></routes>
    • 声明式:useRoutes
  • Routes与Route强绑定,有Routes则必定要传入且只能传入Route;

1.4 useRoutes

import { useRoutes } from "react-router-dom";

// 此时 App 返回的就是已经渲染好的路由元素了
function App() {
  let element = useRoutes([
    {
      path: "/",
      element: <Dashboard />,
      children: [
        {
          path: "/messages",
          element: <DashboardMessages />
        },
        { path: "/tasks", element: <DashboardTasks /> }
      ]
    },
    { path: "/team", element: <AboutPage /> }
  ]);

  return element;
}
  • RouteContext
/**
 * 动态参数的定义
 */
export type Params<Key extends string = string> = {
  readonly [key in Key]: string | undefined;
};

export interface RouteMatch<ParamKey extends string = string> {
  // params 参数,比如 :id 等
  params: Params<ParamKey>;
  // 匹配到的 pathname
  pathname: string;
  /**
   * 子路由匹配之前的路径 url,这里可以把它看做是只要以 /* 结尾路径(这是父路由的路径)中 /* 之前的部分
   */
  pathnameBase: string;
  // 定义的路由对象
  route: RouteObject;
}

interface RouteContextObject {
  // 一个 ReactElement,内部包含有所有子路由组成的聚合组件,其实 Outlet 组件内部就是它
  outlet: React.ReactElement | null;
  // 一个成功匹配到的路由数组,索引从小到大层级依次变深
  matches: RouteMatch[];
}
/**
 * 包含全部匹配到的路由,官方不推荐在外直接使用
 */
const RouteContext = React.createContext<RouteContextObject>({
  outlet: null,
  matches: []
});

/** @internal */
export {
  RouteContext as UNSAFE_RouteContext
};
  • 拆解useRoutes
/**
 * 1.该 hooks 不是只调用一次,每次重新匹配到路由时就会重新调用渲染新的 element
 * 2.当多次调用 useRoutes 时需要解决内置的 route 上下文问题,继承外层的匹配结果
 * 3.内部通过计算所有的 routes 与当前的 location 关系,经过路径权重计算,得到 matches 数组,然后将 matches 数组重新渲染为嵌套结构的组件
 */
export function useRoutes(
  routes: RouteObject[],
  locationArg?: Partial<Location> | string
): React.ReactElement | null {
  // useRoutes 必须最外层有 Router 包裹,不然报错
  invariant(
    useInRouterContext(),
    // TODO: This error is probably because they somehow have 2 versions of the
    // router loaded. We can help them understand how to avoid that.
    `useRoutes() may be used only in the context of a <Router> component.`
  );

  // 1.当此 useRoutes 为第一层级的路由定义时,matches 为空数组(默认值)
  // 2.当该 hooks 在一个已经调用了 useRoutes 的渲染环境中渲染时,matches 含有值(也就是有 Routes 的上下文环境嵌套)
  let { matches: parentMatches } = React.useContext(RouteContext);
  // 最后 match 到的 route(深度最深),该 route 将作为父 route,我们后续的 routes 都是其子级
  let routeMatch = parentMatches[parentMatches.length - 1];
  // 下面是父级 route 的参数,我们会基于以下参数操作,如果项目中只在一个地方调用了 useRoutes,一般都会是默认值
  let parentParams = routeMatch ? routeMatch.params : {};
  // 父路由的完整 pathname,比如路由设置为 /foo/*,当前导航是 /foo/1,那么 parentPathname 就是 /foo/1
  let parentPathname = routeMatch ? routeMatch.pathname : "/";
  // 同上面的 parentPathname,不过是 /* 前的部分,也就是 /foo
  let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
  let parentRoute = routeMatch && routeMatch.route;
  // 获取上下文环境中的 location
  let locationFromContext = useLocation();

  // 判断是否手动传入了 location,否则用默认上下文的 location
  let location;
  if (locationArg) {
    // 格式化为 Path 对象
    let parsedLocationArg =
      typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
    // 如果传入了 location,判断是否与父级路由匹配(作为子路由存在)
    invariant(
      parentPathnameBase === "/" ||
        parsedLocationArg.pathname?.startsWith(parentPathnameBase),
      `When overriding the location using `<Routes location>` or `useRoutes(routes, location)`, ` +
        `the location pathname must begin with the portion of the URL pathname that was ` +
        `matched by all parent routes. The current pathname base is "${parentPathnameBase}" ` +
        `but pathname "${parsedLocationArg.pathname}" was given in the `location` prop.`
    );

    location = parsedLocationArg;
  } else {
    location = locationFromContext;
  }

  let pathname = location.pathname || "/";
  // 剩余的 pathname,整体 pathname 减掉父级已经匹配的 pathname,才是本次 routes 要匹配的 pathname(适用于 parentMatches 匹配不为空的情况)
  let remainingPathname =
    parentPathnameBase === "/"
      ? pathname
      : pathname.slice(parentPathnameBase.length) || "/";
  // 匹配当前路径,注意是移除了 parentPathname 的相关路径后的匹配
  
  // 通过传入的 routes 配置项与当前的路径,匹配对应渲染的路由
  let matches = matchRoutes(routes, { pathname: remainingPathname });

  // 参数为当前匹配到的 matches 路由数组和外层 useRoutes 的 matches 路由数组
  // 返回的是 React.Element,渲染所有的 matches 对象
  return _renderMatches(
    // 没有 matches 会返回 null
    matches &&
      matches.map(match =>
        // 合并外层调用 useRoutes 得到的参数,内部的 Route 会有外层 Route(其实这也叫父 Route) 的所有匹配属性。
        Object.assign({}, match, {
          params: Object.assign({}, parentParams, match.params),
          // joinPaths 函数用于合并字符串
          pathname: joinPaths([parentPathnameBase, match.pathname]),
          pathnameBase:
            match.pathnameBase === "/"
              ? parentPathnameBase
              : joinPaths([parentPathnameBase, match.pathnameBase])
        })
      ),
    // 外层 parentMatches 部分,最后会一起加入最终 matches 参数中
    parentMatches
  );
}

/**
 * 将多个 path 合并为一个
 * @param paths path 数组
 * @returns
 */
const joinPaths = (paths: string[]): string =>
  paths.join("/").replace(///+/g, "/");

总结:

  • 获取上下文中调用useRoutes后的信息,如果证明此次调用时作为子路由使用的,需要合并父路由的匹配信息;
    • 移除父路由已经匹配完毕的pathname前缀后,调用matchRoutes与当前传入的routes配置相匹配,返回匹配到的matches数组;
    • 调用_renderMathces方法,渲染上一步得到的matches数组;

也就对应着:路由上下文解析阶段,路由匹配阶段(matchRoutes),路由渲染阶段(_renderMatches)

  • matchRoutes
/**
 * 通过 routes 与 location 得到 matches 数组
 */
export function matchRoutes(
  // 用户传入的 routes 对象
  routes: RouteObject[],
  // 当前匹配到的 location,注意这在 useRoutes 内部是先有过处理的
  locationArg: Partial<Location> | string,
  // 这个参数在 useRoutes 内部是没有用到的,但是该方法是对外暴露的,用户可以使用这个参数来添加统一的路径前缀
  basename = "/"
): RouteMatch[] | null {
  // 先格式化为 Path 对象
  let location =
    typeof locationArg === "string" ? parsePath(locationArg) : locationArg;

  // 之前提到过,抽离 basename,获取纯粹的 pathname
  let pathname = stripBasename(location.pathname || "/", basename);
  
  // basename 匹配失败,返回 null
  if (pathname == null) {
    return null;
  }

  // 1.扁平化 routes,将树状的 routes 对象根据 path 扁平为一维数组,同时包含当前路由的权重值
  let branches = flattenRoutes(routes);
  // 2.传入扁平化后的数组,根据内部匹配到的权重排序
  rankRouteBranches(branches);

  let matches = null;
  // 3.这里就是权重比较完成后的解析顺序,权重高的在前面,先进行匹配,然后是权重低的匹配
  // branches 中有一个匹配到了就终止循环,或者全都没有匹配到
  for (let i = 0; matches == null && i < branches.length; ++i)   {
    // 遍历扁平化的 routes,查看每个 branch 的路径匹配规则是否能匹配到 pathname
    matches = matchRouteBranch(branches[i], pathname);
  }

  return matches;
}

主要方面:

  • flattenRoutes:扁平化
    • rankRouteBranches:排序
    • matchRouteBranch:路由匹配
    • flattenRoutes:将树形结构转为一维数组
// 保存在 branch 中的路由信息,后续路由匹配时会用到
interface RouteMeta {
  /**
   * 路由的相对路径(刨除与父路由重复部分)
   */
  relativePath: string;
  caseSensitive: boolean;
  /**
   * 用户在 routes 数组中定义的索引位置(相对其兄弟 route 而言)
   */
  childrenIndex: number;
  route: RouteObject;
}

// 扁平化的路由对象,包含当前路由对象对应的完整 path,权重得分与用于匹配的路由信息
interface RouteBranch {
  /**
   * 完整的 path(合并了父路由的,下面会引入相对路由的概念)
   */
  path: string;
  /**
   * 权重,用于排序
   */
  score: number;
  /**
   * 路径 meta,依次为从父级到子级的路径规则,最后一个是路由自己
   */
  routesMeta: RouteMeta[];
}

/**
 * 扁平化路由,会将所有路由扁平为一个数组,用于比较权重
 * @param routes 第一次在外部调用只需要传入该值,用于转换的 routes 数组
 * @param branches
 * @param parentsMeta
 * @param parentPath
 * @returns
 */
function flattenRoutes(
  routes: RouteObject[],
  // 除了 routes,下面三个都是递归的时候使用的
  branches: RouteBranch[] = [],
  parentsMeta: RouteMeta[] = [],
  parentPath = ""
): RouteBranch[] {
  routes.forEach((route, index) => {
    // 当前 branch 管理的 route meta
    let meta: RouteMeta = {
      // 只保存相对路径,这里的值下面会进行处理
      relativePath: route.path || "",
      caseSensitive: route.caseSensitive === true,
      // index 是用户给出的 routes 顺序,会一定程度影响 branch 的排序(当为同一层级 route 时)
      childrenIndex: index,
      // 当前 route 对象
      route
    };

    // 如果 route 以 / 开头,那么它应该完全包含父 route 的 path,否则报错
    if (meta.relativePath.startsWith("/")) {
      invariant(
        meta.relativePath.startsWith(parentPath),
        `Absolute route path "${meta.relativePath}" nested under path ` +
          `"${parentPath}" is not valid. An absolute child route path ` +
          `must start with the combined path of all its parent routes.`
      );

      // 把父路由前缀去除,只要相对路径
      meta.relativePath = meta.relativePath.slice(parentPath.length);
    }

    // 完整的 path,合并了父路由的 path
    let path = joinPaths([parentPath, meta.relativePath]);
    // 第一次使用 parentsMeta 为空数组,从外到内依次推入 meta 到该数组中
    let routesMeta = parentsMeta.concat(meta);

    // 开始递归
    if (route.children && route.children.length > 0) {
      // 如果是 index route,报错,因为 index route 不能有 children
      invariant(
        route.index !== true,
        `Index routes must not have child routes. Please remove ` +
          `all child routes from route path "${path}".`
      );

      flattenRoutes(route.children, branches, routesMeta, path);
    }

    // 没有路径的路由(之前提到过的布局路由)不参与路由匹配,除非它是索引路由
    /* 
      注意:递归是在前面进行的,也就是说布局路由的子路由是会参与匹配的
      而子路由会有布局路由的路由信息,这也是布局路由能正常渲染的原因。
    */
    if (route.path == null && !route.index) {
      return;
    }

    // routesMeta,包含父 route 到自己的全部 meta 信息
    // computeScore 是计算权值的方法,我们后面再说
    branches.push({ path, score: computeScore(path, route.index), routesMeta });
  });

  return branches;
}
  • rankRouteBranches
// 动态路由权重,比如 /foo/:id
const dynamicSegmentValue = 3;
// 索引路由权重,也就是加了 index 为 true 属性的路由
const indexRouteValue = 2;
// 空路由权重,当一段路径值为空时匹配,只有最后的路径以 / 结尾才会用到它
const emptySegmentValue = 1;
// 静态路由权重
const staticSegmentValue = 10;
// 路由通配符权重,为负的,代表当我们写 * 时实际会降低权重
const splatPenalty = -2;

// 判断是否有动态参数,比如 :id 等
const paramRe = /^:\w+$/;
// 判断是否为 *
const isSplat = (s: string) => s === "*";

/**
 * 计算路由权值,根据权值大小匹配路由
 * 静态值 > params 动态参数
 * @param path 完整的路由路径,不是相对路径
 * @param index
 * @returns
 */
function computeScore(path: string, index: boolean | undefined): number {
  let segments = path.split("/");
  // 初始化权重值,有几段路径就是几,路径多的初始权值高
  let initialScore = segments.length;
  // 有一个 * 权重减 2
  if (segments.some(isSplat)) {
    initialScore += splatPenalty;
  }

  // 用户传了 index,index 是布尔值,代表 IndexRouter,权重 +2
  if (index) {
    initialScore += indexRouteValue;
  }

  // 在过滤出非 * 的部分
  return segments
    .filter(s => !isSplat(s))
    .reduce(
      (score, segment) =>
        score +
        // 如果有动态参数
        (paramRe.test(segment)
          ? // 动态参数权重 3
            dynamicSegmentValue
          : segment === ""
          ? // 空值权重为 1,这个其实只有一种情况,path 最后面多一个 /,比如 /foo 与 /foo/ 的区别
            emptySegmentValue
          : // 静态值权重最高为 10
            staticSegmentValue),
      initialScore
    );
}

/**
 * 排序,比较权重值
 * @param branches
 */
function rankRouteBranches(branches: RouteBranch[]): void {
  branches.sort((a, b) =>
    a.score !== b.score
      // 排序,权值大的在前面
      ? b.score - a.score
      : // 如果 a.score === b.score
        compareIndexes(
          // routesMeta 是一个从最外层路由到子路由的数组
          // childrenIndex 是按照 routes 中 route 传入的顺序传值的,写在后面的 index 更大(注意是同级)
          a.routesMeta.map(meta => meta.childrenIndex),
          b.routesMeta.map(meta => meta.childrenIndex)
        )
  );
}


/**
 * 比较子 route 的 index,判断是否为兄弟 route,如果不是则返回 0,比较没有意义,不做任何操作
 * @param a
 * @param b
 * @returns
 */
function compareIndexes(a: number[], b: number[]): number {
  // 是否为兄弟 route
  let siblings =
    // 这里是比较除了最后一个 route 的 path,需要全部一致才是兄弟 route
    a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);

  return siblings
    ? 
      // 如果是兄弟节点,按照传入的顺序排序 a.length - 1 和 b.length - 1 是相等的,只是内部的值不同
      a[a.length - 1] - b[b.length - 1]
    : 
      // 只比较兄弟节点,如果不是兄弟节点,则权重相同
      0;
}
  • matchRouteBranch
/**
 * 通过 branch 和当前的 pathname 得到真正的 matches 数组
 * @param branch
 * @param routesArg
 * @param pathname
 * @returns
 */
function matchRouteBranch<ParamKey extends string = string>(
  branch: RouteBranch,
  pathname: string
): RouteMatch<ParamKey>[] | null {
  let { routesMeta } = branch;

  // 初始化匹配到的值
  let matchedParams = {};
  let matchedPathname = "/";
  // 最终的 matches 数组
  let matches: RouteMatch[] = [];
  // 遍历 routesMeta 数组,最后一项是自己的 route,前面是 parentRoute
  for (let i = 0; i < routesMeta.length; ++i) {
    let meta = routesMeta[i];
    // 是否为最后一个 route
    let end = i === routesMeta.length - 1;
    // pathname 匹配过父 route 后的剩余的路径名
    let remainingPathname =
      matchedPathname === "/"
        ? pathname
        : pathname.slice(matchedPathname.length) || "/";
    // 使用的相对路径规则匹配剩余的值
    let match = matchPath(
      // 在匹配时只有最后一个 route 的 end 才会是 true,其余都是 false,这里的 end 意味路径最末尾的 /
      { path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
      remainingPathname
    );

    // 没匹配上,直接返回 null,整个 route 都匹配失败
    if (!match) return null;

    // 匹配上了合并 params,注意这里是改变的 matchedParams,所以所有 route 的 params 都是同一个
    Object.assign(matchedParams, match.params);

    let route = meta.route;

    // 匹配上了就把路径再补全
    matches.push({
      params: matchedParams,
      pathname: joinPaths([matchedPathname, match.pathname]),
      pathnameBase: joinPaths([matchedPathname, match.pathnameBase]),
      route
    });

    // 更改 matchedPathname,已经匹配上的 pathname 前缀,用作后续子 route 的循环
    if (match.pathnameBase !== "/") {
      matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
    }
  }

  return matches;
}
  • _renderMatches

/**
 * 其实就是渲染 RouteContext.Provider 组件(包括多个嵌套的 Provider)
 */
function _renderMatches(
  matches: RouteMatch[] | null,
  // 如果在已有 match 的 route 内部调用,会合并父 context 的 match
  parentMatches: RouteMatch[] = []
): React.ReactElement | null {
  if (matches == null) return null;

  // 生成 outlet 组件,注意这里是从后往前 reduce,所以索引在前的 match 是最外层,也就是父路由对应的 match 是最外层
  /**
   *  可以看到 outlet 是通过不断递归生成的组件,最外层的 outlet 递归层数最多,包含有所有的内层组件,
   *  所以我们在外层使用的 <Outlet /> 是包含有所有子组件的聚合组件
   * */
  return matches.reduceRight((outlet, match, index) => {
    return (
      <RouteContext.Provider
        // 如果有 element 就渲染 element如果没有填写 element则默认是 <Outlet />,继续渲染内嵌的 <Route />
        children={
          match.route.element !== undefined ? match.route.element : <Outlet />
        }
        // 代表当前 RouteContext 匹配到的值,matches 并不是全局状态一致的,会根据层级不同展示不同的值,最后一个层级是完全的 matches,这也是之前提到过的不要在外部使用 RouteContext 的原因
        value={{
          outlet,
          matches: parentMatches.concat(matches.slice(0, index + 1))
        }}
      />
    );
    // 最内层的 outlet 为 null,也就是最后的子路由
  }, null as React.ReactElement | null);
}
  • 总结
    • useRoutes是react-router中的核心,用户不管是直接使用useRoutes还是用Routes与Route组件结合最终都会转换为它。
    • useRoutes在上下文解析阶段会解析在外层是否已经调用过useRoutes,如果调用过会先获取外层的上下文数据,最后将外层数据与用户传入的routes数组结合,生成最终结果;
    • useRoutes在匹配阶段会将传入的routes与当前的location(可手动传入,但内部会做校验)做一层匹配,通过对route中声明的path的权重计算,拿到当前pathname所能匹配到的最佳matches数组,索引从小到大层数关系从到外到内;
    • useRoutes在渲染阶段会将matches数组渲染为一个聚合的React Element,该元素整体是许多RouteContext.Provider的嵌套,从外到内依次是【父 => 子 => 孙子】这样的关系,每个Provider包含两个值,与该级别对应的matches数组(最后的元素是该级别的route自身)与outlet元素,outlet元素就是嵌套RouteContext.Provider 存放的地方,每个RouteContext.Provider的children就是route的element属性;
    • 每次使用outlet实际上都是渲染的内置的路由关系(如果当前route没有element属性,则默认渲染outlet,这也是为什么可以直接写不带element组件嵌套的原因),我们可以在当前级别route的element中任意地方使用outlet来渲染子路由;

1.5 Navigate

// useNavigate 返回的 navigate 函数定义,可以传入 to 或者传入数字控制浏览器页面栈的显示
export interface NavigateFunction {
  (to: To, options?: NavigateOptions): void;
  (delta: number): void;
}

export interface NavigateOptions {
  // 是否替换当前栈
  replace?: boolean;
  // 当前导航的 state
  state?: any;
}

/**
 * 返回的 navigate 函数可以传和文件夹相同的路径规则
 */
export function useNavigate(): NavigateFunction {
  invariant(
    useInRouterContext(),
    // TODO: This error is probably because they somehow have 2 versions of the
    // router loaded. We can help them understand how to avoid that.
    `useNavigate() may be used only in the context of a <Router> component.`
  );
  
  // Router 提供的 navigator,本质是 history 对象
  let { basename, navigator } = React.useContext(NavigationContext);
  // 当前路由层级的 matches 对象(我们在前面说了,不同的 RouteContext.Provider 层级不同该值不同)
  let { matches } = React.useContext(RouteContext);
  let { pathname: locationPathname } = useLocation();

  // 依次匹配到的子路由之前的路径(/* 之前)
  let routePathnamesJson = JSON.stringify(
    matches.map(match => match.pathnameBase)
  );

  // 是否已经初始化完毕(useEffect),这里是要让页面不要在一渲染的时候就跳转,应该在 useEffect 后才能跳转,也就是说如果一渲染就要跳转页面应该写在 useEffect 中
  let activeRef = React.useRef(false);
  React.useEffect(() => {
    activeRef.current = true;
  });

  // 返回的跳转函数
  let navigate: NavigateFunction = React.useCallback(
    (to: To | number, options: NavigateOptions = {}) => {
      if (!activeRef.current) return;

      // 如果是数字
      if (typeof to === "number") {
        navigator.go(to);
        return;
      }

      // 实际路径的获取,这个方法比较复杂,我们下面单独说
      let path = resolveTo(
        to,
        JSON.parse(routePathnamesJson),
        locationPathname
      );

      // 有 basename,加上 basename
      if (basename !== "/") {
        path.pathname = joinPaths([basename, path.pathname]);
      }

      (!!options.replace ? navigator.replace : navigator.push)(
        path,
        options.state
      );
    },
    [basename, navigator, routePathnamesJson, locationPathname]
  );

  return navigate;
}

import type { To } from 'history';

export interface NavigateProps {
  // To 从 history 中引入
  /*
    export declare type To = string | PartialPath;
  */
  to: To;
  replace?: boolean;
  state?: any;
}

/**
 * 组件式导航,当页面渲染后立刻调用 navigate 方法,很简单的封装
 */
export function Navigate({ to, replace, state }: NavigateProps): null {
  // 必须在 Router 上下文中
  invariant(
    useInRouterContext(),
    // TODO: This error is probably because they somehow have 2 versions of
    // the router loaded. We can help them understand how to avoid that.
    `<Navigate> may be used only in the context of a <Router> component.`
  );

  let navigate = useNavigate();
  React.useEffect(() => {
    navigate(to, { replace, state });
  });

  return null;
}
  • 总结:navigate内部还是调用的useNavigate,而useNavigate内部则是对用户传入的路径做处理,获取到最终的路径值,再传递给NavigationContext提供navigator对象;

2. react-router-dom

这里主要介绍在react-router-dom中引用的BrowserRouter、hashRouter以及historyRouter

BrowserRouter 和 HashRouter的区别,是区分链接还是hash,从history库中取到

import { createBrowserHistory, createHashHistory } from "history";

2.1 BrowserRouter

export interface BrowserRouterProps {
  basename?: string;
  children?: React.ReactNode;
  window?: Window;
}

/**
 * A `<Router>` for use in web browsers. Provides the cleanest URLs.
 */
export function BrowserRouter({
  basename,
  children,
  window,
}: BrowserRouterProps) {
  let historyRef = React.useRef<BrowserHistory>();
  if (historyRef.current == null) {
    historyRef.current = createBrowserHistory({ window });
  }

  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location,
  });

  React.useLayoutEffect(() => history.listen(setState), [history]);

  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  );
}

2.2 hashRouter

export interface HashRouterProps {
  basename?: string;
  children?: React.ReactNode;
  window?: Window;
}

/**
 * A `<Router>` for use in web browsers. Stores the location in the hash
 * portion of the URL so it is not sent to the server.
 */
export function HashRouter({ basename, children, window }: HashRouterProps) {
  let historyRef = React.useRef<HashHistory>();
  if (historyRef.current == null) {
    historyRef.current = createHashHistory({ window });
  }

  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location,
  });

  React.useLayoutEffect(() => history.listen(setState), [history]);

  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  );
}

2.3 HistoryRouter

export interface HistoryRouterProps {
  basename?: string;
  children?: React.ReactNode;
  history: History;
}

/**
 * A `<Router>` that accepts a pre-instantiated history object. It's important
 * to note that using your own history object is highly discouraged and may add
 * two versions of the history library to your bundles unless you use the same
 * version of the history library that React Router uses internally.
 */
function HistoryRouter({ basename, children, history }: HistoryRouterProps) {
  const [state, setState] = React.useState({
    action: history.action,
    location: history.location,
  });

  React.useLayoutEffect(() => history.listen(setState), [history]);

  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  );
}

if (__DEV__) {
  HistoryRouter.displayName = "unstable_HistoryRouter";
}

export { HistoryRouter as unstable_HistoryRouter };