BrowserRouter

先看一些例子

const App = () => {
  return (
    <BrowserRouter>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Layout />}>
            <Route index element={<Home/>} />
            <Route path="/courses" element={<CourseLayout />}>
              <Route index element={<CourseIndex/>} />
              <Route path="/courses/:id" element={<Course/>} />
            </Route>
          </Route>
          {/* <Route path="*" element={<NotMatch/>} /> */}
        </Routes>
      </BrowserRouter>
    </BrowserRouter>
  )
}

源码实现

  • 使用 history 库 创建 BrowserHistory
  • 监听当前history变化, 即当 url 中的location 发生变化时, 会重新setState
  • 讲这些参数传到<Router />组件中, <Router /> 组件 其实就是包含两个 provider 的 context
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}
    />
  );
}
  • 组件简易实现

传下去的children 就是 , 后面需要对 Routes下的children 进行遍历

const Router = (props) => {
  const { 
    basename, 
    children, 
    location: locationProps 
    navigator
  } = props;

  // 对当前locationProps 做处理,最后返回, 这里我直接用‘’ 表示
  const location = {
    pathname: '',
    search: '',
    hash: '',
    state: '',
    key: '',
  }

  // navigator 就是 history
  let navigationContext = React.useMemo(
    () => ({ basename, navigator, static: staticProp }),
    [basename, navigator, staticProp]
  );

  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider
        children={children}
        value={{ location, navigationType }}
      />
    </NavigationContext.Provider>
  )
}
  • 再来看
export function Routes({
  children,
  location,
}: RoutesProps): React.ReactElement | null {
  return useRoutes(createRoutesFromChildren(children), location);
}

这里说明一下<Route> 就是一个空方法,但在下面遍历的时候会判断当前组件是不是<Route /> 后面也是会直接收集element 属性
createRoutesFromChildren 是对 进行children 遍历,获取路由,这里会进行递归操作,如果 进行嵌套的话,element.props.children 就会进行递归遍历
在遍历过程中会判断当前组件是不是,如果不是会报错
已上面例子为例: 就会生成以下结果:

[
  {
    caseSensitive: '',
    element: <Layout />,
    index: '',
    path: '/',
    children: [
      { 
        caseSensitive: '',
        element: <Home />,
        index: true,
        path: undefined,
      },
      {
        caseSensitive: '',
        element: <CourseLayout />,
        index: undefined,
        path: "/courses",
        children: [
          // 略
        ]
      }
    ]
  }
]

export function createRoutesFromChildren(
  children: React.ReactNode
): RouteObject[] {
  let routes: RouteObject[] = [];

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

    if (element.type === React.Fragment) {
      routes.push.apply(
        routes,
        createRoutesFromChildren(element.props.children)
      );
      return;
    }

    // 判断当前children 是不是<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 这个hook, 这个hook 生成了一个ReactNode, 是一个 RouterContext.Provider 生成的节点

    • 从上面得到的routes列表以及 url 上的location 可以得到符合当前地址的所有路由,数组顺序为路由的辈分关系
      比如上面的 /courses 连接, 会得到一下匹配的路由
    matches = [
      {
        params: {},
        pathname: "/",
        pathnameBase: "/",
        route: [
          { 
            caseSensitive: '',
            element: <Layout />,
            index: '',
            path: '/',
            children: [
              // 略
            ]
          }
        ]
      },
      { 
        params: {},
        pathname: "/courses",
        pathnameBase: "/courses",
        route: [
          { 
            caseSensitive: '',
            element: <CourseLayout />,
            index: undefined,
            path: "/courses",
            children: [
              // 略
            ]
          }
        ]
      },
      { 
        params: {},
        pathname: "/courses/",
        pathnameBase: "/courses",
        route: [
          { 
            caseSensitive: '',
            element: <CourseIndex />,
            index: true,
            path: undefined,
          }
        ]
      }
    ]
    • 生成节点, 这里的outlet 可以当做children,在官网上可以用 <Outlet /> 表示 children

      • 如果有element, 那么用provider 包裹一下,注意:这里是从孙子节点到爷爷节点反序构建节点的
      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);
      }
      • 可以这么理解
      // 第一次
      let first = (
        <RouteContext.Provider>
          children={
            <CourseIndex />
          }
          value={{ 
                outlet: null,
                matches: matches.slice(0, 3)
              }}
        >
        </RouteContext.Provider>
      )
      
      // 第二次
      let second = (
        <RouteContext.Provider>
          children={
            <CourseLayout />
          }
          value={{ 
                outlet: first,
                matches: matches.slice(0, 2)
              }}
        >
        </RouteContext.Provider>
      )
      
      // 第三次
       let third = (
        <RouteContext.Provider>
          children={
            <Layout />
          }
          value={{ 
                outlet: second,
                matches: matches.slice(0, 1)
              }}
        >
        </RouteContext.Provider>
      )

