React Router V6 如何使用,与V5的区别
阅读本文后,你将快速掌握 react router v6 的基本使用并简单对比了与 React Route V5 版本的区别。
安装
npm install react-router-dom@6 --save
使用react脚手架创建一个 react + Ts 的 Demo 来演示,并且使用了 antd5 的组件:
// 创建 react 项目
npx create-react-app my-app --template redux-typescript
// 安装 antd5
npm install antd --save
// 安装react-router-dom
npm install react-router-dom@6 --save
常用组件和hooks
组件名 | 作用 | 说明 |
---|---|---|
<BrowserRouter> | 路由模式 | history 路由模式 |
<HashRouter> | 路由模式 | hash 路由模式 |
<Routers> | 一组路由 | 代替原有 |
<Router> | 基础路由 | Router是可以嵌套的,解决原有V5中严格模式,后面与V5区别会详细介绍 |
<Link> | 导航组件 | 在实际页面中跳转使用 |
<Outlet/> | 自适应渲染组件 | 根据实际路由url自动选择组件 |
hooks名 | 作用 | 说明 |
---|---|---|
useParams | 返回当前参数 | 根据路径读取参数 |
useNavigate | 返回当前路由 | 代替原有V5中的 useHistory |
useOutlet | 返回根据路由生成的element | |
useLocation | 返回当前的location 对象 | |
useRoutes | 同Routers组件一样,只不过是在js中使用 | |
useSearchParams | 用来匹配URL中?后面的搜索参数 |
简单使用方法
启用全局路由模式
本文使用 BrowserRouter - history模式。URL采用真实的URL资源,每次路由变更其实都是一次 get 请求。 如图我们定义了一个路由地址为 '/home' 的首页 , 当我们在游览器地址输入 http://localhost:3000/home 时,可以看到我们向这个地址发起了一次 get 请求。
在项目的入口文件使用BrowserRouter来创建路由:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter, HashRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
reportWebVitals();
使用 Routes、Route 配置路由分支
在项目里分别定义了两个组件 Home 和 GoodsList , 在App.tsx文件中使用 Route时 ,外层必须加上 Routes 组件,也就是 Routes -> Route 的组合配置路由分支,在v6版本中移除了v5中的 Switch 组件。
App.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Home from './pages/Home';
import { Route, Routes } from 'react-router-dom';
import GoodsList from './pages/GoodsList';
import GoodsDetail from './pages/GoodsDetail';
import User from './pages/User';
import UserDetail from './pages/UserDetail';
import AddUser from './pages/AddUser';
import NotFound from './pages/NotFound';
function App() {
return (
<div className="App">
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/home" element={<Home />}></Route>
<Route path="/goodsList" element={<GoodsList />}></Route>
</Routes>
</div>
);
}
export default App;
此时使用 "/" 、"/home" 访问页面时,页面渲染 Home 组件。
useNavigate hook
在Home组件使用 useNavigatehook,它返回一个函数帮助我们能够编程式导航。
类型定义:
declare function useNavigate(): NavigateFunction;
interface NavigateFunction {
(
to: To,
options?: {
replace?: boolean;
state?: any;
relative?: RelativeRoutingType;
}
): void;
(delta: number): void;
}
const navigate = useNavigate()
这个 navigate 函数上有两个签名
- 当使用to这个值时其接收的路由地址,例如 navigate('/home')
- 使用delta 时,将你想要放入历史堆栈的增量传递给历史堆栈。 例如 navigate(-1)
Home.tsx 使用:
import React from 'react';
import { useNavigate } from "react-router-dom";
import { MailOutlined } from '@ant-design/icons';
import { MenuProps } from 'antd';
import { Menu } from 'antd';
import './index.css';
type MenuItem = Required<MenuProps>['items'][number];
...
const items: MenuProps['items'] = [
getItem('菜单一', 'sub1', <MailOutlined />, [
getItem('Item 1', 'g1', null, [getItem('商品列表', '1'), getItem('用户列表', '2')], 'group'),
getItem('Item 2', 'g2', null, [getItem('Option 3', '3'), getItem('Option 4', '4')], 'group'),
]),
];
const Home: React.FC = () => {
// 路由跳转方法
const navigate = useNavigate()
const onClick: MenuProps['onClick'] = (e) => {
if (e.key === '1') {
// 接收想要导航到的路由名称
navigate(`/goodsList`);
}
if (e.key === '2') {
navigate(`/user`);
}
};
return (
<div className='container'>
<Menu
onClick={onClick}
style={{ width: 256 }}
defaultOpenKeys={['sub1']}
mode="inline"
items={items}
/>
<div className='content'>
这是首页
</div>
</div>
);
};
export default Home;
此时点击【首页】左侧菜单上的【商品列表】,跳转到商品列表页。
Link 组件
在商品列表页 GoodsList 通过使用 Link 组件 ,在其 to 属性上传递导航到的路由路径实现跳转。
GoodsList.tsx
import React from 'react';
import { Space, Table, Tag } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { Link } from 'react-router-dom';
...
const GoodsList: React.FC = () => {
return (
<div>
<h2>商品列表页</h2>
<Table columns={columns} dataSource={data} />
<div>
// 点击跳转到首页
<Link to='/home'>首页</Link>
</div>
</div>
)
};
export default GoodsList;
经过前面的组件和方法使用一个简单的路由demo就能够正常运行了
动态路由
我们在项目要新增一个 GoodsDetail 商品详情页,该页面是需要从商品列表页跳转过去,并且在URL地址上携带上商品名称,此时我们就可以使用动态路由实现
首先在 App.tsx 里对 GoodsDetail 组件进行动态路由的配置:
import GoodsList from './pages/GoodsList';
import GoodsDetail from './pages/GoodsDetail';
...
function App() {
return (
<div className="App">
<Routes>
...
<Route path="/goodsList" element={<GoodsList />}></Route>
// 以 `:key 形式定义动态路由的key值`
<Route path='/goods/:name' element={<GoodsDetail />}></Route>
</Routes>
</div>
);
}
export default App;
在 GoodList 组件实现动态路由跳转:我们点击商品列表页的 Table 组件的【详情】按钮时,页面导航到商品详情页,并且将商品名称值携带过去。
...
const columns: ColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text) => <a>{text}</a>,
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
{
title: 'Action',
key: 'action',
render: (_, record) => (
<Space size="middle">
// 点击详情按钮,将商品名称拼接到路由地址上
<Link to={`/goods/${record.name}`}>详情</Link>
</Space>
),
},
];
const GoodsList: React.FC = () => {
return (
...
)
};
export default GoodsList;
useParams hook 获取动态路由参数
在上面的动态路由实现过程中,我们从商品列表页跳转商品详情页时,路由地址写到了商品名称,我们可以通过useParams hook拿到。它返回一个包含动态路由 key-value 形式的对象。
GoodsDetail.tsx
import { Button } from 'antd';
import React from 'react'
import { useNavigate, useParams } from 'react-router-dom'
export default function GoodsDetail() {
// 获取动态路路由参数
const routeParams = useParams();
console.log('动态路由参数', routeParams); // 动态路由参数 {name: '苹果'}
const navigate = useNavigate();
return (
<div>
<h2>商品详情页</h2>
<div>商品名称: {routeParams.name}</div>
<div style={{ margin: 20 }}> <Button type='primary' onClick={() => { navigate(-1) }}>
返回商品页面页
</Button>
</div>
<div style={{ margin: 20 }}> <Button onClick={() => { navigate('/home') }}>
返回首页
</Button>
</div>
</div>
)
}
路由嵌套
路由嵌套是一个很强大的功能,可以减少冗杂的布局代码,降低布局的难度。如下使用:
定义一组关于 User 相关的嵌套路由
分别通过如下三种路径匹配:
● "/user"
● "/user/007"
● "/user/addUser"
App.tsx
...
function App() {
return (
<div className="App">
<Routes>
...
{/* 嵌套路由 */}
<Route path='user' element={<User />}>
<Route path=":id" element={<UserDetail />} />
<Route path="addUser" element={<AddUser />} />
</Route>
</Routes>
</div>
);
}
export default App;
Outlet
使用Outlet组件渲染子组件
可以使用一个路由占位符,针对多匹配的路由位置进行复用,类似于vue router的路由插槽和Angular的router-outlet:
User.tsx
import { Button } from 'antd'
import { Link, Outlet } from 'react-router-dom'
export default function User(props: any) {
return (
<div>
<h2>
用户页面
</h2>
<p>
<span style={{ marginRight: 20 }}>用户名称:憨憨</span>
{/* 展示用户详情组件 */}
<Link to={`/user/${'007'}`}>详情</Link>
</p>
<Button>
{/* 展示新增用户组件 */}
<Link to='/user/addUser'>新增用户</Link>
</Button>
<div>
{/* Outlet 渲染子代路由的地方, 功能类似 Vue Router 里的 <router-view> 组件 */}
<Outlet />
</div>
</div>
)
}
访问 /user 时
点击 【详情】按钮访问 /user/007
点击【新增用户】按钮访问 /user/addUser
默认路由 (index 路由)
当一个父路由有多个子路由,且当前URL停留在父路由时,上面例子中,路由为'/user'时,outlet里就无法识别渲染),界面该如何渲染呢?你需要给父路由设置一个默认渲染的子路由。
加上index属性后就会默认渲染该路由下的组件。而且默认索引路由可以放在路由嵌套的任何一级中使用。
...
function App() {
return (
<div className="App">
<Routes>
...
{/* 嵌套路由 */}
<Route path='user' element={<User />}>
{/* 当路由为 /user 时,Outlet 里会默认展示 UserDetail 组件 */}
<Route index element={<UserDetail />} />
<Route path=":id" element={<UserDetail />} />
<Route path="addUser" element={<AddUser />} />
</Route>
</Routes>
</div>
);
}
export default App;
嵌套路由中使用Link
...
// <User />
<div>
...
<Link to={`${'007'}`}>详情</Link>
...
<Link to='addUser'>新增用户</Link>
</div>
...
<Routes>
<Route path='user' element={<User />}>
<Route path=":id" element={<UserDetail />} />
<Route path="addUser" element={<AddUser />} />
</Route>
</Routes>
在上面嵌套路由的例子,我们将Link组件 to 属性里值写法变更
<Link to='/user/addUser'>新增用户</Link> => <Link to='addUser'>新增用户
在这种情况下,上述两个Link会连接到/user/007 和 /user/adduser 两个地址,这就是相对路由。
useSearchParams hook
useSearchParams hook 解析当前路径,返回location中的search;并可重新设置 search 并触发跳转。类似React 自带的 useState hook, 它也是返回一个包含两个值的数组。
实例:
import React from 'react'
import { useParams, useSearchParams } from 'react-router-dom'
import './index.css'
export default function UserDetail() {
const queryParams = useParams();
// 获取是地址栏上URL的 search 参数
let [searchParams, setSearchParams] = useSearchParams();
// 此时 http://localhost:3000/user/007?name=鲁迅Ï
console.log(searchParams.get("name")); // 鲁迅
return (
<div className='box'>
<h3>用户详情</h3>
<div>用户id: {queryParams.id || ''}</div>
<div>用户名称:{searchParams.get("name")}</div>
</div>
)
}
404路由
当没有一个URL匹配时,就会查找是否配置了通用路由 path="*",他可以匹配任何形式的路由,当没有更精确的路由匹配时,就进入该路由的配置,该路由的优先级是最低的。
...
function App() {
return (
<div className="App">
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/home" element={<Home />}></Route>
...
{/* 关于NotFound类路由,可以用*来代替 */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
);
}
export default App;
这是一个不存在的路由名称
v5和v6的区别
v5 的用法可参考:React Router V5
组件层面上:
● 老版本路由采用了 Router Switch Route 结构,Router -> 传递状态,负责派发更新; Switch -> 匹配唯一路由 ;Route -> 真实渲染路由组件。
● 新版本路由采用了 Router Routes Route 结构,Router 为了抽离一 context; Routes -> 形成路由渲染分支,渲染路由;Route 并非渲染真实路由,而是形成路由分支结构。
使用层面上:
● 老版本路由,对于嵌套路由,配置二级路由,需要写在具体的业务组件中。
● 新版本路由,在外层统一配置路由结构,让路由结构更清晰,通过 Outlet 来实现子代路由的渲染,一定程度上有点类似于 vue 中的 view-router。
● 新版本做了 API 的大调整,比如 useHistory 变成了 useNavigate,减少了一些 API ,增加了一些新的 api
原理层面上:
● 老版本的路由本质在于 Route 组件,当路由上下文 context 改变的时候,Route 组件重新渲染,然后通过匹配来确定业务组件是否渲染。
● 新版本的路由本质在于 Routes 组件,当 location 上下文改变的时候,Routes 重新渲染,重新形成渲染分支,然后通过 provider 方式逐层传递 Outlet,进行匹配渲染。