likes
comments
collection

react router v6 使用详解以及部分源码解析(新老版本对比)

作者站长头像
站长
· 阅读数 9

前言

最近公司新起项目,想起来 react router 已经更新到 v6 版本,于是使用了新版本的路由,在此记录一下与之前版本的区别以及新版本的具体使用方法

老版本路由使用

  • BrowserRouter:通过 history 库,传递 history 对象,location 对象(一般服务端支持不使用 HashRouter)
    • hash 地址就是指 # 号后面的 url
  • Switch:匹配唯一的路由 Route,展示正确的路由组件
  • Route:视图承载容器,控制渲染 UI 组件
  • 获取路由状态
    • props + Route:可以通过 props 逐层传递
    • withRouter:withRouter 是一个高阶组件 HOC ,因为默认只有被 Route 包裹的组件才能获取到路由状态,如果当前非路由组件想要获取状态,那么可以通过 withRouter 包裹来获取 history、location、match 信息
    • useHistory、useLocation、useParams:函数组件可以通过这两个来获取 history、location、params 对象

路由跳转

  1. react-router-dom 内置的 Link、NavLink 来跳转

    // 跳转
    <Link to="/detail/123">skip</Link>
    
  2. 通过 history 对象下面的路由跳转方法

  • history.push()

  • history.replace() ,清除历史记录

    • 此方式跳转会删除历史记录,不会向 history 添加新记录,使用和传参方式和 history.push() 相同
    • Link 组件也可以使用 replace 方式,只需要在 Link 标签上添加 replace 属性即可
  • history.go(num) 路由跳转,前进、后退

    // 跳转
    import { useHistory } from 'react-router-dom';
    const history = useHistory();
    history.push({
      pathname: '/breeze',
      search: 'test=123',
      state: { test: 'test' },
      query: { id: 456 },
    });
    

接收参数

  1. 路由传参:通过 withRouter 包裹组件,在 props 上拿到 match 获取到;或者使用 useParams 拿到参数

    // 组件
    <Route exact path="/detail/:id" component={Detail}></Route>
    
    // 跳转
    <Link to="/detail/123">skip</Link>
    history.push('/detail/123')
    
    // 获取参数
    import { withRouter, useParams } from 'react-router-dom';
    export default withRouter((props) => {
      const { match } = props; // props方法
      const params = useParams(); // useParams 方法
      console.log(params);
      return 123
    })
    
  2. params 形式传参(出现在地址栏):通过 props 的 location 拿到;或者通过 useLocation

    //跳转
    history.push({ pathname: '/detail', search: 'id=123' })
    <Link to={{ pathname: '/detail', search: 'id=123' }} >skip</Link>
    <Link to={'/detail?id=123'} >skip</Link>
    
    // 获取参数
    import { useLocation } from 'react-router-dom'
    export default withRouter((props) => {
      const { search } = useLocation()
      const { location } = props
      console.log(location.search)
      return 123
    })
    
  3. state 形式传参:页面刷新不会丢失数据,并且地址栏也看不到数据;

    // 跳转
    history.push({ pathname: '/detail', state: { test: 'test' } });
    // 获取参数
    export default withRouter((props) => {
      const { state } = useLocation();
      const { location } = props;
      console.log(location.state);
      return 123;
    });
    
  4. query 形式传参:传参地址栏不显示,刷新地址栏,参数丢失;

    // 跳转
    history.push({ pathname: '/detail', query: { test: 'test' } });
    
    // 获取参数
    export default withRouter((props) => {
      const { query } = useLocation();
      const { location } = props;
      console.log(location.query);
      return 123;
    });
    

react router v6

介绍

  • BrowserRouter 和 HashRouter 还是在整个应用的最顶层,提供了 history 等核心的对象

Routes、Route

