【源码共读】| 简易实现React-Router

629 阅读5分钟

React Router是React中最常用的路由库,它提供了一种灵活且强大的方式来处理单页面应用的路由。
本文主要解决几个问题:

  1. React Router的源码解析
    • React Router的源码结构
    • React Router的核心代码解析
    • React Router的工作原理
  2. React Router的实现
    • 创建一个简单的路由组件
    • 实现路由的匹配和切换
    • 实现路由的嵌套

再进一步~~(这里不实现)~~

  • 动态路由匹配
  • 实现路由的重定向
  • 路由守卫
  • 懒加载

基本使用

下面是官方文档的使用案例 github.com/remix-run/r…

import { Routes, Route, Outlet, Link } from "react-router-dom";

export default function App() {
  return (
    <div>
      <h1>Basic Example</h1>

      {/* Routes nest inside one another. Nested route paths build upon
            parent route paths, and nested route elements render inside
            parent route elements. See the note about <Outlet> below. */}
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="dashboard" element={<Dashboard />} />

          {/* Using path="*"" means "match anything", so this route
                acts like a catch-all for URLs that we don't have explicit
                routes for. */}
          <Route path="*" element={<NoMatch />} />
        </Route>
      </Routes>
    </div>
  );
}

function Layout() {
  return (
    <div>
      {/* A "layout route" is a good place to put markup you want to
          share across all the pages on your site, like navigation. */}
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/dashboard">Dashboard</Link>
          </li>
          <li>
            <Link to="/nothing-here">Nothing Here</Link>
          </li>
        </ul>
      </nav>

      <hr />

      {/* An <Outlet> renders whatever child route is currently active,
          so you can think about this <Outlet> as a placeholder for
          the child routes we defined above. */}
      <Outlet />
    </div>
  );
}

function Home() {
  return (
    <div>
      <h2>Home</h2>
    </div>
  );
}

function About() {
  return (
    <div>
      <h2>About</h2>
    </div>
  );
}

function Dashboard() {
  return (
    <div>
      <h2>Dashboard</h2>
    </div>
  );
}

function NoMatch() {
  return (
    <div>
      <h2>Nothing to see here!</h2>
      <p>
        <Link to="/">Go to the home page</Link>
      </p>
    </div>
  );
}

源码分析

其中包含了以下几个组件:

  • Routes
    • Route
  • Outlet
  • Link

Routes

  • 匹配对应的地址
  • 渲染匹配的元素
  • 将子元素的参数拿出来,组成Route配置
/**
 * A container for a nested tree of <Route> elements that renders the branch
 * that best matches the current location.
 *
 * @see https://reactrouter.com/components/routes
 */
// 用来嵌套和匹配当前的组件
export function Routes({
  children,
  location,
}: RoutesProps): React.ReactElement | null {
  // 通过这个函数来实现对应渲染的组件
  return useRoutes(createRoutesFromChildren(children), location);
}

// createRoutesFromChildren
export function createRoutesFromChildren(
  children: React.ReactNode,
  parentPath: number[] = []
): RouteObject[] {
  let routes: RouteObject[] = [];
  // 省略....
  let route: RouteObject = {
    id: element.props.id || treePath.join("-"),
    caseSensitive: element.props.caseSensitive,
    element: element.props.element,
    Component: element.props.Component,
    index: element.props.index,
    path: element.props.path,
    loader: element.props.loader,
    action: element.props.action,
    errorElement: element.props.errorElement,
    ErrorBoundary: element.props.ErrorBoundary,
    hasErrorBoundary:
      element.props.ErrorBoundary != null ||
      element.props.errorElement != null,
    shouldRevalidate: element.props.shouldRevalidate,
    handle: element.props.handle,
    lazy: element.props.lazy,
  };

  if (element.props.children) {
    route.children = createRoutesFromChildren(
      element.props.children,
      treePath
    );
  }

  routes.push(route);
});

return routes;
}

// useRoutes
export function useRoutes(
  routes: RouteObject[],
  locationArg?: Partial<Location> | string
): React.ReactElement | null {
  return useRoutesImpl(routes, locationArg);
}

