一小时快速回顾 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
则无需设置path
loader
action
第二个参数是一个配置对象,可能会用到的属性有 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