Route
  • Route 的 component 改成了 element

  • Route caseSensitive 属性 caseSensitive: boolean,用于正则匹配 path 时是否开启 ignore 模式(匹配时是否忽略大小写)

  • index 路由:加了 index 属性,有多个子路由的情况下,默认渲染加了 index 属性的路由组件

    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<List />} />
        <Route path="list" element={<List />} />
        <Route path="detail" element={<Detail />} />
      </Route>
    </Routes>
    
  • 只支持两种动态占位符

    :id
    path='article/:id'
    path='article/:id/detail'
    
    通配符,只能在 path 的末尾使用,如 article/*
    
  • Route 代码分析:Route 提供三种 props,主要使用参数的含义上文已经讲过

    1. 路径路由

      export interface PathRouteProps {
        caseSensitive?: boolean;
        // children 代表子路由
        children?: React.ReactNode;
        element?: React.ReactNode | null;
        index?: false;
        path: string;
      }
      
    2. 布局路由

    • 组件没有 path 属性,匹配当前内部的子路由,匹配完子路由向上寻找父路由提供的 element 进行渲染

      export interface LayoutRouteProps {
        children?: React.ReactNode;
        element?: React.ReactNode | null;
      }
      
    • 可以避免重复组件渲染,如下

        <Route element={<PageLayout />}>
          <Route path="/privacy" element={<Privacy />} />
          <Route path="/tos" element={<Tos />} />
        </Route>
      
        // 否则可能需要的写法,重复渲染了 PageLayout 组件
        <Route
          path="/privacy"
          element={
            <PageLayout>
              <Privacy />
            </PageLayout>
          }
        />
        <Route
          path="/tos"
          element={
            <PageLayout>
              <Tos />
            </PageLayout>
          }
        />
      
      
    1. 索引路由:index 的作用

      export interface IndexRouteProps {
        element?: React.ReactNode | null;
        index: true;
      }
      
Routes
  • v6 取消 Switch(可以根据当前的路由 path ,匹配唯一的 Route 组件加以渲染) 组件,取而代之的是在 Route 的外层必须加上 Routes 组件,否则报错

  • 子路由嵌套

    import { BrowserRouter, Routes, Route, Outlet, Link } from 'react-router-dom'
    
    function App() {
      return (
        <BrowserRouter >
          <Routes>
            <Route element={<Home />} path="/home" ></Route>
            <Route element={<Layout />} path="/father" >
              <Route element={<Child/>} path="/father/child" ></Route>
            </Route>
          </Routes>
        </BrowserRouter>
      )
    }
    
    function Layout() {
      return (
        <div>
          <Link to="child">to child</Link>
          <Outlet />
        </div>
      )
    }
    
    
  • 如果不是直接嵌套,而是在子组件中,需要尾部加上 *

    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="father/*" element={<Layout />} />
      </Routes>
    </BrowserRouter>
    
    function Layout() {
      return (
        <div>
          <Routes>
            <Route element={<ChildLayout />}>
            <Route path="child" element={<Child />} />
          </Route>
        </div>
      );
    }
    
    function ChildLayout () {
      return (
        <div>
          <Link to="child">to child</Link>
          <Outlet />
        </div>
      );
    }
    
    
  • 代码分析:

    1. 先看下 props 参数以及 Routes 方法

      export interface RoutesProps {
        children?: React.ReactNode;
        // 用户传入的 location 对象,一般不传,默认用当前浏览器的 location
        location?: Partial<Location> | string;
      }
      
      export function Routes({ children, location }: RoutesProps): React.ReactElement | null {
        // 此处调用了 useRoutes 这个 hook,并且使用了 createRoutesFromChildren 将 children 转换成了 useRoutes 所需要配置的参数格式
        return useRoutes(createRoutesFromChildren(children), location);
      }
      
    2. createRoutesFromChildren 方法

      // 参数,和上面路径路由的参数一致
      export interface RouteObject {
        caseSensitive?: boolean;
        children?: RouteObject[];
        element?: React.ReactNode;
        index?: boolean;
        path?: string;
      }
      
      export function createRoutesFromChildren(children: React.ReactNode): RouteObject[] {
        let routes: RouteObject[] = [];
      
        // 就是递归遍历 children 然后格式化后推入 routes 数组中
        React.Children.forEach(children, (element) => {
          // Ignore non-elements.
          if (!React.isValidElement(element)) {
            return;
          }
      
          // 如果类型为 React.Fragment 继续递归遍历
          if (element.type === React.Fragment) {
            // 相当于 routes.push(...createRoutesFromChildren(element.props.children))
            routes.push.apply(routes, createRoutesFromChildren(element.props.children));
            return;
          }
      
          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

  • 通过 useRoutes 生成元素

    import { useRoutes } from 'react-router-dom';
    function App() {
      const options = [
        {
          path: '/',
          element: <Layout />,
          children: [
            {
              path: 'detail',
              element: <Detail />,
            },
          ],
        },
      ];
    
      const element = useRoutes(options);
    
      return <div>{element}</div>;
    }
    
  • 代码分析

    • useRoutes 为整个 v6 版本的核心:即使前面使用了命令式的配置路由方式,根据代码分析也是可以看出,最终也是使用了 useRoutes 进行了转换

    • useRoutes 分为路由上下文解析(父子路由)、路由匹配、路由渲染三个步骤

      function useRoutes(
        routes: RouteObject[],
        locationArg?: Partial<Location> | string
      ): React.ReactElement | null {
        invariant(
          // 外层需要 router 包裹,否则报错
          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.`
        );
        let { matches: parentMatches } = React.useContext(RouteContext);
      
        let routeMatch = parentMatches[parentMatches.length - 1]; // 最后的一个 route 将作为父路由,后续的 routes 都是其子路由
        let parentParams = routeMatch ? routeMatch.params : {}; // 父路由参数
        // 父路由完整 pathname,如果路由设置 /article/*,当前导航 /article/1,那么值为 /article/1
        let parentPathname = routeMatch ? routeMatch.pathname : '/';
        // 同上类比,看 base 命名可以看出值为 /article
        let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : '/';
        let parentRoute = routeMatch && routeMatch.route;
      
        let locationFromContext = useLocation(); // 获取当前的 location 状态
        let location;
        // 判断是否传入 locationArg 参数,没有的话使用当前的 location
        if (locationArg) {
          // 格式化为 Path 对象
          let parsedLocationArg =
            typeof locationArg === 'string' ? parsePath(locationArg) : locationArg;
          // 如果传入了 location,判断是否与父级路由匹配(作为子路由存在)
          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;
        }
      
        let pathname = location.pathname || '/';
        let remainingPathname =
          parentPathnameBase === '/' ? pathname : pathname.slice(parentPathnameBase.length) || '/';
      
        // 通过传入的 routes 配置项与当前的路径,匹配对应渲染的路由
        let matches = matchRoutes(routes, { pathname: remainingPathname });
      
        // 调用_renderMatches方法,返回的是 React.Element,渲染所有的 matches 对象
        return __renderMatches(
          matches &&
            // 合并外层调用 useRoutes 得到的参数,内部的 Route 会有外层 Route(其实这也叫父 Route) 的所有匹配属性。
            matches.map((match) =>
              Object.assign({}, match, {
                params: Object.assign({}, parentParams, match.params),
                // joinPaths 函数用于合并字符串
                pathname: joinPaths([parentPathnameBase, match.pathname]),
                pathnameBase:
                  match.pathnameBase === '/'
                    ? parentPathnameBase
                    : joinPaths([parentPathnameBase, match.pathnameBase]),
              })
            ),
          // 外层 parentMatches 部分,最后会一起加入最终 matches 参数中
          parentMatches
        );
      }
      
      1. 解析上下文调用 useRoutes 的地方,如果是子路由调用,合并父路由的匹配信息

      2. 路由匹配:调用 matchRoutes 返回 matches 数组

        function matchRoutes(
          routes: RouteObject[],
          locationArg: Partial<Location> | string, // 当前匹配到的 location
          basename = "/"
        ): RouteMatch[] | null {
          // 转为 path 对象
          let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg; // 转为 path 对象
          let pathname = stripBasename(location.pathname || "/", basename);
          if (pathname == null) {
            return null;
          }
          let branches = flattenRoutes(routes); // 扁平化 routes 为一维数组,包含当前路由的权重
          rankRouteBranches(branches); // 根据权重排序
        
          let matches = null;
          // 这边遍历,判断条件如果没有匹配到就继续,匹配到就结束,知道所有的全部遍历完
          for (let i = 0; matches == null && i < branches.length; ++i)   {
            // 遍历扁平化的 routes,查看每个 branch 的路径匹配规则是否能匹配到 pathname
            matches = matchRouteBranch(branches[i], pathname);
          }
          return matches;
        }
        
      3. 路由渲染:_renderMatches 方法

        • _renderMatches

          function _renderMatches(
            matches: RouteMatch[] | null,
            // 如果在已有 match 的 route 内部调用,会合并父 context 的 match
            parentMatches: RouteMatch[] = []
          ): React.ReactElement | null {
            if (matches == null) return null;
            // 生成 outlet 组件,这边就是渲染 RouteContext.Provider 组件(嵌套关系)
            return matches.reduceRight((outlet, match, index) => {
              // 有 element 就渲染 element,如果没有则默认是 <Outlet />,继续渲染内嵌的 <Route />
              return (
               <RouteContext.Provider
                 children={ match.route.element !== undefined ? match.route.element : <Outlet /> }
                 value={{ outlet, matches: parentMatches.concat(matches.slice(0, index + 1)) }}
               />
              );
            // 最内层的 outlet 为 null,也就是最后的子路由
            }, null as React.ReactElement | null);
          }
          
        • RouteContext

          export type Params<Key extends string = string> = {
            readonly [key in Key]: string | undefined;
          };
          
          export interface RouteMatch<ParamKey extends string = string> {
            // params 参数,比如 :id 等
            params: Params<ParamKey>;
            pathname: string;
            // 子路由匹配之前的路径 url,这里可以把它看做是只要以 /* 结尾路径(这是父路由的路径)中 /* 之前的部分
            pathnameBase: string;
            route: RouteObject;
          }
          
          interface RouteContextObject {
            // 一个 ReactElement,内部包含有所有子路由组成的聚合组件,其实 Outlet 组件内部就是它
            outlet: React.ReactElement | null;
            // 一个成功匹配到的路由数组,索引从小到大层级依次变深
            matches: RouteMatch[];
          }
          
          // 包含全部匹配到的路由,官方不推荐在外直接使用
          const RouteContext = React.createContext<RouteContextObject>({
            outlet: null,
            matches: []
          });
          
          export {
            RouteContext as UNSAFE_RouteContext
          };
          

Outlet

  • 一般作为父 Route 组件里的占位元素,Outlet 是用来真正渲染子路由组件

    • 实际上就是内部渲染 RouteContext 的 outlet 属性
  • 提供了两种方式:useOutlet<Outlet />

    import { useOutlet, Outlet } from 'react-router-dom';
    function Child() {
      const outlet = useOutlet();
      return (
        <div>
          <h1>Dashboard</h1>
          {outlet}
          {/* or */}
          {/* <Outlet /> */}
        </div>
      );
    }
    
    function Father() {
      return (
        <Routes>
          <Route path="/" element={<Child />}>
            <Route path="detail" element={<Detail />} />
            <Route path="user" element={<User />} />
          </Route>
        </Routes>
      );
    }
    
  • 代码分析

    // 在 outlet 中传入的上下文信息
    const OutletContext = React.createContext < unknown > null;
    
    //可以在嵌套的 routes 中使用,这里的上下文信息是用户在使用 <Outlet /> 或者 useOutlet 时传入的
    export function useOutletContext<Context = unknown>(): Context {
      return React.useContext(OutletContext) as Context;
    }
    
    export function useOutlet(context?: unknown): React.ReactElement | null {
      let outlet = React.useContext(RouteContext).outlet;
      // 可以看到,当 context 有值时才使用 OutletContext.Provider,如果没有值会继续沿用父路由的 OutletContext.Provider 中的值
      if (outlet) {
        return (
          <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
        );
      }
      return outlet;
    }
    
    export interface OutletProps {
      // 可以传入要提供给 outlet 内部元素的上下文信息
      context?: unknown;
    }
    
    export function Outlet(props: OutletProps): React.ReactElement | null {
      return useOutlet(props.context);
    }
    
    • 可以看到 Outlet 上可以传入 context 参数,所以可以传值

      import { useOutlet, useOutletContext, Outlet } from 'react-router-dom';
      
      function Parent() {
        const [count, setCount] = React.useState(0);
        // 下面两种方式等同
        // const outlet = useOutlet([count, setCount])
        return <Outlet context={[count, setCount]} />;
      }
      
      // 在子路由中获取传入的上下文信息
      function Child() {
        const [count, setCount] = useOutletContext();
        const add = () => setCount((c) => c + 1);
        return <button onClick={add}>{count}</button>;
      }
      

