学了不用,等于没学;学了长期不用,这 TM 又是什么新技术。
为什么会叫做 react-router v6 的回忆呢?
因为在去年的时候,公司的每两周都会进行一次前端技术分享会。轮到我的时候,我就选择了比较新的 react-router v6 作为主题素材,我前前后后大致花费了半个月多的时间,对 react-router v6 的基本使用、注意事项,到后面的源码分析,最后用 PPT 进行呈现的准备工作。我那是天真的以为我对 react-router v6 的使用已经非常的熟练了,可是现实打败了我。
最近在复习 react 相关的技术栈的时候,我对 react_router v6又陌生了,是不是很讽刺?
所以这篇就是结合我以前准备的 PPT,对 react-router v6 的回忆。
开始回忆
01:V6的基本介绍
02:前端路由基本原理
03:V6 的新特性
<BrowserRouter> 和 <HashRouter>的异同点:
- 创建 historyRef ,调用的函数不同,就走了两套不同的路由模式。
- 都是返回一个
<Router>组件。
该两个函数都是来源于 history 第三库,history 库也分为了两个版本:v4 和 v5 ,react-router v6 就是走的 v5 版本。
这是我在 github 读取 history 库的源码截图。
- 返回的 history 对象
- push 函数的内部实现。
<Router> 组件来自于 react-router 的核心包里面。
源码目录补充:
react-router:是整个路由核心的实现,为
react-router-dom和react-router-native提供支持。react-router-dom:供 web 端(浏览器)使用。
react-router-native:供 react-native 使用。
Router 组件的实现,使用了嵌套的两个 context,给容器里面的内容,提供支持。比如常见的 location 对象,navigator 对象(也就是history 对象)。
BrowserRouter 和 HashRouter 接收的 props。
react-router 新的hooks 和 components。
就是用 <Router> 组件中的第二层 context:LocationContext,返回 location 对象。
跳转路由。
useRoutes() 是 react-router v6 非常重要的一个 hooks,后面的基本上所有的逻辑都围绕着它转。
hooks 完了,就来组件。
这里就是 V5 形式的组件写法,转成 tree 的数据结构。
在 V6 中,可以自己写一个树状结构(类似于 react-router-config ),路由统一管理,直接传递给 useRoutes 直接处理(也是推荐的写法)。
对于上面两个组件,都是处于可用可不用的状态,特别是 <NavLink> 组件。
组件式的跳转路由。
嵌套路由的占位符,相信使用过的,都知道。
哈哈哈,还在这里感悟一下。
04:V6 的案例演示
到了这里,就可以动手实操了(其实本来不想演示的,但是都到了这步,还是写写吧,也回顾一下)。
// App.tsx
import 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.tsx
import 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 的源码分析
跟着问题读源码,分析源码,知道原因后,就会有着一种小小的满足感。
在解答上面的问题时,先来看看 useRoutes 的源码的大致流程,所以的答案基本上就在里面,这也是我为什么说 useRoutes 是非常重要的原因。
useRoutes 内部的实现:
-
第一步:拿到当前的
pathname。 -
第二步:调用
matchRoutes函数,参数为路由配置(tree 的数据结构)和pathname。flattenRoutes:扁平化函数,把 tree 的路由数据结构平铺,在此过程中组装了路由 path 的完整路径、使用 routesMeta 保存了路由层级的数据、计算了路由对应的分数(score)等系列操作,这里可以把平铺好的数据看成一个个分支(branch)。rankRouteBranches:根据平铺好的数据,依据它们的 score 进行排序,从到高到低。- 遍历 branch,依次调用
matchRouteBranch函数,去匹配 pathname,匹配上了,返回对应的路由信息。
-
第三步:执行
_renderMatches函数,接收上面返回的路由信息作为参数,渲染路由组件。
下图是路由平铺的数据展示:
大致对 useRoutes 的源码流程有所了解之后,就来解决上面的五个问题。
问题一:<Outlet> 组件实现的原理
遍历分支,匹配上 pathname 之后,返回路由信息。这里着重关注一下,路由的返回格式:[ 一级路由信息, 二级路由信息]。
_renderMatches 函数,拿到返回的路由信息就开始渲染,那么源码内部的实现呢?
必须想清楚 reduceRight 函数,跟 reduce 函数基本上是一致的,只不过顺序反了而已,累积的一种过程。
这里就是一个节点累积,先返回下级路由的 <RouteContext>,然后在把子组件的 <RouteContext> 继续累积在上级路由的 <RouteContext>,从而:
返回一个嵌套的 <RouteContext> 组成的 ReactElement 元素,在渲染的时候,就是采用的就近原则,进行渲染。
上面大致就是 Outlet 的实现原理,不知道讲清楚没有。
问题二:navigate 函数接收多种参数,内部如何解析?
其实,这里的一些判断还是比较简单的,对 to 属性的类型判断,对跳转方式的判断。
这里比较头痛的,就是这里的 resolveTo 函数,对 to 属性的完全解析。
参数来源。
定义 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);
这里应该还是很好理解的,就是处理 navigate 传递的参数存在 ..,回退匹配信息 matches 路由。
// 比如:当前路径为: /list/list1
navigate('/list_child') // 没有以 .. 开头,from = '/list/list1'
navigate('../list_child') // 存在 .. 开头,就退一级, from = '/list'
确定了 to 和 from ,就调用 resolvePath 函数。
这里就是过滤掉 / 开头的路径,如果不是以 / 开头,就继续调用 resolvePathname 函数。
该函数的作用就是,to 属性和 matches 拼接成完整的路径。
这就是 navigate 对不同字符串形式参数的处理方式。
问题三:react-router-config 的三种写法
还记得是哪三种写法吗?记不清楚的话,回头看看。
三种写法都是针对二级路由的,所以在解析的时候,如果遇到 / ,直接把一级路由的字符串截取掉,然后在和一级路由拼接,就是出来的 path 属性。
对于写法二,在上面的表格中,就完全体现出来,最后的地址不是我们想要的效果。
问题四:V5 版本的写法,为什么一定要在上一级加上 /* ?
努力过,但是正则劝退了我,放弃了,记住结论吧,哈哈。
发生在遍历 branchs,去匹配 pathname 的阶段,挑战自己的,可以去尝试一下。
问题五:为什么 ***** 代表着404,找不到路径?
在这里解释的比较清楚了,就不过多说。
结语
好了,react-router V6 就到此为止了,在上面也存在偏差,希望各位大佬多多指点;
既然看到这里了,也希望各位都有一丢丢的收获。如果对你们有所帮助,点个赞在溜呗。