react-router 阅读笔记

334 阅读5分钟

基本构成和关系

核心源码三部分

  • react-router
  • react-router-dom
  • react-router-native

核心和关键在 react-router

react-router 可以理解为是 react-router-dom 的核心, 封装了 Router、Routes 等的组件,实现了核心的路由匹配和渲染切换

react-router 里面, 路由的核心是 history 库,histroy 实现了路由的原理, 就像是在 single-spa 中介绍的路由原理那样,处理了路由的两种路由模式下的

监听问题, 处理了两种路由模式下的 api 的处理问题

基本使用

基本使用代码示例

import * as React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
 
const root = ReactDOM.createRoot(
  document.getElementById("root")
);
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
import * as React from "react";
import { Routes, Route, Link } from "react-router-dom";
import "./App.css";
 
function App() {
  return (
    <div className="App">
      <h1>Welcome to React Router!</h1>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="about" element={<About />} />
      </Routes>
    </div>
  );
}

接下来分析这种使用方式的的原理

3. 原理分析

3.1. BrowserRouter

/**
 * A `<Router>` for use in web browsers. Provides the cleanest URLs.
 *
 * @see https://reactrouter.com/docs/en/v6/routers/browser-router
 */
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}
    />
  );
}

history 中有针对路由的监听, BrowserRouter 组件, 创建了 BrowserRouter 实例, 并且在 history 变化的时候, 重新监听路由。

react-router 中路由的监听便是在这里完成的, 同时向下传递路由实例

3.2. Router

// context provide 提供路由信息和上下文环境
 
export function Router({
  basename: basenameProp = "/",
  children = null,
  location: locationProp,
  navigationType = NavigationType.Pop,
  navigator,
  static: staticProp = false,
}: RouterProps): React.ReactElement | null {
  invariant(
    !useInRouterContext(),
    `You cannot render a <Router> inside another <Router>.` +
      ` You should never have more than one in your app.`
  );
 
  // 格式化 basename
  let basename = normalizePathname(basenameProp);
   
  // navigationContext 上下文
  let navigationContext = React.useMemo(
    () => ({ basename, navigator, static: staticProp }),
    [basename, navigator, staticProp]
  );
 
  if (typeof locationProp === "string") {
    // 解析路径, 转化为对象
    locationProp = parsePath(locationProp);
  }
 
  let {
    pathname = "/",
    search = "",
    hash = "",
    state = null,
    key = "default",
  } = locationProp;
 
  let location = React.useMemo(() => {
    let trailingPathname = stripBasename(pathname, basename);
 
    if (trailingPathname == null) {
      return null;
    }
 
    return {
      pathname: trailingPathname,
      search,
      hash,
      state,
      key,
    };
  }, [basename, pathname, search, hash, state, key]);
 
  warning(
    location != null,
    `<Router basename="${basename}"> is not able to match the URL ` +
      `"${pathname}${search}${hash}" because it does not start with the ` +
      `basename, so the <Router> won't render anything.`
  );
 
  if (location == null) {
    // 无法匹配,不渲染任何东西
    return null;
  }
 
   
  // 主要用来注入 NavigationContext LocationContext 的上下文
  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider
        children={children}
        value={{ location, navigationType }}
      />
    </NavigationContext.Provider>
  );
}

Router 很好理解, 其实就是一个上下文提供者, 没有实质性的组件渲染,提供了 NavigationContext: 存储 history路由实例等数据

提供 LocationContext:主要用来提供 location 信息, 和目前的导航方式

3.3. Routes

/**
 * A container for a nested tree of <Route> elements that renders the branch
 * that best matches the current location.
 *
 * @see https://reactrouter.com/docs/en/v6/components/routes
 */
export function Routes({
  children,
  location,
}: RoutesProps): React.ReactElement | null {
  return useRoutes(createRoutesFromChildren(children), location);
}

代码比较简单: 主要调用 createRoutesFromChildren 来生成 routes 配置的数据结构,然后传递到 useRoutes 来渲染

createRoutesFromChildren

