本章将讨论 React 的路由:通过路由,你可以制作出“多页面”的系统。
React 是典型的SPA(单页面应用),这里所说的“多页面”是指逻辑上的,或者说是从访客的感观上的。
react-router
应该是 React 生态中最受欢迎的路由组件了,在开始使用它之前,需要先把它安装到当前项目里:
yarn add react-router-dom
本教程使用的是 react-router 6 ,你从其它途径搜索或学习到的大概率会是旧版的 react-router 5。两个版本差异非常大,请注意甄别。
常用的路由组件
组件/hook | 说明 |
---|---|
BrowserRouter | 使用直观的 URL 将当前 location 存储在浏览器的地址栏中,并使用浏览器的内置历史堆栈进行导航。 |
HashRouter | 将当前localtion 存储在当前 URL 的Hash部分中(# ),因此它永远不会发送到服务器。 |
Routes | 匹配当前 location 的一组子路由 |
Route | 路由系统最重要的部分。将 URL 段耦合到组件中 |
Link | 允许用户通过单击或点击它导航到另一个页面。类似于 HTML 中的 <a> 标签 |
NavLink | Link 的特例,会自动加上 active 类,以便高亮显示与当前 location 匹配的链接。主要用于导航栏、面包屑等场景。 |
Navigate | 改变当前 location ,主要用于跳转。 |
Outlet | 用于嵌套路由中渲染子路由对应的组件。 |
useNavigate | 以编程方式使用 Navigate 组件,即以编程方式实现跳转 |
useParams | 以编程方式获取动态路由中的参数,比如 /user/:userid 中的 userid 参数 |
useSearchParams | 以编程方式获取路由中的 URL 参数,比如 /users?page=1&sort=desc 中的 page 和sort 参数 |
useRoutes | 它的功能和Routes 组件一样。但它使用 JavaScript 的简单对象来代替 Route 元素的定义。 |
BrowserRouter 还是 HashRouter
首先我想用最通俗的方式介绍 BrowserRouter
和 HashRouter
。假设 location
是 /user/profile
,那么:
- BrowserRouter长成这样:
https://axum.rs/user/profile
- HashRouter长成这样:
https://axum.rs/#/user/profile
BrowserRouter | HashRouter | |
---|---|---|
直观性 | 高 | 一般 |
SEO | 有利 | 基本不支持 |
浏览器支持度 | 主流浏览器都支持 | 基本上所有浏览器都支持 |
部署 | 需要 Web 服务器做一些简单设置 | 基本上所有环境都能部署,包括 Github Page、Cloudflare Page 等 |
服务端是否能接收到URL | 能 | 不能。它永远不会发送到服务端 |
本教程将使用 BrowserRouter,原因如下:
- 哪怕是最受诟病的 Windows 平台也很少人用老掉牙的 IE 浏览器,而是使用 Chrome(及其衍生品)等主流浏览器
- 由于后面章节会讲到SEO和服务端渲染,HashRouter 根本做不到
- 虽然说部署时需要做一些设置,但这不是问题。使用 Nginx 部署,只要加一条指令即可。
基于以下原因,你可能不得不使用 HashRouter:
- 面向的访客主要以老旧的操作系统为主,他们无法使用现代的、主流的浏览器(题外话:如果真是要面向 WIN XP 这种客户,还是安心用你的
<table>
布局吧,本专题你都可以不用学了,本专题的 React、NextJS 和 TailwindCSS 都需要现代的主流浏览器) - 不关心SEO和服务端渲染
- 需要部署到 Github Page 或 Cloudflare Page 等平台
react-router 要求每个路由组件都要包裹在一个 Router
(抽象类,BrowserRouter
和 HashRouter
是它的具体实现。注意,不要和后面的 Route
组件混淆,一个字母的不同,造就了两个完全不一样的组件) 中,所以我们在入口文件中,加上 BrowserRouter
:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; // 引入 BrowserRouter
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
{/* 使用 BrowserRouter 包裹主组件 */}
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
案例
下面开始以案例驱动学习的方式,开始对 react-roter 进行学习。
“页面”组件 | 路由 | 说明 |
---|---|---|
Home | / | 首页 |
News | /news | 新闻列表 |
NewsDetail | /news-detail/:id | 新闻详情 |
About | /about | 关于我们 |
在“多页面”应用中,我们通常把“页面”放在 pages
目录下(还记得之前章节说过,组件放在 components
目录下吗),所以一个典型的 react 应用的目录结构应该是:
- src
- components
- Foo.jsx
- Bar
- index.jsx
- Foobar.jsx
- pages
- Page1.jsx
- PageN
- index.jsx
- SubPage.jsx
- App.jsx
开始动手之前,别忘了将
BrowserRouter
添加到入口文件,并包裹整个App
主组件。往上翻你就能看到代码
创建页面组件
在 src
目录下创建 pages
目录,然后分别在其中创建页面组件:
首页: src/pages/Home.jsx
import React from 'react';
export default function Home() {
return <div>主页</div>;
}
新闻列表: src/pages/News/index.jsx
import React from 'react';
export default function News() {
return <div>新闻列表</div>;
}
新闻详情: src/pages/News/Detail.jsx
import React from 'react';
export default function NewsDetail() {
return <div>新闻详情</div>;
}
关于我们: src/pages/Home.jsx
import React from 'react';
export default function About() {
return <div>关于我们</div>;
}
创建导航栏
为了演示 NavLink
和 Link
的区别,本案例我们将分别用这两个组件创建两个不同版本的导航栏组件。在实际开发中完全没有这个必要——直接使用 NavLink
就好了。在后续案例中,我们也只会使用 NavLink
来创建导航栏组件。
使用 Link
的 NavBarUsingLink
// src/compontents/NavBar/UsingLink.jsx
import React from 'react';
import { Link } from 'react-router-dom';
export default function NavBarUsingLink() {
return (
<nav>
<Link to="/">首页</Link>
<Link to="/news">新闻</Link>
<Link to="/about">关于我们</Link>
</nav>
);
}
使用 NavLink
的 NavBarUsingNavLink
// src/compontents/NavBar/UsingNavLink.jsx
import React from 'react';
import { NavLink } from 'react-router-dom';
export default function NavBarUsingLink() {
return (
<nav>
<NavLink to="/">首页</NavLink>
<NavLink to="/news">新闻</NavLink>
<NavLink to="/about">关于我们</NavLink>
</nav>
);
}
可以看到,Link
和 NavLink
的 API 基本一样, 这是自然的,NavLink
只是 Link
的一种特例,也就是一个子类,它们拥有相同的某些属性,比如:
to
:指定要跳转的目标地址。这和 HTML 的<a href="">
非常类似
简单 CSS
nav {
display: flex;
gap: 0.25rem;
}
.active {
color: red;
}
在主组件上添加导航栏
现在,我们把定义好的导航栏添加到 App
组件上。先用 Link
实现的试试:
import React from 'react';
import NavBarUsingLink from './components/NavBar/UsingLink';
function App() {
return (
<div>
<NavBarUsingLink />
<h1>你好,axum中文网 - 来自 react 的问候</h1>
</div>
);
}
export default App;
很好,导航栏出现了,但也只是出现了而已。让我们换成用 NavLink
实现的试试:
哦吼,奇迹出现了,导航栏中的“首页”竟然变成了红色。
-
这个红色怎么来的?此刻麻烦你把
index.css
中的.active
的颜色改一下,比如:.active { color: green; }
你会发现,“首页”的颜色跟着你的CSS改动而发生变化,由此可知,此时“首页”的样式应该是受
.active
这个 CSS 类控制的。 -
NavLink
是一种特殊的Link
,它会自动将那个和当前URL匹配的项,应用.active
样式 -
如果使用原始的
Link
,要实现这种效果,就要写一大堆代码了
继续点击导航栏中其它链接,它们也都会出现高亮,但页面内容却没变,我们将在下一小节解决这个问题。
本小节代码:axum-rs-react-router-1-navbar
让页面动起来:路由匹配
本小节我们将让页面动起来:点击导航栏不同的链接,页面显示对应的内容。我们只需要在 App
组件中需要显示页面内容的区域加上路由匹配就行了。就是说,只要把上例中的 <h1>你好,axum中文网 - 来自 react 的问候</h1>
这部分内容替换为对应页面的内容即可。
import React from 'react';
// import NavBarUsingLink from './components/NavBar/UsingLink';
import NavBarUsingNavLink from './components/NavBar/UsingNavLink';
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import News from './pages/News';
import About from './pages/About';
import NewsDetail from './pages/News/Detail';
function App() {
return (
<div>
<NavBarUsingNavLink />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/news" element={<News />} />
<Route path="/news-detail/:id" element={<NewsDetail />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
);
}
export default App;
我们重点关注这段代码:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/news" element={<News />} />
<Route path="/news-detail/:id" element={<NewsDetail />} />
<Route path="/about" element={<About />} />
</Routes>
<Routes>
:这里就是定义路由匹配了,它只是提供了一种“容器”,告诉 React,这个部分要匹配路由啦。具体的匹配规则由它那些<Route>
子组件定义。<Route>
:定义具体的路由匹配规则:path
:指明要匹配的路由。你可以指定多种路由:- 静态路由:像
/
、/news
、/about
这种,直接以字面量的形式指定的称为静态路由 - 动态路由:像
/news-detail/:id
这种的称为动态路由,因为它包含一个参数:id
- 静态路由:像
element
:指定路由对应的组件。第一眼看上去,你对它的写法可能有点迷惑,让我们以<Route path="/" element={<Home />} />
为例进行说明。element={}
是什么意思?- 首先,
element
是一个props
- 其次,因为用了
{}
,所以它需要传递一个 JS 表达式
- 首先,
<Home />
是什么?- 它是 JSX
- JSX 是什么?它是
React.createElement()
的语法糖 React.createElement()
是什么?它是一个函数(你也可以称其为方法)。我们在讲组件通讯时说过,JS 里万物皆对象,React.createElement()
自然可以作为表达式传递给element
这个props
很好,目前为止至少让页面动起来了。
你会注意到,”新闻详情“并没有在导航栏上,这是因为要进到”新闻列表“里才会出现,因为它的URL是由”新闻列表“在渲染时,根据ID动态生成的。
下一小节我们将实现”新闻列表“和”新闻详情“
本小节代码:axum-rs-react-router-1-match-routes
实现“新闻列表”和“新闻详情”
首先,我们实现“新闻列表”。我们使用模拟数据来做一个列表:
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
export default function News() {
const [newsList, setNewsList] = useState([]); // 第5行
useEffect(() => { // 第7行
// 模拟从后端获取数据
setNewsList([
{ id: 1, title: '新闻1' },
{ id: 2, title: '新闻2' },
{ id: 3, title: '新闻3' },
{ id: 4, title: '新闻4' },
{ id: 5, title: '新闻5' },
{ id: 6, title: '新闻6' },
{ id: 7, title: '新闻7' },
{ id: 8, title: '新闻8' },
{ id: 9, title: '新闻9' },
{ id: 10, title: '新闻10' },
]);
}, []); // 第21 行
return (
<div>
<ul>
{newsList.map((news) => (
<li key={news.id}>
<Link to={`/news-detail/${news.id}`}>{news.title}</Link>
</li>
))}
</ul>
</div>
);
}
-
第5行:创建了用于维护新闻列表的状态及其设置函数
-
第7~21行:创建了模拟数据
-
第25~29行:遍历这个模拟数据
{newsList.map((news) => ( <li key={news.id}> <Link to={`/news-detail/${news.id}`}>{news.title}</Link> </li> ))}
<Link>
的to
属性:使用 JS 的字符串模板功能,将当前新闻的ID拼接成一个完整的 URL
那么,“新闻详情”如何能接收到传递过来的 :id
参数呢?答案是使用 usePramas
hook:
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
export default function NewsDetail() {
const { id } = useParams(); // 第4行
const [detail, setDetail] = useState('');
useEffect(() => {
// 模拟从后端获取数据
setDetail(`ID为${id}的新闻详情`);
}, []);
return <div>{detail}</div>;
}
第4行:通过 useParams
hook 获取到传递过来的 :id
参数:
-
useParams
会将动态路由里定义的所有参数打包成一个 JS 普通对象,类似这样:{ id: 1, foo: 'bar', }
-
通过解构,可以很方便的获取到其中某个参数:
const {id} = useParams()
,如果不使用解构操作,你可能需要这样:const ps = useParams(); const id = ps.id;
-
useParams
在将动态路由的参数打包时,会将参数名作为key,所以你要使用和参数同名变量进行解构。比如:/news-detail/:newsid
这样的定义,你就要const { newsid } = useParams()
才能得到正确的结果
看上去很OK了,但这里有个严重的问题:“新闻详情”应该是和“新闻列表”一起组成了一个完整的“新闻”,它们应该使用相同的 URL 前缀。并且,在进入“新闻详情”时,导航栏中的“新闻”应该保持actvie
状态,但目前并没有。下一节我们将通过嵌套路由解决这个问题。
本小节代码:axum-rs-react-router-1-news
使用嵌套路由实现完整的“新闻”
我们只需要把 App
中对应的路由匹配部分进行修改就可以了:
//...
function App() {
return (
<div>
<NavBarUsingNavLink />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/news">
<Route path="" element={<News />} />
<Route path="detail/:id" element={<NewsDetail />} />
</Route>
<Route path="/about" element={<About />} />
</Routes>
</div>
);
}
// ...
重点来看:
<Route path="/news">
<Route path="" element={<News />} />
<Route path="detail/:id" element={<NewsDetail />} />
</Route>
<Route path="/news">
,声明一个匹配/news
的规则,作为前缀,它不需要指定element
- 然后在它里面声明了两个子路由
<Route path="" element={<News />} />
,这里的path
为空字符串,所以它实际匹配的就是/news
<Route path="detail/:id" element={<NewsDetail />} />
,它实际匹配的是/news/detail/:id
这里和 axum 有所不同,在 axum 里,声明子路由时可以使用绝对路径的形式,axum 会正确处理,但 react-router不行:
{/* 错误示例 */} <Route path="/news"> <Route path="/" element={<News />} /> <Route path="/detail/:id" element={<NewsDetail />} /> </Route>
当然,“新闻列表”里指向“新闻详情”的链接也要做出修改:
//...
export default function News() {
//...
return (
<div>
<ul>
{newsList.map((news) => (
<li key={news.id}>
<Link to={`/news/detail/${news.id}`}>{news.title}</Link>
</li>
))}
</ul>
</div>
);
}
Link
的 to
由原来的 /news-detail
改成了 /news/detail
。
至此,我们使用 react-router 实现了一个“多页面”应用。恭喜恭喜,撒花撒花 🎉🎉。但作为一个无私奉献的站长,我还想补充两个知识点。继续下一小节吧/
本节代码:axum-rs-react-router-1-nested
获取查询参数
虽然现在大家越来越习惯使用 Path 参数(就是我们这个案例中的“新闻详情”这种动态路由),但 URL 查询参数也还是需要的。利用 react-router 的 useSearchParams
hook就能获取到传递过来的 URL 查询参数。
//...
export default function News() {
const [newsList, setNewsList] = useState([]);
const [searchParams, setSearchParams] = useSearchParams({
page: 0,
keyword: '',
});
const page = searchParams.get('page') || 0;
const keyword = searchParams.get('keyword') || '';
// ...
return (
<div>
<div>
当前页码:{page}, 搜索关键字:
{keyword || '(无)'}
</div>
<hr />
<ul>
{newsList.map((news) => (
<li key={news.id}>
<Link to={`/news/detail/${news.id}`}>{news.title}</Link>
</li>
))}
</ul>
<hr />
<ul>
<li>
<Link to="/news?page=5&keyword=aaa">第5页-aaa</Link>
</li>
<li>
<Link to="/news?page=10&keyword=bbb">第10页-bbb</Link>
</li>
<li>
<button
onClick={() => setSearchParams({ page: 20, keyword: 'axum中文网' })}
>
编程设置:第20页,axum中文网
</button>
</li>
</ul>
</div>
);
}
本节代码:axum-rs-react-router-1-search-params
-
使用
useSearchParams
hookconst [searchParams, setSearchParams] = useSearchParams({ page: 0, keyword: '', });
- 它的返回值是一个数组,分别是:维护所有参数的变量名,以及设置参数的函数
- 它的参数是可选的,可以通过该参数设置URL参数的默认值
-
获取 URL 参数
const page = searchParams.get('page') || 0; const keyword = searchParams.get('keyword') || '';
- 使用
get()
方法获取到指定的 URL 参数。通过||
运算符给定默认值是一种非常好的编程技巧(此方法适用于很多 C 语言系的编程语言,包括 JavaScript、PHP、Bash等)。当然,由于我们useSearchParams
时已经给了默认值,所以此处的||
并不是那么有必要。
- 使用
-
使用 URL 参数
<div> 当前页码:{page}, 搜索关键字: {keyword || '(无)'} </div>
- 将获取到的 URL 参数显示出来,没什么好讲的。
-
设置 URL 参数有很多种方法:
-
通过拼接 URL:
<Link to="/news?page=5&keyword=aaa">第5页-aaa</Link>
-
通过调用
setSearchParams
:- 它允许使用 Javascript 的简单对象
- 它会自动对中文、特殊字符进行 URL 编码
<button onClick={() => setSearchParams({ page: 20, keyword: 'axum中文网' })} > 编程设置:第20页,axum中文网 </button>
-
还有一种是通过
method="get"
的表单进行提交,本案例没有使用这种方法,你自己可以尝试 -
我们不建议使用拼接URL的方式:
- 需要写成
?key=value&key=value
的形式 - 更重要的是,对于中文和特殊字符,需要使用 URL 编码后的内容进行拼接
- 而
setSearchParams
和 表单提交的方式显然更智能一些
- 需要写成
-
本小节代码:axum-rs-react-router-1-search-params
使用路由表统一管理路由规则
路由表允许我们将路由的规则使用一个Javascript的简单对象来定义,方便统一管理和维护。
在与antd等UI库集成时,路由表会非常有用。
目前,我们的路由匹配规则写在 App
里的,让我们回顾一下:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/news">
<Route path="" element={<News />} />
<Route path="detail/:id" element={<NewsDetail />} />
</Route>
<Route path="/about" element={<About />} />
</Routes>
现在,我们要把这些规则用 JavaScript 的普通对象来表示,然后通过 useRoutes
把它们加载到 App
组件中。
编写路由表
// src/routeTable.jsx
import Home from './pages/Home';
import News from './pages/News';
import About from './pages/About';
import NewsDetail from './pages/News/Detail';
const routeTable = [
{
path: '/',
element: <Home />,
},
{
path: '/news',
children: [
{
path: '',
element: <News />,
},
{
path: 'detail/:id',
element: <NewsDetail />,
},
],
},
{
path: '/about',
element: <About />,
},
];
export default routeTable;
- 整个路由表是一个数组,用于代替
<Routes>
- 每个路由规则是一个对象,用于代替
<Route>
- 如果是嵌套路由,则将子路由放在
children
中
非常直观,和之前的直接以标签形式写的几乎是一一对应。
使用路由表
要想使用路由表,必须借助 useRoutes
hook:
// src/App.jsx
import React from 'react';
// import NavBarUsingLink from './components/NavBar/UsingLink';
import NavBarUsingNavLink from './components/NavBar/UsingNavLink';
import { useRoutes } from 'react-router-dom';
import routeTable from './routeTable';
function App() {
const routes = useRoutes(routeTable);
return (
<div>
<NavBarUsingNavLink />
{routes}
</div>
);
}
export default App;
import routeTable from './routeTable';
导入刚刚定义的路由表const routes = useRoutes(routeTable);
将路由表进行转换,并赋值给routes
变量- 在需要匹配路由的地方直接使用该变量:
{routes}