export function useRoutesImpl(
  routes: RouteObject[],
  locationArg?: Partial<Location> | string,
  dataRouterState?: RemixRouter["state"]
): React.ReactElement | null {
  // 省略代码...
  let locationFromContext = useLocation();

  let location;
  // 省略代码...
  let pathname = location.pathname || "/";
  let remainingPathname =
    parentPathnameBase === "/"
    ? pathname
    : pathname.slice(parentPathnameBase.length) || "/";

  let matches = matchRoutes(routes, { pathname: remainingPathname });


  let renderedMatches = _renderMatches(
    matches &&
    matches.map((match) =>
      Object.assign({}, match, {
        params: Object.assign({}, parentParams, match.params),
        pathname: joinPaths([
          parentPathnameBase,
          // Re-encode pathnames that were decoded inside matchRoutes
          navigator.encodeLocation
          ? navigator.encodeLocation(match.pathname).pathname
          : match.pathname,
        ]),
        pathnameBase:
          match.pathnameBase === "/"
          ? parentPathnameBase
          : joinPaths([
            parentPathnameBase,
            // Re-encode pathnames that were decoded inside matchRoutes
            navigator.encodeLocation
            ? navigator.encodeLocation(match.pathnameBase).pathname
            : match.pathnameBase,
          ]),
      })
               ),
    parentMatches,
    dataRouterState
  );

Outlet

  • 返回子路由的元素
/**
 * Renders the child route's element, if there is one.
 *
 * @see https://reactrouter.com/components/outlet
 */
export function Outlet(props: OutletProps): React.ReactElement | null {
  return useOutlet(props.context);
}

/**
 * Returns the element for the child route at this level of the route
 * hierarchy. Used internally by <Outlet> to render child routes.
 *
 * @see https://reactrouter.com/hooks/use-outlet
 */
export function useOutlet(context?: unknown): React.ReactElement | null {
  let outlet = React.useContext(RouteContext).outlet;
  if (outlet) {
    return (
      <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
    );
  }
  return outlet;
}

Link

  • 返回一个带a标签的元素
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
  function LinkWithRef()
  // 省略代码....
  return (
  <a
  {...rest}
  href={absoluteHref || href}
onClick={isExternal || reloadDocument ? onClick : handleClick}
ref={ref}
target={target}
  />
  )
);

通过上述的分析,我们可以知道React Router的原理:
image.png
一句话总结:React Router通过监听location的变化来触发组件更新,通过Routes的组件匹配对应的组件进行渲染。

简易实现

根据上面的源码分析,我们来实现一下核心功能。

Router

这里以BrowserRouter为例

  • 监听url变化
  • 传递下去
import React, { useLayoutEffect } from "react";
import Router from "./Router";
import { createBrowserHistory } from "history";

export default function BrowserRouter({ children }) {
  // 创建一个引用,用于存储浏览器历史记录对象
  let historyRef = React.useRef();

  // 如果引用中的值为null,则创建一个浏览器历史记录对象,并将其赋值给引用
  if (historyRef.current == null) {
    historyRef.current = createBrowserHistory();
  }

  // 从引用中获取浏览器历史记录对象
  const history = historyRef.current;

  // 使用状态钩子函数创建一个名为state的状态,初始值为一个包含当前路由位置的对象
  const [state, setState] = React.useState({
    location: history.location,
  });

  // 使用useLayoutEffect钩子函数,在组件挂载或更新时监听路由变化,并更新状态
  useLayoutEffect(() => {
    // 监听路由变化,并在变化时调用setState函数更新状态
    history.listen(setState);
  }, [history]);

  // 返回一个Router组件,将children、history和state.location作为props传递给Router组件
  return (
    <Router children={children} navigator={history} location={state.location} />
  );
}

// Router
import React, { useMemo } from "react";
import { NavigationContext } from "./Context";

