react-router6源码学习

324 阅读3分钟

思维导图

image.png

demo

// import {
//   BrowserRouter as Router,
//   // HashRouter as Router,
//   // MemoryRouter as Router,
//   Routes,
//   Route,
//   Link,
//   Outlet,
//   useNavigate,
//   useParams,
//   useResolvedPath,
// } from "react-router-dom";

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

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

function Layout() {
  return (
    <div>
      <Link to="/">首页</Link>
      <Link to="product">商品</Link>
      <Outlet />
    </div>
  );
}

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

function Product() {
  const path = useResolvedPath("123");
  console.log("path", path); //sy-log
  return (
    <div>
      <h1>Product</h1>
      <Link to="123">详情</Link>
      <Outlet />
    </div>
  );
}

function ProductDetail() {
  const params = useParams();
  const navigate = useNavigate();
  return (
    <div>
      <h1>ProductDetail: {params.id}</h1>
      <button
        onClick={() => {
          navigate("/");
        }}
      >
        go home
      </button>
    </div>
  );
}

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

BrowserRouter

import { createBrowserHistory } from "history";
import { useRef, useState, useLayoutEffect } from "react";

import Router from "./Router";

export default function BrowserRouter({ children }) {
  // 存值,在组件卸载前,这个值在组件任何一个生命周期都指向同一个地址
  let historyRef = useRef();
  if (historyRef.current == null) {
    historyRef.current = createBrowserHistory();
  }

  let history = historyRef.current;
  //location是为了实现useLocation用的
  const [state, setState] = useState({ location: history.location });

  useLayoutEffect(() => {
    const unlisten = history.listen(setState);
    // history.listen((location) => {
    //   setState({ location });
    // });

    return unlisten;
  }, [history]);

  return (
    <Router children={children} naviagtor={history} location={state.location} />
  );
}

  • history是h5的api,可提供replace、push等路由跳转api
  • historyRef保存了history的值,在组件卸载前,这个值在任何一个生命周期都指向同一个地址
  • navigator的作用是给后代传history值,比如可以实现useNavigate这个自定义hook。location主要是监听路由地址的变化,用于实现useLocation

Router

import { NavigationContext } from "./Context";

// 跨组件层级传递数据 context
export default function Router({ naviagtor, children, location }) {
  let naviagtionContext = { naviagtor, location };

  return (
    <NavigationContext.Provider value={naviagtionContext}>
      {children}
    </NavigationContext.Provider>
  );
}

单纯作为一个中间封装,到后来HashRouter也会用到这个组件

Routes

import React, { isValidElement } from "react";
import { useRoutes } from "./hooks";

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

  React.Children.forEach(children, (child) => {
    if (!isValidElement(child)) {
      return;
    }

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

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

    routes.push(route);
  });

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

  return useRoutes(routes);
}

  • createRoutesFromChildren借助虚拟dom转化为fiber的实现,从child里获取route,最终得到routes
  • useRoutes里的实现是路由匹配渲染的核心,具体可见useRoutes的实现

hooks

import { useContext, useMemo } from "react";
import { matchRoutes } from "react-router-dom";
import { parsePath } from "history";
import Outlet from "./Outlet";
import { NavigationContext, RouteContext } from "./Context";
import {
  normalizePathname,
  //  matchRoutes
} from "./utils";

// todo 1.监听location,准确渲染路由
// todo 2. 渲染子路由 实现Outlet

export function useRoutes(routes) {
  // 遍历routes,渲染匹配的route
  const location = useLocation();
  let pathname = location.pathname;
  console.log(routes, "routes000");

  const matches = matchRoutes(routes, { pathname });
  console.log(matches, "matches000");

  return _renderMatches(matches);
}

function _renderMatches(matches, parentMatches = []) {
  if (matches == null) {
    return null;
  }

  return matches.reduceRight((outlet, match, index) => {
    return (
      <RouteContext.Provider
        children={match.route.element}
        value={{
          outlet,
          matches: parentMatches.concat(matches.slice(0, index + 1)),
        }}
      />
    );
  }, null);
}

export function useLocation() {
  const { location } = useContext(NavigationContext);
  return location;
}

// 路由跳转函数
export function useNavigate() {
  const { naviagtor } = useContext(NavigationContext);
  return naviagtor.push;
}

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

export function useParams() {
  const { matches } = useContext(RouteContext);

  const rootMatch = matches[matches.length - 1];
  return rootMatch ? rootMatch.params : {};
}