在嵌套的路由中渲染 children

  • 文档地址
  • 嵌套路由中使用children, 是使用 API 中的 <Outlet />,
  • 我们知道上面在匹配matches 的时候, 会进行<RouteContext.Provider />封装,同时传递 outlet 给他的子组件,那么<OutLet />可以消费他的context
  • 实现
const OutletContext = React.createContext<unknown>(null);

export function Outlet(props: OutletProps): React.ReactElement | null {
  return useOutlet(props.context);
}

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 /> 标签 做的,
  • 当我们传入自定义onClick 事件,会在点击时运行,后续会判断一下event.defaultPrevented, 表明当前事件是否被调用了,如果没有则调用内置internalOnClick 事件
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
  function LinkWithRef(
    { onClick, reloadDocument, replace = false, state, target, to, ...rest },
    ref
  ) {
    let href = useHref(to);
    let internalOnClick = useLinkClickHandler(to, { replace, state, target });
    function handleClick(
      event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
    ) {
      if (onClick) onClick(event);
      if (!event.defaultPrevented && !reloadDocument) {
        internalOnClick(event);
      }
    }

    return (
      <a
        {...rest}
        href={href}
        onClick={handleClick}
        ref={ref}
        target={target}
      />
    );
  }
);

useLinkClickHandler 的实现

其实就是调用了 history.push 或者是 history.replace 做跳转

export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
  to: To,
  {
    target,
    replace: replaceProp,
    state,
  }: {
    target?: React.HTMLAttributeAnchorTarget;
    replace?: boolean;
    state?: any;
  } = {}
): (event: React.MouseEvent<E, MouseEvent>) => void {
  let navigate = useNavigate();
  let location = useLocation();
  let path = useResolvedPath(to);

  return React.useCallback(
    (event: React.MouseEvent<E, MouseEvent>) => {
      if (
        // 表示用户点击了鼠标左键
        event.button === 0 &&
        (!target || target === "_self") &&
        !isModifiedEvent(event) 
      ) {
        event.preventDefault();

        // 判断一下当前应该用 history.replace 还是 history.push
        let replace =
          !!replaceProp || createPath(location) === createPath(path);

        navigate(to, { replace, state });
      }
    },
    [location, navigate, path, replaceProp, state, target, to]
  );
}

useNavigation 实现

一开始 <BrowserHistory /> 下的<Router /> 使用 <NavigationContext.Provider /> 下发 navigator,
最后返回 navigate 方法,实际上就是 history.replace 或者是 history.push

export type Navigator = Pick<History, "go" | "push" | "replace" | "createHref">;
export function useNavigate(): NavigateFunction {
  // 这里拿到 navigator, 实际就是 history, 就是一开始 createBrowserHistory
  let { basename, navigator } = React.useContext(NavigationContext);
  let { matches } = React.useContext(RouteContext);
  let { pathname: locationPathname } = useLocation();

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

  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
      );

      if (basename !== "/") {
        path.pathname = joinPaths([basename, path.pathname]);
      }
      // 最后这里 进行 history.replace 或者是 history.push
      (!!options.replace ? navigator.replace : navigator.push)(
        path,
        options.state
      );
    },
    [basename, navigator, routePathnamesJson, locationPathname]
  );

  return navigate;
}