/**
 * Creates a route config from a React "children" object, which is usually
 * either a `<Route>` element or an array of them. Used internally by
 * `<Routes>` to create a route config from its children.
 *
 * @see https://reactrouter.com/docs/en/v6/utils/create-routes-from-children
 */
export function createRoutesFromChildren(
  children: React.ReactNode
): RouteObject[] {
  let routes: RouteObject[] = [];
 
  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;
    }
 
    // React.Fragment 子元素
    if (element.type === React.Fragment) {
      // Transparently support React.Fragment and its children.
      routes.push.apply(
        routes,
        createRoutesFromChildren(element.props.children)
      );
      return;
    }
 
    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;
}

从如下结构里面, 递归拿到路由的配置,不再赘述

<Routes>
  <Route path="/" element={<Home />} />
    <Route path="users" element={<Users />}>
      <Route path="me" element={<OwnUserProfile />} />
        <Route path=":id" element={<UserProfile />} />
          </Route>
</Routes>

useRoutes

/**
 * Returns the element of the route that matched the current location, prepared
 * with the correct context to render the remainder of the route tree. Route
 * elements in the tree must render an <Outlet> to render their child route's
 * element.
 *
 * @see https://reactrouter.com/docs/en/v6/hooks/use-routes
 */
export function useRoutes(
  routes: RouteObject[],
  locationArg?: Partial<Location> | string
): React.ReactElement | null {
   
  // 非locationContext.provider 报错
  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.`
  );
 
 
  // {
  //   outlet: null,
  //   matches: [],
  // }
  //
  let { matches: parentMatches } = React.useContext(RouteContext);
  let routeMatch = parentMatches[parentMatches.length - 1];
  let parentParams = routeMatch ? routeMatch.params : {};
  let parentPathname = routeMatch ? routeMatch.pathname : "/";
  let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
  let parentRoute = routeMatch && routeMatch.route;
 
  if (__DEV__) {
    // You won't get a warning about 2 different <Routes> under a <Route>
    // without a trailing *, but this is a best-effort warning anyway since we
    // cannot even give the warning unless they land at the parent route.
    //
    // Example:
    //
    // <Routes>
    //   {/* This route path MUST end with /* because otherwise
    //       it will never match /blog/post/123 */}
    //   <Route path="blog" element={<Blog />} />
    //   <Route path="blog/feed" element={<BlogFeed />} />
    // </Routes>
    //
    // function Blog() {
    //   return (
    //     <Routes>
    //       <Route path="post/:id" element={<Post />} />
    //     </Routes>
    //   );
    // }
    let parentPath = (parentRoute && parentRoute.path) || "";
    warningOnce(
      parentPathname,
      !parentRoute || parentPath.endsWith("*"),
      `You rendered descendant <Routes> (or called \`useRoutes()\`) at ` +
        `"${parentPathname}" (under <Route path="${parentPath}">) but the ` +
        `parent route path has no trailing "*". This means if you navigate ` +
        `deeper, the parent won't match anymore and therefore the child ` +
        `routes will never render.\n\n` +
        `Please change the parent <Route path="${parentPath}"> to <Route ` +
        `path="${parentPath === "/" ? "*" : `${parentPath}/*`}">.`
    );
  }
 
  // 默认值 null
  let locationFromContext = useLocation();
 
  let location;
  if (locationArg) {
    let parsedLocationArg =
      typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
 
    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;
  }
 
    // 计算 pathname
  let pathname = location.pathname || "/";
  let remainingPathname =
    parentPathnameBase === "/"
      ? pathname
      : pathname.slice(parentPathnameBase.length) || "/";
 
  // 核心匹配算法
  let matches = matchRoutes(routes, { pathname: remainingPathname });
 
  if (__DEV__) {
    warning(
      parentRoute || matches != null,
      `No routes matched location "${location.pathname}${location.search}${location.hash}" `
    );
 
    warning(
      matches == null ||
        matches[matches.length - 1].route.element !== undefined,
      `Matched leaf route at location "${location.pathname}${location.search}${location.hash}" does not have an element. ` +
        `This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.`
    );
  }
 
  // 渲染组件, 由子到父顺序
  return _renderMatches(
    matches &&
      matches.map((match) =>
        Object.assign({}, match, {
          params: Object.assign({}, parentParams, match.params),
          pathname: joinPaths([parentPathnameBase, match.pathname]),
          pathnameBase:
            match.pathnameBase === "/"
              ? parentPathnameBase
              : joinPaths([parentPathnameBase, match.pathnameBase]),
        })
      ),
    parentMatches
  );
}

这部分属于是核心部分源码, 一共做三个事情

  • 生成 pathname , 比如:路由 /aa/bb, 那么pathname 就是 /aa/bb
  • 通过 matchRoutes 核心路由的匹配算法, 找到匹配的路由分支 。 比如:/manage/edit 明显是二级路由,那么路由分支应该是: / → manage→ edit
  • 渲染匹配到的路由的对应组件, 同时 location 变化时,也会重新进行渲染

matchRoutes

/**
 * Matches the given routes to a location and returns the match data.
 *
 * @see https://reactrouter.com/docs/en/v6/utils/match-routes
 */
export function matchRoutes(
  routes: RouteObject[],
  locationArg: Partial<Location> | string,
  basename = "/"
): RouteMatch[] | null {
  // 解析翻译为对象
  let location =
    typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
 
 
  let pathname = stripBasename(location.pathname || "/", basename);
 
  if (pathname == null) {
    return null;
  }
 
  // 拿到拍平以后的路由配置, 并且给了 store
  let branches = flattenRoutes(routes);
 
  // 按照分数进行排序
  rankRouteBranches(branches);
 
  let matches = null;
  for (let i = 0; matches == null && i < branches.length; ++i) {
    // 从子路由开始匹配
    matches = matchRouteBranch(branches[i], pathname);
  }
 
  return matches;
}
  • 拍平 routes 的结构, 子路由在前,父路由在后
  • 根据不同的分支, 不同的权重, 将分支按照 store 进行排序, 确定了匹配的优先级
  • 根据排序后的数组, 匹配对应的组件

_renderMatches

export function _renderMatches(
  matches: RouteMatch[] | null,
  parentMatches: RouteMatch[] = []
): React.ReactElement | null {
  if (matches == null) return null;
 
  return matches.reduceRight((outlet, match, index) => {
    return (
      <RouteContext.Provider
        children={
          match.route.element !== undefined ? match.route.element : outlet
        }
        value={{
          outlet,
          matches: parentMatches.concat(matches.slice(0, index + 1)),
        }}
      />
    );
  }, null as React.ReactElement | null);
}

这段代码很精妙,信息量也非常大,通过 reduceRight 来形成 react 结构 elmenet,这一段解决了三个问题:

  • 第一层 route 页面是怎么渲染
  • outlet 是如何作为子路由渲染的。
  • 路由状态是怎么传递的。

reduceRight 是从右向左开始遍历,那么之前讲到过 match 结构是 root -> children -> child1, reduceRight 把前一项返回的内容作为后一项的 outlet,那么如上的 match 结构会这样被处理。

  • 1 首先通过 provider 包裹 child1,那么 child1 真正需要渲染的内容 Child1 组件 ,将被当作 provider 的 children,最后把当前 provider 返回,child1 没有子路由,所以第一层 outlet 为 null。
  • 2 接下来第一层返回的 provider,讲作为第二层的 outlet ,通过第二层的 provider 的 value 里面 outlet 属性传递下去。然后把 Layout 组件作为 children 返回。
  • 3 接下来渲染的是第一层的 Provider ,所以 Layout 会被渲染,那么 Child1 并没有直接渲染,而是作为 provider 的属性传递下去。

那么从上面我们都知道 child1 是在 container 中用 Outlet 占位组件的形式渲染的。那么我们先想一下 Outlet 会做哪些事情,应该会用 useContext 把第一层 provider 的 outlet 获取到然后渲染就可以渲染 child1 的 provider 了,而 child1 为 children 也就会被渲染了

4. 总结

路由本质在于 Routes 组件,当 location 上下文改变的时候,Routes 重新渲染,重新形成渲染分支,然后通过 provider 方式逐层传递 Outlet,进行匹配渲染