React Router 是一个基于 React 之上的路由库,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 间的同步。

React Router 是建立在 history 库之上的。 history 监听浏览器地址栏的变化,解析 URL 并转化为 location 对象, 然后 router 使用它匹配到路由,最后正确地渲染对应的组件。

该文章写于 2021-11-29 10:56:40 目前 React-router 最新版本为 6.0.2,React-router 不同版本 API 相差较大,该文章以当前最新版本为标准。

React router 用法

参考 React router 教程

index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from "react";
import { render } from "react-dom";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import App from './App';
import { Expenses, Invoices, Invoice } from "./routes";

render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} >
<Route path="expenses" element={<Expenses />} />
<Route path="invoices" element={<Invoices />}>
<Route
index
element={
<main style={{ padding: "1rem" }}>
<p>Select an invoice</p>
</main>
}
/>
<Route path=":invoiceId" element={<Invoice />} />
</Route>
<Route path="old-expenses" element={
<Navigate to="/expenses" replace />
}>
</Route>
<Route
path="*"
element={
<main style={{ padding: "1rem" }}>
<p>There's nothing here!</p>
</main>
} />
</Route>
</Routes>
</BrowserRouter>,
document.getElementById("root")
);
  1. 根结点 BrowserRouter 表示采用浏览器 path 导航
  2. Routes 是 Route 的父节点,表明从子节点 Route 中选一个匹配,Route 的 path 参数表明对应的路由路径,element 参数表明需要渲染的组件
  3. Route 可以嵌套,当 Route 嵌套时,父组件中的 Outlet 组件表明了子组件渲染的位置
  4. Route 的 index 参数为 true 表示其为当兄弟 Route 的 path 均不匹配时的默认渲染组件
  5. path 参数’*’为通配符,以 ‘/‘开头时为绝对路径,否则为相对路径
  6. Navigate 表示跳转到对应路径,replace 参数代表是否替换当前 history,所以
    1
    2
    3
    <Route path="old-expenses" element={
    <Navigate to="/expenses" replace />
    }>
    其实实现了重定向的功能
App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Outlet, Link } from "react-router-dom";
export default function App() {
return (
<div>
<h1>Bookkeeper!</h1>
<nav
style={{
borderBottom: "solid 1px",
paddingBottom: "1rem"
}}
>
<Link to="/invoices">Invoices</Link> |{" "}
<Link to="/expenses">Expenses</Link>
</nav>
<Outlet/>
</div>
);
}
  1. Link 表示点击跳转(渲染为a元素),to 参数表示跳转的路径
  2. Outlet 标志了子组件的渲染位置
routes/invoices.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import { NavLink, Outlet, useSearchParams } from "react-router-dom";
import { getInvoices } from "../data";

import { useLocation } from "react-router-dom";

// 如果想要跳转时保留searchparams 需要使用QueryNavLink,否则使用Navlink
function QueryNavLink({ to, ...props }) {
let location = useLocation();
return <NavLink to={to + location.search} {...props} />;
}

export default function Invoices() {
let invoices = getInvoices();
let [searchParams, setSearchParams] = useSearchParams();
return (
<div style={{ display: "flex" }}>
<nav
style={{
borderRight: "solid 1px",
padding: "1rem"
}}
>
<input
value={searchParams.get("filter") || ""}
onChange={event => {
let filter = event.target.value;
if (filter) {
setSearchParams({ filter });
} else {
setSearchParams({});
}
}}
/>
{invoices
.filter(invoice => {
let filter = searchParams.get("filter");
if (!filter) return true;
let name = invoice.name.toLowerCase();
return name.startsWith(filter.toLowerCase());
}).map(invoice => (
<QueryNavLink
style={({ isActive }) => {
return {
display: "block",
margin: "1rem 0",
color: isActive ? "red" : ""
};
}}
to={`/invoices/${invoice.number}`}
key={invoice.number}
>
{invoice.name}
</QueryNavLink>
))}
</nav>
<Outlet />
</div>
);
}
  1. 通过 useSearchParams 钩子可以获取到搜索参数
  2. NavLink 同 Link 类似,也会渲染成 a 元素,但是 style 属性和 className 属性中提供了 isActive 参数来识别是否和当前页面一致
  3. NavLink 比较跳转链接和当前页面地址是否一致时只比较页面路径,如果还需比较其他参数,可以通过 useLocation 钩子获取 location 相关的参数自行比较
routes/invoice.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { useParams, useNavigate } from "react-router-dom";
import { getInvoice, deleteInvoice } from "../data";

export default function Invoice() {
let navigate = useNavigate();
let params = useParams();
let invoice = getInvoice(parseInt(params.invoiceId, 10));

return (
<main style={{ padding: "1rem" }}>
<h2>Total Due: {invoice.amount}</h2>
<p>
{invoice.name}: {invoice.number}
</p>
<p>Due Date: {invoice.due}</p>
<p>
<button
onClick={() => {
deleteInvoice(invoice.number);
navigate("/invoices");
}}
>
Delete
</button>
</p>
</main>
);
}
  1. 通过 useNavigate 钩子可以实现程序控制的跳转/重定向(添加第二个参数 { replace: true } )
  2. 通过 useParams 钩子可以获取页面 path 参数,该参数为字符串类型