export default function Router({ navigator, children, location }) {
  // 使用useMemo来缓存navigationContext对象
  let navigationContext = useMemo(
    () => ({ navigator, location }),
    [navigator, location]
  );

  // 使用NavigationContext.Provider组件将navigationContext对象作为值传递给子组件
  return (
    <NavigationContext.Provider value={navigationContext}>
      {children}
    </NavigationContext.Provider>
  );
}

Routes

  • 匹配路由
  • 构建子元素的Route配置
// Routes
import { createRoutesFromChildren } from "./createRoutesFromChildren";
import { useRoutes } from "./hooks";

export default function Routes({ children }) {
  let routes = createRoutesFromChildren(children);

  return useRoutes(routes);
}

// createRoutesFromChildren
import React from "react";

export function createRoutesFromChildren(children) {
  const routes = [];

  React.Children.forEach(children, (child) => {
    const { path, element } = child.props;

    let route = {
      path,
      element,
    };

    if (child.props.children) {
      route.children = createRoutesFromChildren(child.props.children);
    }
    routes.push(route);
  });

  return routes;
}

// useRoutes
export function useRoutes(routes) {
  // 获取当前页面的URL路径
  const location = useLocation();
  // 获取当前页面的路径名
  const pathName = location.pathname;

  // 遍历路由数组
  return routes.map((route) => {
    // 判断当前页面的路径名是否以路由的路径开头
    const match = pathName.startsWith(route.path);

    // 如果匹配成功
    return (
      match &&
      // 遍历路由的子路由数组
      route.children.map((child) => {
        // 判断子路由的路径名是否与当前页面的路径名相等
        // 规范化路由
        // normalizePathname(""); // 输出: "/"
        // normalizePathname("/home/"); // 输出: "/home"
        // normalizePathname("////home////"); // 输出: "/home"
        let m = normalizePathname(child.path) === pathName;

        // 如果匹配成功
        return (
          m && (
            // 传递子元素给outlet
            <RouteContext.Provider
              value={{ outlet: child.element }}
              children={
                // 如果路由的元素属性存在则渲染该元素否则渲染<Outlet />组件
                route.element !== undefined ? route.element : <Outlet />
              }
              ></RouteContext.Provider>
          )
        );
      })
    );
  });
}
export function useLocation() {
  const { location } = useContext(NavigationContext);

  return location;
}

Link

在上述代码中,传递了history对象过来,我们可以直接调用api来实现跳转

// Link
import { useNavigate } from "./hooks";

export default function Link({ to, children }) {
  const navigate = useNavigate()
  const handleClick = (e) => {
    // 页面不刷新,拦截默认行为
    e.preventDefault();
    navigate(to)
  };
  return (
    <a href={to} onClick={handleClick}>
      {children}
    </a>
  );
}


export function useNavigate() {
  // 跳转
  const { navigator } = useContext(NavigationContext);

  return navigator.push;
}


Outlet

只需要取出传递过来的child.element即可

import { useOutlet } from "./hooks";

export default function Outlet(props) {
  return useOutlet();
}


export function useOutlet() {
  const { outlet } = useContext(RouteContext);
  return outlet;
}

验证demo

import {
  BrowserRouter as Router,
  Routes,
  Route,
  Link,
  Outlet,
} from "./mini-react-router";

export default function App(props) {
  return (
    <div className="app">
      <Router>
        <Routes>
          <Route path="/" element={<Layout />}>
            <Route path="/" element={<Home />} />
            <Route path="product" element={<Product />}>
            </Route>
          </Route>
        </Routes>
      </Router>
    </div>
  );
}

function Layout(props) {
  return (
    <div>
      <h2>Layout</h2>
      <Link to="/">首页</Link>
      <Link to="/product">商品</Link>

      <Outlet />
    </div>
  );
}

function Home() {
  return (
    <div>
      <h1>Home</h1>
    </div>
  );
}

function Product() {
  return (
    <div>
      <h1>Product</h1>
    </div>
  );
}

Kapture 2023-10-02 at 23.05.59.gif
到目前为止,我们已经完成了路由跳转的核心功能。接下来,我们可以进一步思考如何实现动态匹配、路由守卫和懒加载等功能。


参考文章