美文网首页
react深入3 - react-router

react深入3 - react-router

作者: 申_9a33 | 来源:发表于2022-02-12 18:39 被阅读0次

1. react-router 使用

1.1 安装 npm i react-router-dom

1.2 基本使用

// src\main.tsx
import React from 'react';
import { render } from 'react-dom';
import {
  BrowserRouter, Routes, Route, Link,
} from 'react-router-dom';
import HomePage from './view/Home';
import Login from './view/Login';
import Product from './view/Product';
import Details from './view/Details';
import Error from './view/Error';

function App() {
  return (
    <div>
      <BrowserRouter>
        <div>
          <Link to="/"> home </Link>
          |
          <Link to="/login"> login </Link>
          |
          <Link to="/product"> product </Link>
          |
          <Link to="/product/123"> details </Link>
          |
        </div>

        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/login" element={<Login />} />
          <Route path="/product" element={<Product />}>
            <Route path="/product/:id" element={<Details />} />
          </Route>
          <Route path="*" element={<Error />} />
        </Routes>

      </BrowserRouter>
    </div>
  );
}

render(<App />, document.getElementById('root'));

1.3 history 模式webpack 需要开启 historyApiFallback: true

1.4 嵌套路由 使用 Outlet

// src\view\Product.tsx

import React from 'react';
import { Outlet } from 'react-router-dom';

export default function Product() {
  return (
    <div>
      product
      <Outlet />
    </div>
  );
}

2.react-route实现(以history模式来实现)

2.1 BrowserRouter 组件

  • 需要以history模式,提供navigator 以及 当前location
  • 这里使用 history 库,创建一个history作为navigator ,然后监听history.listen监听当前路由变化,并且存储在state中
import * as React from 'react';
import { createBrowserHistory, BrowserHistory } from 'history';
import Router from './Router';

export interface BrowserRouterProps {
  children?: React.ReactNode;
  window?: Window;
}
export default function BrowserRouter({
  children,
  window,
}:BrowserRouterProps) {
  const historyRef = React.useRef<BrowserHistory>();
  if (!historyRef.current) {
    historyRef.current = createBrowserHistory({ window });
  }

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

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

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

2.2 Router 组件

  • 提供 navigator 用于 push,go,back等路由导航操作
  • 提供 location 代表当前路由的一些信息,path,params,query等
  • 他不关心你是history,还是hash,或者memory模式,只负责将接收到的navigator 和location ,通过context的方式传下去抹平不同模式的差异
export default function Router({
  children = null,
  location: locationProp,
  navigationType = NavigationType.Pop,
  navigator,
}: IRouterProps):React.ReactElement | null {
  const navigationContext = React.useMemo(
    () => ({ navigator }),
    [navigator],
  );

  if (typeof locationProp === 'string') {
    // eslint-disable-next-line no-param-reassign
    locationProp = parsePath(locationProp);
  }

  const {
    pathname = '/',
    search = '',
    hash = '',
    state = '',
    key = 'default',
  } = locationProp;
  const location = React.useMemo(() => ({
    pathname,
    search,
    hash,
    state,
    key,
  }), [hash, key, pathname, search, state]);

  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider
        value={{ location, navigationType }}
      >
        {children}
      </LocationContext.Provider>
    </NavigationContext.Provider>
  );
}

2.3 Routes 组件

2.3.1 根据 children 里面的Route 得到路由配置,内部包含路由地址和React组件,类似于

[
  {
    path:'/product',
    element=ReactComponents,
    children:[
      {
          path:'/product/:id',
          element=ReactComponents,
      }
      // ...
    ]
  }

  // ... 
]
  • createRoutesFromChildren 得到上述的路由配置
  • 方式就是获取 Route 组件props里面传入的 path和element
export function createRoutesFromChildren(
  children:React.ReactNode,
):RouteObject[] {
  const routes:RouteObject[] = [];

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

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

    const route:RouteObject = {
      element: element.props.element,
      path: element.props.path,
    };

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

    routes.push(route);
  });

  return routes;
}

2.3.2 根据路由配置和当前路由,匹配出需要渲染的组件,得到类似于

[
  {
    params:{},
    pathname:'/product',
    route:{
          path:'/product',
          element=ReactComponents,
    }
  }
  //...
]
  • params 是匹配路由的参数
  • pathname 是以什么字符串匹配到的路由
  • route 是匹配到的路由配置
  • 路由上的多个组件,父组件在前面,子组件在后面
  • 具体匹配逻辑 matchRoutes放在最后说

2.3.3 renderMatches 渲染匹配出来的组件

  • 已知matches.reduceRight会从数组右到左执行reducer,reducer 第一个参数是上一个reducer 返回的值,第二个参数是当前值,最后一个index是元素在数组中的索引
  • matchRoutes匹配出来的路由顺序是父组件在前面,例如[parentComponent,childComponent]
  • renderMatches的做法是使用matches.reduceRight,那么第一次执行的必然是最后一个组件,这里是childComponent,然后reducer里面使用context包裹childComponent,并返回,后续reducer执行时,接受到上一个包裹组件,并且放在context的value中,包裹当前组件,并且返回,依此类推
  • 得到一个类似与下面的结构
<Context.Provider value={
  <Context.Provider value={null}>
    {childComponent}
  </Context.Provider>
}>
    {parentComponent}
</Context.Provider>
  • 因为Context会匹配最近的Context提供的值,所以父组件里面的Outlet,会拿到父组件的Provider Value,如果子组件还有子组件,也会依次拿到自己的子组件的值
  • 总而言之大佬的思维还是牛逼
export function renderMatches(
  matches:RouteMatch[] | null,
  parentMatches:RouteMatch[] = [],
):React.ReactElement | null{
  if (matches === null) return null;

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

2.4 Route 组件

  • 这个组件不渲染,主要作用是props接受用户传入的值
  • Routes 组件,会从Route 拿到props.path和props.element,从而得到路由配置
  • 感觉这里可以跟vue一样,用一个配置文件不更好,不知道大佬啥想法
export default function Route(
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  _props: PathRouteProps | LayoutRouteProps | IndexRouteProps,
): React.ReactElement | null {
  throw new Error('A <Route> is only ever to be used as the child of <Routes> element, '
  + 'never rendered directly. Please wrap your <Route> in a <Routes>.');
}

2.5 Outlet 组件

  • 已知 Routes 会将每个组件用Context包一层,并且Context里面存放着子组件
  • 又因为Context的就近原则,所以Outlet 只用拿Context的值,就能对应的子组件
import * as React from 'react';
import { OutletContext, RouteContext } from './context';

export function useOutlet(context?: unknown): React.ReactElement | null {
  const { outlet } = React.useContext(RouteContext);
  if (outlet) {
    return (
      <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
    );
  }
  return outlet;
}

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

2.7 Link组件

  • 已知 Router 存放着 navigator 和 location
  • Link只用拿到navigator 执行一下里面的方法
import React, { MouseEvent, useContext } from 'react';
import { NavigationContext } from './context';

export default function Link({ to, children }:any) {
  const navigation = useContext(NavigationContext);

  const handle = (e:MouseEvent<HTMLAnchorElement>) => {
    e.preventDefault();

    navigation.navigator.push(to);
  };
  return <a href={to} onClick={handle}>{children}</a>;
}

源码

相关文章

网友评论

      本文标题:react深入3 - react-router

      本文链接:https://www.haomeiwen.com/subject/ylhykrtx.html