常用API

参考 React router API

Packages 包

React router 包含了三个 npm 库 react-router、react-router-dom 和 react-router-native。

  • react-router 库包含了最核心的组件和钩子

  • react-router-dom 库包含了 react-router 库和一些 DOM 操作相关的API 例如 <BroserRouter> <HashRouter> <Link>

  • react-router-native 库包含了 react-router 库和一些和 RN 相关的API 例如 <NativeRouter> 和 本地版本的 <Link>

Setup 开始

你必须在组件树的根结点使用一个 router 组件才能在应用中使用 React router。根据应用类型的不同,React router 提供了几种不同的 router 。

  • <BrowserRouter><HashRouter> 应当在 web 浏览器环境中使用,其中 <BrowserRouter> 采用浏览器 path 导航,体验更好,而 <HashRouter> 采用 hash 导航,兼容性更好。
  • <StaticRouter> 在服务器渲染的时候使用。
  • <NativeRouter> 在 React Native 应用中使用。
  • <MemoryRouter> 在测试场景中使用。

Routing 路由

路由决定了当你给定应用的页面时,哪一个React组件应当被渲染以及如何嵌套。React router 提供了两种接口描述路由。

  • 使用JSX的话可以用 <Routes><Route>
  • 使用Js配置的话,可以用 useRoutes

React router 的导航接口允许通过修改当前 location 来改变当前渲染的页面。

  • <Link><NavLink> 生成一个 <a> 元素,当用户点击时进行相应的导航。<NavLink>styleclassName 属性提供了 isActive 参数来判断 Link 是否是当前页面地址
  • useNavigate<Navigate> 允许程序性的导航,尤其是在事件处理器中或者状态变化时

Hooks 钩子

  • useSearchParams 钩子可以获取到 URL 的搜索参数
  • useLocation 钩子可以获取到当前的 history.location 参数
  • useParams 钩子可以获取到 path 传递的参数
  • useRoutes 钩子是 <Routes> 的函数形式
  • useNavigate 钩子返回 navigate 函数,允许程序性的导航

History

React router 底层使用到了 history 库。 history 库抽象表示了浏览历史的变化,它监听浏览器地址栏(页面路由)的变化,并解析 URL 将其转化为 locaton 对象。基于环境和实现方式的不同,history 库提供了三种形式的 history:

  • browserHistory: 通过 HTML5 提供的 history API 实现,需要配置服务器路由将请求重定向到html文件,对应 React router 的 BrowserRouter

例如在 nodejs 中需要配置

1
2
3
app.get('*', function (request, response){
response.sendFile(path.resolve(__dirname, 'public', 'index.html'))
})

如果使用的 nginx 服务器,则需要配置

1
2
3
4
5
6
server {
...
location / {
try_files $uri /index.html
}
}

如果使用的 Apache 服务器,则需要配置

1
2
3
4
5
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
  • hashHistory: 通过 hash(#) 来实现,不支持 history API 的老旧浏览器也可使用,对应 React router 的 HashRouter
  • memoryHisotry: 路由存储在内存里,url不会变化,因此不能通过url分享,通常在 node , React Native 环境下使用,对应 React router 的 MemoryRouter

React router 和 Redux 集成

React router 可以和状态管理库 Redux 集成,以便将路由信息同步到 Redux state 统一管理,并且路由组件可以从 Redux 获取路由信息,实现导航等功能。

集成好处:

  1. 路由信息可以同步到统一的 store 并可以从中获得
  2. 可以使用 Redux 的 dispatch action 来导航
  3. 集成 Redux 可以支持在 Redux devtools 中路由改变的时间履行调试

方法:用 react-redux 的<Provider store={store}> 包住 react-router 的路由组件<Router><BrowserRouter>

1
2
3
4
5
ReactDOM.render(
(<Provider store={store}>
<APPRouter/>
</Provider>)
, document.getElementById('root'));

代码分割

React router 可以结合 webpack 和 @babel/plugin-syntax-dynamic-import、loadable-components 来实现代码分割。

一个 React router + webpack + @babel/plugin-syntax-dynamic-import + loadable-components 实现代码分割的简单示例:

.babelrc 配置文件:

1
2
3
4
{
"prestes": ["@babel/presets-react"],
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}

@babel/plugin-syntax-dynamic-import 插件避免 babel 对动态 import 语法做过多的转化,允许 webpack 打包时将动态 import 的代码分离成单独的 bundle,实现代码分割。

使用 loadable-component 和动态 import 懒下载组件:

1
2
3
4
5
6
7
8
9
10
11
12
import loadable from "@loadable/component";
import Loading from "./Loading.js";

const LoadableComponent = loadable(() => import("./Dashboard.js"), {
fallback: <Loading />
});

export default class LoadableDashboard extends React.Component {
render() {
return <LoadableComponent />;
}
}

react-router & react-router-dom 关系

react-router 只是一个核心库,在具体使用时应该基于不同的平台要使用不同的绑定库。比如:我们要在浏览器中使用 React router,就安装 react-router-dom 库,如果在 React Native 中使用 React router 就应该安装 react-router-native 库。但是我们不需要直接安装 react-router 库。