路由跳转

Link
  • 路径的写法支持相对路径

    • 在 v5 版本中,例如父组件 Article 中, <Link to="detail"> 会跳转到 /detail,想要正常跳转需要写成 <Link to="/article/detail">
    • v6 中直接写成 <Link to="detail"> 即可正常跳转
  • 在 v6 中,路由跳转,像常用的 cd 命令

    
    当前路由为 /dashboard/article 或者 /dashboard/article/
    
    <Link to="detail"> -> /dashboard/article/detail
    <Link to="../detail"> -> /dashboard/detail
    <Link to="../../detail"> -> /detail
    
useNavigate
  • 新版本提供 useNavigate(useHistory 没了)

    • navigate 默认就是 history.push

      import { useNavigate } from 'react-router-dom';
      const navigate = useNavigate();
      navigate('/detail?id=1', { state: 'test' }); // 默认就是 history.push()
      
    • naviaget(to, { replace: true }) 等同于 history.replace

    • naviaget(number) 等同于 history.go

      navigate(-2); // go 2 pages back
      navigate(-1); // go back
      navigate(1); // go forward
      navigate(2); // go 2 pages forward
      

获取参数以及状态

useParams
  • 获取动态路由参数

    <Route element={<Detail/>} path="/detail/:id"></Route>
    <button onClick={() => { navigate('/detail/1') } } >跳转详情页</button>
    const params = useParams()
    console.log(params,'params')
    
useLocation
  • 获取状态,有 hash、key、pathname、search、state

    import { useLocation } from 'react-router-dom';
    let location = useLocation();
    
useSearchParams
  • 获取参数,第一个获取参数,第二个设置参数

    import { useSearchParams } from 'react-router-dom';
    const [getParams, setParam] = useSearchParams();
    console.log(getParams.get('id'));
    setParam({ id: '2' });
    
更多hooks

参考