react-router v6 的回忆

481 阅读7分钟

学了不用,等于没学;学了长期不用,这 TM 又是什么新技术。

为什么会叫做 react-router v6 的回忆呢?

因为在去年的时候,公司的每两周都会进行一次前端技术分享会。轮到我的时候,我就选择了比较新的 react-router v6 作为主题素材,我前前后后大致花费了半个月多的时间,对 react-router v6 的基本使用、注意事项,到后面的源码分析,最后用 PPT 进行呈现的准备工作。我那是天真的以为我对 react-router v6 的使用已经非常的熟练了,可是现实打败了我。

最近在复习 react 相关的技术栈的时候,我对 react_router v6又陌生了,是不是很讽刺?

所以这篇就是结合我以前准备的 PPT,对 react-router v6 的回忆。

开始回忆

2.png

01:V6的基本介绍

3.png

4.png

02:前端路由基本原理

5.png

6.png

7.png

8.png

9.png

10.png

03:V6 的新特性

11.png

12.png

<BrowserRouter><HashRouter>的异同点:

  • 创建 historyRef ,调用的函数不同,就走了两套不同的路由模式。
  • 都是返回一个 <Router> 组件。

13.png

该两个函数都是来源于 history 第三库,history 库也分为了两个版本:v4v5react-router v6 就是走的 v5 版本。

14.png

这是我在 github 读取 history 库的源码截图。

  • 返回的 history 对象
  • push 函数的内部实现。

15.png

<Router> 组件来自于 react-router 的核心包里面。

源码目录补充:

react-router:是整个路由核心的实现,为 react-router-domreact-router-native 提供支持。

react-router-dom:供 web 端(浏览器)使用。

react-router-native:供 react-native 使用。

Router 组件的实现,使用了嵌套的两个 context,给容器里面的内容,提供支持。比如常见的 location 对象,navigator 对象(也就是history 对象)。

16.png

BrowserRouter 和 HashRouter 接收的 props。

17.png

react-router 新的hooks 和 components。

18.png

就是用 <Router> 组件中的第二层 context:LocationContext,返回 location 对象。

19.png

跳转路由。

20.png

useRoutes() 是 react-router v6 非常重要的一个 hooks,后面的基本上所有的逻辑都围绕着它转。

21.png

22.png

23.png

hooks 完了,就来组件。

24.png

25.png

26.png

27.png

这里就是 V5 形式的组件写法,转成 tree 的数据结构。

在 V6 中,可以自己写一个树状结构(类似于 react-router-config ),路由统一管理,直接传递给 useRoutes 直接处理(也是推荐的写法)。

28.png

29.png

对于上面两个组件,都是处于可用可不用的状态,特别是 <NavLink> 组件。

30.png

组件式的跳转路由。

31.png

嵌套路由的占位符,相信使用过的,都知道。

哈哈哈,还在这里感悟一下。

04:V6 的案例演示

32.png

33.png

到了这里,就可以动手实操了(其实本来不想演示的,但是都到了这步,还是写写吧,也回顾一下)。

// App.tsximport React, { Suspense } from "react";
import {
  BrowserRouter,
  useRoutes,
  useNavigate,
} from "./react-router-dom";
import { routers } from "./router";
​
// 第一种:直接使用 useRoutes() 来注册路由
function RenderRouter() {
  const elements = useRoutes(routers);
  return <div>{elements}</div>;
}
​
// 第二种:V5 方式的组件式写法(当然嵌套,又有两种写法,这里就不一一写出)
function RenderRouter() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/detail" element={<Detail />} />
      {/* 如果是这种形式的话,需要添加 /*  , 不然跳转不过去  */}
      <Route path="/list" element={<List />} />
      {/* <Route path="/list" element={<List />}>
        <Route path="list1" element={<List1 />} />
        <Route path="list2/:listId/:listName" element={<List2 />} />
      </Route> */}
    </Routes>
   )
}
​
function RenderContent() {
  const navigate = useNavigate();
  const path = (path: string) => {
    navigate(path);
  };
  return (
    <div>
      <button onClick={() => path("/home")}>首页</button>
      <button onClick={() => path(`/detail?name=copyer&age=18`)}>详情</button>
      <button onClick={() => path("/list")}>列表</button>
    </div>
  );
}
​
function App() {
  return (
    <BrowserRouter>
      {/* 路由展示 */}
      <RenderContent />
      {/* 路由配置 */}
      {/* 使用React.lazy懒加载的方式,需要使用Suspense,不然会报错 */}
      <Suspense>
        <RenderRouter />
      </Suspense>
    </BrowserRouter>
  );
}
​
export default App;

组件式写法,路由不能统一管理,不推荐使用。

还是直接使用 useRoutes 的方式,下面式路由统一管理的文件。

// router/index.tsx: 路由统一管理import { RouteObject, Navigate } from "../react-router-dom";
import { lazy } from "react";
​
import Home from "../pages/Home";
const Detail = lazy(() => import("../pages/Detail"));
const List = lazy(() => import("../pages/List"));
const List1 = lazy(() => import("../pages/List/List1"));
const List2 = lazy(() => import("../pages/List/List2"));
​
const routers: RouteObject[] = [
  {
    path: "/",
    element: <Navigate to="/home" />, // 重定向
  },
  {
    path: "/home",
    element: <Home />,
  },
  {
    path: "/detail",
    element: <Detail />,
  },
  {
    path: "/list",
    element: <List />,
    children: [
      {
        path: "list1",
        element: <List1 />,
      },
      {
        path: "list2/:listId/:listName",
        element: <List2 />,
      },
    ],
  },
  {
    path: "*",
    element: <div>404</div>,
  },
];
​
export { routers };
// List.tsximport React from "react";
import {
  useNavigate,
  Outlet,
} from "../../react-router-dom";
// import List1 from "./List1";
// import List2 from "./List2";
const List = () => {
  // const location = useLocation();
  // console.log("location对象", location);
​
  const navigate = useNavigate();
  const path = (path: string) => {
    navigate(path);
​
    // 当前路径 /aa/bb
    // navigate('../cc')  // 会被解析成 /aa/cc
  };
​
  return (
    <React.Fragment>
      <h2>List组件</h2>
      <div>
        <button onClick={() => path("list1")}>list1</button>
        <button onClick={() => path("list2/32132/列表名称")}>list2</button>
      </div>
      
      
      {/* 统一管理的方式:  二级路由展示 */}
      <Outlet />
      
      {/* 分开式的管理方式:  二级路由展示*/}
      <Routes>
        <Route path="list1" element={<List1 />} />
        <Route path="list2/:listId/:listName" element={<List2 />} />
      </Routes>
      
    </React.Fragment>
  );
};
​
export default List;
​

