一小时快速回顾 React Router
React 回顾

React 官网是这样描述 React 的:
The library for web and native user interfaces
用于构建 Web 和 原生 交互界面的库
意思就是 React 是用来构建和操作视图的。
其具有以下三个最主要的特点:
- 声明式:声明式编程区别于命令式编程,对于命令式编程来讲,我们要操作视图上的一系列
DOM元素,需要我们进行包括创建容器、获取DOM、更新数据等一系列繁琐的事情来告诉机器要怎么做。而声明式编程,我们只需要通过setState来修改状态,React就能完成对应视图的更新,只需要告诉机器我们需要什么样的结果,而不需要关心内部DOM究竟是如何被修改的。 - 组件化:
React的图标是物理化学中的原子的概念,通过React我们可以将UI拆分成多个独立单元,这些独立单元就是组件,也就是“原子”。而React中编写组件的方式是JSX,也就是在JavaScript中编写HTML,可以将结构和交互逻辑组合在一起,通过 原子化CSS、CSS in JS等方案还可以将结构、样式、交互逻辑组合在一起,也就真正做到一个JSX文件对应一个组件。 - 跨平台编写:
React是通过先构建虚拟DOM再来操作真实DOM的。虚拟DOM的本质就是JavaScript对象,我们只需要不同的渲染器就可以实现一次编写,跨平台使用。
但以上仅仅只是 React 在构建和操作视图方面的功能,只使用这些特性是很难开发大型应用的,对此我们需要格外学习 React 生态的其他工具帮助我们开发,比如路由工具、状态管理工具等。
今天我们要分享的就是对 React Router 的一些理解和应用。

本篇文章主要是针对 React Router v6.10 的相关介绍和分析
为什么不是 React Router v5?

虽然 npm 的官网显示 v5.3.4 的周下载量甚至还要高于 v6.10.0,但是 Remix 团队在 v6.4 后的版本中增添了许多新特性,其中包括读取写入和路由相关的数据等方案。此外,新版本的写法也有很大的改变,这些改变肯定会延续到后续版本的变更中。尽管 v6.4 后的版本包含更多的特性和改进,但它们已经迭代数次变得相当成熟,因此从学习的层面来看,我们应该直接开始学习 v6 版本。
同时通过 Bundlephobia 可见: v6.3 之前的版本相比 v5 版本包大小要小得多,但是 v6.4 版本之后引入了读取写入和路由相关的数据等方案,才加大了包的体积。

React Router 的基本使用
参考官方文档的 快速开始。可以帮助你快速掌握 React Router 的基本使用(但个人感觉官方给的示例对于第一次上手有点难度)。
🚗 功能概述
首先我们思考一下为什么需要前端路由库?
🖥️ 客户端路由(Client Side Routing)
传统多页面
浏览器从服务端请求网页,会下载 CSS、JavaScript 资源,并渲染从服务端发送过来的 HTML。当我们点击链接切换路由时,会整页刷新,这些请求都会重新执行。

这就导致了页面切换加载缓慢,切换断层感强,流畅度不够,用户体验较差。
客户端路由
单页面应用只需初始加载一次,获取所需的所有资源,当我们跳转切换页面时,页面局部刷新或更改,不会再向服务器发送请求。
其优点主要有两方面:
- 页面切换快,几乎无感,用户体验较好。
- 数据传递层面来讲,单页面应用较为容易,直接在页面组件之间进行数据传输,而多页面应用则依赖
url传参,或者Cookie、localStorage等本地存储方案。
但也有相应的缺点:
不利于 SEO(搜索引擎优化),客户端路由是根据匹配到的路由通过 JavaScript 动态渲染上去的,初次返回的 HTML 基本是个空壳,因此不利于搜索引擎爬虫爬取。对于这种情况可以使用 SSR 等方案来优化。对于多页面应用来讲就不存在这类问题。