export function useResolvedPath(to) {
  const { matches } = useContext(RouteContext);

  const { pathname: locationPathname } = useLocation();

  let routePathenameJoson = JSON.stringify(
    matches.map((match) => match.pathnameBase)
  );

  return useMemo(
    () => resolveTo(to, JSON.parse(routePathenameJoson), locationPathname),
    [to, routePathenameJoson, locationPathname]
  );
}

function resolveTo(toArg, routePathnames, locationPathname): Path {
  let to = typeof toArg === "string" ? parsePath(toArg) : toArg;
  let toPathname = toArg === "" || to.pathname === "" ? "/" : to.pathname;

  let from;
  if (toPathname == null) {
    from = locationPathname;
  } else {
    let routePathnameIndex = routePathnames.length - 1;

    if (toPathname.startsWith("..")) {
      let toSegments = toPathname.split("/");

      while (toSegments[0] === "..") {
        toSegments.shift();
        routePathnameIndex -= 1;
      }

      to.pathname = toSegments.join("/");
    }

    from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
  }

  let path = resolvePath(to, from);

  if (
    toPathname &&
    toPathname !== "/" &&
    toPathname.endsWith("/") &&
    !path.pathname.endsWith("/")
  ) {
    path.pathname += "/";
  }

  return path;
}

export function resolvePath(to, fromPathname = "/"): Path {
  let {
    pathname: toPathname,
    search = "",
    hash = "",
  } = typeof to === "string" ? parsePath(to) : to;

  let pathname = toPathname
    ? toPathname.startsWith("/")
      ? toPathname
      : resolvePathname(toPathname, fromPathname)
    : fromPathname;

  return {
    pathname,
  };
}

function resolvePathname(relativePath, fromPathname) {
  let segments = fromPathname.replace(/\/+$/, "").split("/");
  let relativeSegments = relativePath.split("/");

  relativeSegments.forEach((segment) => {
    if (segment === "..") {
      if (segments.length > 1) segments.pop();
    } else if (segment !== ".") {
      segments.push(segment);
    }
  });

  return segments.length > 1 ? segments.join("/") : "/";
}

Link

import { useNavigate, useResolvedPath } from "./hooks";

export default function Link({ to, children }) {
  const { pathname } = useResolvedPath(to);

  const navigate = useNavigate();

  const handleClick = (e) => {
    e.preventDefault();
    // 跳转
    navigate(pathname);
  };

  return (
    <a href={pathname} onClick={handleClick}>
      {children}
    </a>
  );
}

Outlet

import { useOutlet } from "./hooks";

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

utils

// 1. 删除结尾的多个/
// 2. 删除开头的多少/

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

// 如 ///product/detail/// -> /product/detail
export const normalizePathname = (pathname) =>
  pathname.replace(/\/+$/, "").replace(/^\/*/, "/");

// [/, /product] -> ///product -> /product
const joinPaths = (paths) => paths.join("/").replace(/\/\/+/g, "/");

function stripBasename(pathname, basename) {
  if (basename === "/") return pathname;

  if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
    return null;
  }

  let nextChar = pathname.charAt(basename.length);
  if (nextChar && nextChar !== "/") {
    // pathname does not start with basename/
    return null;
  }

  return pathname.slice(basename.length) || "/";
}

export function matchRoutes(routes, location) {
  let pathname = location.pathname;
  let branches = flatternRoutes(routes);

  let matches = null;

  for (let i = 0; matches == null && i < branches.length; i++) {
    matches = matchRouteBranch(branches[i], pathname);
  }

  return matches;
}

function flatternRoutes(
  routes,
  branches = [],
  parentMeta = [],
  parentPath = ""
) {
  routes.forEach((route) => {
    let meta = {
      relativePath: route.path || "",
      route,
    };

    let path = joinPaths([parentPath, meta.relativePath]);
    let routesMeta = parentMeta.concat(meta);

    if (route.children && route.children.length > 0) {
      flatternRoutes(route.children, branches, routesMeta, path);
    }

    if (route.path == null && !route.index) {
      return;
    }
    branches.push({
      path,
      routesMeta,
    });
  });

  return branches;
}

// /prodcut/123
function matchRouteBranch(branch, pathname) {
  const { routesMeta } = branch;

  let matches = [];
  for (let i = 0; i < routesMeta.length; i++) {
    let meta = routesMeta[i];

    let end = routesMeta.length - 1 === i;
    let match = matchPath({ path: meta.relativePath, end }, pathname);

    if (!match) {
      return null;
    }

    matches.push({
      params: match.params,
      pathname: match.pathname,
      route: meta.route,
    });
  }

  return matches;
}