上面是嵌套的路由方式,两种写法:

  • <Outlet> 占位符,统一式路由管理的写法
  • 继续 <Routes><Route> 的嵌套写法,这里必须成对。

其他的代码就不需要演示了,就是对 api 和 组件的应用。

05:V6 的源码分析

34.png

跟着问题读源码,分析源码,知道原因后,就会有着一种小小的满足感。

35.png

36.png

37.png

在解答上面的问题时,先来看看 useRoutes 的源码的大致流程,所以的答案基本上就在里面,这也是我为什么说 useRoutes 是非常重要的原因。

38.png

useRoutes 内部的实现:

  • 第一步:拿到当前的 pathname

  • 第二步:调用 matchRoutes 函数,参数为 路由配置(tree 的数据结构)pathname

    • flattenRoutes:扁平化函数,把 tree 的路由数据结构平铺,在此过程中组装了路由 path 的完整路径、使用 routesMeta 保存了路由层级的数据、计算了路由对应的分数(score)等系列操作,这里可以把平铺好的数据看成一个个分支(branch)。
    • rankRouteBranches:根据平铺好的数据,依据它们的 score 进行排序,从到高到低。
    • 遍历 branch,依次调用 matchRouteBranch 函数,去匹配 pathname,匹配上了,返回对应的路由信息。
  • 第三步:执行 _renderMatches 函数,接收上面返回的路由信息作为参数,渲染路由组件。

下图是路由平铺的数据展示:

39.png

大致对 useRoutes 的源码流程有所了解之后,就来解决上面的五个问题。

问题一:<Outlet> 组件实现的原理

40.png

遍历分支,匹配上 pathname 之后,返回路由信息。这里着重关注一下,路由的返回格式:[ 一级路由信息, 二级路由信息]

_renderMatches 函数,拿到返回的路由信息就开始渲染,那么源码内部的实现呢?

41.png

必须想清楚 reduceRight 函数,跟 reduce 函数基本上是一致的,只不过顺序反了而已,累积的一种过程。

这里就是一个节点累积,先返回下级路由的 <RouteContext>,然后在把子组件的 <RouteContext> 继续累积在上级路由的 <RouteContext>,从而:

返回一个嵌套的 <RouteContext> 组成的 ReactElement 元素,在渲染的时候,就是采用的就近原则,进行渲染。

42.png

上面大致就是 Outlet 的实现原理,不知道讲清楚没有。

问题二:navigate 函数接收多种参数,内部如何解析?

43.png

其实,这里的一些判断还是比较简单的,对 to 属性的类型判断,对跳转方式的判断。

这里比较头痛的,就是这里的 resolveTo 函数,对 to 属性的完全解析。

44.png

参数来源。

定义 resolveTo 函数,下面截图中没有体现出来,所以补充出来了。

export function resolveTo(
  toArg: To,
  routePathnames: string[],
  locationPathname: string
): path{
  let to = typeof toArg === "string" ? parsePath(toArg) : toArg;
  let toPathname = toArg === "" || to.pathname === "" ? "/" : to.pathname;
}
/*
  中间就是截图部分了
*/let path = resolvePath(to, from);

45.png

这里应该还是很好理解的,就是处理 navigate 传递的参数存在 ..,回退匹配信息 matches 路由。

// 比如:当前路径为: /list/list1navigate('/list_child') // 没有以 .. 开头,from = '/list/list1'
navigate('../list_child') // 存在 .. 开头,就退一级, from = '/list'

确定了 to 和 from ,就调用 resolvePath 函数。

46.png

这里就是过滤掉 / 开头的路径,如果不是以 / 开头,就继续调用 resolvePathname 函数。

该函数的作用就是,to 属性和 matches 拼接成完整的路径。

47.png

48.png

这就是 navigate 对不同字符串形式参数的处理方式。

问题三:react-router-config 的三种写法

还记得是哪三种写法吗?记不清楚的话,回头看看。

49.png

三种写法都是针对二级路由的,所以在解析的时候,如果遇到 / ,直接把一级路由的字符串截取掉,然后在和一级路由拼接,就是出来的 path 属性。

对于写法二,在上面的表格中,就完全体现出来,最后的地址不是我们想要的效果。

问题四:V5 版本的写法,为什么一定要在上一级加上 /* ?

努力过,但是正则劝退了我,放弃了,记住结论吧,哈哈。

50.png

发生在遍历 branchs,去匹配 pathname 的阶段,挑战自己的,可以去尝试一下。

问题五:为什么 ***** 代表着404,找不到路径?

51.png

在这里解释的比较清楚了,就不过多说。

结语

好了,react-router V6 就到此为止了,在上面也存在偏差,希望各位大佬多多指点;

既然看到这里了,也希望各位都有一丢丢的收获。如果对你们有所帮助,点个赞在溜呗。

52.png