了解了为什么需要前端路由,我们再考虑一下一个前端路由库应该具备哪些功能:
URL路由匹配:实现能根据URL路径匹配对应的页面或组件,且路由为客户端路由,切换页面时不向服务器发送请求,不重新刷新整个页面。- 路由跳转与传参:怎么在各个路由之间进行跳转,跳转的时候如何传递、获取参数。
- 与路由相关的数据处理:在路由跳转的时候,实现相关数据处理,实现不跳转页面的表单提交、组件渲染之前并行加载内部所有嵌套组件的数据、实现加载路由过程中的页面等操作。
👶 创建路由
在开始之前我们需要先引人 react-router-dom 包。
react-router 是核心包,react-router-dom 是在 react-router 的基础上的浏览器端路由库,react-router-native 是在 react-router 的基础上的 React Native 端路由库。
pnpm install react-router-dom
我们要使用客户端路由首先需要创建路由实例。
创建路由实例
主流的创建路由实例的方法有两种,分别是 createBrowserRouter 和 createHashRouter,其他创建路由实例的方法可以参考官方文档。
createBrowserRouter
推荐创建的路由,也是项目中 99% 的情况下使用的路由模式。
createBrowserRouter 是一个工厂函数,它用于创建一个 Browser Router 对象。
其基本用法如下:
// 创建 History 路由实例
const router = createBrowserRouter([
// 数组的第一个参数是一个对象数组,里面存的是 Route 对象
{
// 路径
path: "/home",
// 组件
element: <Home />,
// 子路由
children: [
{
index: true,
element: <Class />,
},
{
path: "team",
element: <Team />,
},
],
},
{
// 路径
path: "/about",
// 组件
element: <About />,
errorElement: <Error />,
},
], {
// 根路径
basename: '/app'
});
// 使用 JSX 的形式
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<Root />}>
<Route path="team" element={<Team />} />
</Route>
)
);
第一个参数是一个对象数组,里面存的是 Route 对象,包含属性:
path:与url匹配的路径element:匹配上路径时渲染的组件children:嵌套子路由,值也是Route对象数组errorElement:抛出异常时渲染的组件index:是否是默认子路由,如果为true则无需设置pathloaderaction
第二个参数是一个配置对象,可能会用到的属性有 basename,用来设置客户端路由的根路由,适用于不能部署到域的根目录,只能部署到子目录的情况。
除了上面直接传入配置项的方式,还支持通过 createRoutesFromElements 以 JSX 的方式创建路由实例。
其原理是使用 DOM History API 来更新 url、管理历史堆栈。history api 可以修改、监听浏览器的 URL,改变的 URL 会发送给服务端,这就导致如果我们进入了某个子路由,然后在该子路由页面点击刷新按钮,URL 就会被发送到服务端,但是单页面应用也就是客户端路由在服务端只有一个 index.html 在根目录下, 因此会报找不到该资源的错,所以 History 模式下需要服务端配置:当找不到匹配路由资源的时候,将所有路由情况都返回 index.html,由客户端路由来判断到底该 URL 要渲染哪些组件。
createHashRouter
不推荐使用哈希路由,除非你不能配置你的服务器。
Hash 模式更改 url 后面的哈希部分,这部分对服务器不可见,意思是再怎么折腾请求服务端那边都是根目录下的 index.html,前端通 过 local.hash 来改变和获取哈希值,当更改哈希值时,浏览器不会重新加载页面,hashchange 事件监听变化,最后实现无刷新的页面跳转。
其余部分与 History 路由基本一致。该路由模式是在 window.pushState 等 history API 标准化之前的解决方案。
使用路由实例
使用路由实例我们通过 <RouterProvider /> 组件来实现,我们将创建的路由实例作为 router 参数传递给 <RouteProvider /> 组件,将其渲染。
// react 将 <RouterProvider /> 渲染在根组件上
ReactDOM.createRoot(document.getElementById("root")).render(
<RouterProvider
router={router}
/>
);
特性
嵌套路由
一个路由下可以设置多个子路由,通过 Outlet 来展示子路由的组件。
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/*
Outlet 组件将会渲染 DashboardMessages 如果路由匹配上 /messages,
会渲染 DashboardTasks 如果路由匹配上 /tasks
*/}
<Outlet />
</div>
);
}
function App() {
const router = createBrowserRouter([
{
path: "/",
element: <Dashboard />,
children: [
{
path: "messages",
element: <DashboardMessages />,
},
{
path: "tasks",
element: <DashboardTasks />,
},
],
},
]);
return (
<RouterProvider
router={router}
/>
);
}
动态参数
URL 的片段是可以被解析并提供给各种 api 的。
path="projects/:projectId/tasks/:taskId"
如上面两个前面带 : 的路由片段是动态的,在该路由组件下可以通过 useParams 可获取到 projects 和 taskId 两个参数:
// 如果当前的路由是 /projects/abc/tasks/3
createBrowserRouter([
{
path: "projects/:projectId/tasks/:taskId",
element: <Task />,
},
]);
// 组件内获取
function Task() {
// useParams 返回对应的参数
const params = useParams();
params.projectId; // abc
params.taskId; // 3
}
优先匹配
对于下面两种路由都会匹配上的情况,如 teamId 为 new:
<Route path="/teams/:teamId" />
<Route path="/teams/new" />
React Router 会选择第二个(/teams/new),也就是更具体的那一个。
错误路由
可以设置当抛出错误时的路由渲染页面。
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
// 抛出错误时渲染的组件
errorElement: <ErrorPage />,
children: [
{
path: "team",
element: <Team />,
},
],
},
]);
errorElement 具有冒泡特性,如果子路由抛出错误,首先会检查当下有无 errorElement,没有则会向上查找,直到根路由。
默认路由
如果在当前路由下没有匹配到对应的路由,我们往往需要设置一个类似 404 的页面,来告知用户该路由不存在,react-router 本身提供了一个默认 404 页面,但是如果我们想要自定义该怎么办?
我们可以在设置路由的 path="*" 来设置当匹配不到路由的时候展示的页面。
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from "./Home";
import About from "./About";
function App() {
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/about",
element: <About />,
},
{
// 设置默认路由
path: "*",
element: <div>not found</div>,
},
]);
return <RouterProvider router={router} />;
}
export default App;
当我们进入不存在的路由时,就会默认展示 path=* 提供的组件:

🚀 路由间跳转
声明式路由和编程式路由是 React Router 中两种不同的路由跳转方式。
声明式跳转
概念
通过使用 JSX 元素,如 <Link />、<NavLink /> 等,设置 to 属性,点击该元素来进行跳转的方式就为声明式跳转。
示例
<Link />:允许用户点击跳转到另一个路由。
-
to:要跳转到的路径,可以通过../的方式跳转到上层组件/路径 -
relative:如果通过../的方式跳转上层,默认情况下是跳转到上层组件,即relative的值默认为route,如果要跳转到上层路径,则需设置为path<Route path="/" element={<Layout />}> <Route path="contacts/:id" element={<Contact />} /> <Route path="contacts/:id/edit" element={<EditContact />} /> </Route>; function EditContact() { return ( // 如果为 path 则是跳转到 contacts/:id,如果为 route 则是跳转到 / <Link to=".." relative="path"> Cancel </Link> ); } -
preventScrollReset:决定是否在跳转时将滚动位置重置为窗口顶部 -
replace:是否直接替换历史堆栈中的当前路由 -
state:通过该属性可以存储状态,并传递给跳转后的路由,该状态在url中不会体现,需要用useLocation获取 -
reloadDocument:整页刷新
<NavLink />:是一种特殊的 <Link />,用来判断当前路由是“活跃的”还是“待定的”。
-
默认
active类:会自动为“活跃的路由”设置上active类,以便直接用css对其进行样式修改// jsx <nav id="sidebar"> <NavLink to="/messages" /> </nav> // css #sidebar a.active { color: red; } -
将是否活跃等参数传入
className、style、children等,以便我们判断进行逻辑处理<NavLink to="/messages" className={({ isActive, isPending }) => isPending ? "pending" : isActive ? "active" : "" } style={({ isActive, isPending }) => { return { fontWeight: isActive ? "bold" : "", color: isPending ? "red" : "black", }; }} > {({ isActive, isPending }) => ( <span className={isActive ? "active" : ""}>Tasks</span> )} </NavLink>; -
可以设置
end属性,来决定是否完全匹配完全匹配只的是当只有
URL完全一样的时候才会标识为活跃状态,如果有超出的路由片段则为待定状态:
| NavLink | URL | isActive |
|---|---|---|
<NavLink to="/tasks" /> | /tasks | true |
<NavLink to="/tasks" /> | /tasks/123 | true |
<NavLink to="/tasks" end /> | /tasks | true |
<NavLink to="/tasks" end /> | /tasks/123 | false |
编程式跳转(命令式跳转)
概念
通过命令代码来进行路由的跳转,可以将跳转命令绑定到事件中。
useNavigate
该 hooks 返回一个函数实例,以便编程式跳转。
const navigate = useNavigate();
navigate("/session");
返回函数有以下参数:
-
to:要跳转到的路径或在历史堆栈中想要跳转的增量,如 -1navigate(-1); -
options:包含replace和state
对比
声明式路由更加简单直观,可以适用大多数场景,而编程式路由则更加灵活,能满足一些特定的需求。
📮 路由间传参
useLocation
let location = useLocation();
该 hooks 可以获取 location 对象,包含以下属性:

-
pathname:url的路径部分 -
search:url的?参数片段 -
hash:url的#及后面的哈希片段 -
state:用来获取跳转前传入的state,该state不会出现在url当中,对于一些不想明面表现在url中的参数可以使用state进行传参。
-
key:与路径关联的唯一字符串
useParams
该 hooks 返回动态参数键值对的对象。

useSearchParams
该 hooks 返回一个包含两个元素的数组,第一个元素是一个 URLSearchParams 对象,包含该 url ? 后面跟着的参数,第二个元素是一个函数,用来设置 url ? 后面的参数,酷似 useState 的用法。

✏️ 手写简易版 BrowserRouter
该实现方法和 v6 的语法不相同,重在体验 React Router 内部的原理。
首先我们需要有两个组件:<BrowserRouter> 和 <Route>。

其中 <BrowserRouter> 用来包裹整个路由,内部的 <Route> 表示路由组件,含有 path 和 element 属性,分别为匹配的路径和匹配上后渲染的组件。
BorwserRouter 组件
<BrowserRouter> 组件主要完成的功能有如下:
- 保存当前路径状态,并传递给它包裹的所有子组件(即
<Route>),初始值为浏览器地址栏中的路径。 - 提供更新路径的方法,并传递给它包裹的所有子组件,用来内部更新路径状态。
- 监听路径的变化,并因为外部方式(浏览器的回退和前进)使路径变化时实时更新路径状态。
保存当前路径状态
保存当前路径状态,初始值为浏览器地址栏中的路径,提供 setPath,用于给更新路径的方法提供更新手段。

更新路径的方法

通过 history.pushState 更新路径,同时更新 path 状态。
history.pushState 方法接收三个参数,第一个参数是 state 对象,传入的 state 对象可以在对应路由下通过 window.history.state 获取;第二个参数是 title,但现在大多数浏览器都忽略次参数,可以传入空字符串;第三个参数是 URL。
监听浏览器前进后退并更新路由
通过监听 popstate 事件,更新 path 状态。
调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。
popstate 事件只会在浏览器某些行为下触发,比如点击后退按钮。

监听到浏览器路由因为浏览器行为发生变化后,获取变化后的路径,更新路径状态。
传递给子组件
通过 createContext 创建上下文,来实现跨组件通信:

这里创建了两个上下文,一个用来存储更新路径状态的方法,一个用来存储当前路径状态。通过 Provider 传递给子组件。

Route 组件
在 <Route> 中获取到配置路由中传入的 path 和 element 表示这个路由组件要渲染的路径和对应组件。

然后通过 useContext 获取当前的路径状态,与各个 <Route> 组件的 path 进行对比,如果相匹配,则将其渲染。

Route 的 element 组件
在路由渲染组件中,通过 useContext 获取到 HistoryContext 的 push 方法用来更新路径,并可以传入 state。

若传入了 state,可以在相应路由组件中使用 window.history.state 获取传入的参数。
📗 思考
- 实现一个简易版
react-router-dom的基础功能,并尽可能增加特性,如错误路由、默认路由、嵌套路由等。 - 实现路由守卫,如果处于未登录状态进入页面,则跳转到登陆页,并对各个页面进行权限管理,相应角色权限只能进入相应的页面。
转载自:https://juejin.cn/post/7245975053232881724