React-router-v7
1.安装
1.从react-router-v7
开始,将使用如下方式安装。
bash
npm i react-router
v6即之前为
react-router-dom@6
2.你的应用程序外层渲染一个 <BrowserRouter>
组件:
jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter} from "react-router";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<App/>
</BrowserRouter>
</StrictMode>
);
2.路由器
react-router
在组件顶层放置了一个路由器xxRouter
组件,里面维护了各种路由Route
组件。
路由器一共分为两种,BrowserRouter
和HashRouter
。
BrowserRouter
-
基于history模式:页面跳转原理是使用了HTML5为浏览器全局的history对象新增了两个API,包括
history.pushState
、history.replaceState
;和
vue router
的history模式实现一致 -
最终实现,直接拼接路径,例如
localhost:8080/about
-
后端需要做处理,因为我们在浏览器中输入链接时会触发一个get请求,
localhost:8080/about
的形式,实际上也会去匹配后端的/about
路由。
HashRouter
-
基于hash模式 :页面跳转原理是使用了
location.hash
、location.replace
;和vue router
的hash模式实现一致 -
在域名后,先拼接/#,再拼接路径;也就是利用锚点,实现路由的跳转;例如:
localhost:8080/#/about
我们切换路由时实际上切换的是
#/xx
的部分,不改变域名,所以不会触发新的请求,也就无需后端做处理。
示例
二者的使用方式是一样的,不会影响到其他组件。
jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { HashRouter} from "react-router";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<HashRouter>
<App/>
</HashRouter>
</StrictMode>
);
3.路由
配置路由
路由是通过渲染 <Routes>
和 <Route>
组件来进行配置的,它将 path 和 对应组件关联了起来。
jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router";
import App from "./app";
const root = document.getElementById("root");
ReactDOM.createRoot(root).render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
</Routes>
</BrowserRouter>
);
下面是稍微复杂一点的例子:
jsx
<Routes>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route element={<AuthLayout />}>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
</Route>
<Route path="concerts">
<Route index element={<ConcertsHome />} />
<Route path=":city" element={<City />} />
<Route path="trending" element={<Trending />} />
</Route>
</Routes>
<Routes>
开辟路由空间,用于包裹<Route>
,提示内部是具体的路由规则组件。
<Route>
具体的路由规则。从源码我们可以知道其上的属性。
ts
interface PathRouteProps {
caseSensitive?: NonIndexRouteObject["caseSensitive"];
path?: NonIndexRouteObject["path"];
id?: NonIndexRouteObject["id"];
lazy?: LazyRouteFunction<NonIndexRouteObject>;
loader?: NonIndexRouteObject["loader"];
action?: NonIndexRouteObject["action"];
hasErrorBoundary?: NonIndexRouteObject["hasErrorBoundary"];
shouldRevalidate?: NonIndexRouteObject["shouldRevalidate"];
handle?: NonIndexRouteObject["handle"];
index?: false;
children?: React.ReactNode;
element?: React.ReactNode | null;
hydrateFallbackElement?: React.ReactNode | null;
errorElement?: React.ReactNode | null;
Component?: React.ComponentType | null;
HydrateFallback?: React.ComponentType | null;
ErrorBoundary?: React.ComponentType | null;
}
/**
* @category Types
*/
interface LayoutRouteProps extends PathRouteProps {
}
/**
* @category Types
*/
interface IndexRouteProps {
caseSensitive?: IndexRouteObject["caseSensitive"];
path?: IndexRouteObject["path"];
id?: IndexRouteObject["id"];
lazy?: LazyRouteFunction<IndexRouteObject>;
loader?: IndexRouteObject["loader"];
action?: IndexRouteObject["action"];
hasErrorBoundary?: IndexRouteObject["hasErrorBoundary"];
shouldRevalidate?: IndexRouteObject["shouldRevalidate"];
handle?: IndexRouteObject["handle"];
index: true;
children?: undefined;
element?: React.ReactNode | null;
hydrateFallbackElement?: React.ReactNode | null;
errorElement?: React.ReactNode | null;
Component?: React.ComponentType | null;
HydrateFallback?: React.ComponentType | null;
ErrorBoundary?: React.ComponentType | null;
}
type RouteProps = PathRouteProps | LayoutRouteProps | IndexRouteProps;
/**
* Configures an element to render when a pattern matches the current location.
* It must be rendered within a {@link Routes} element. Note that these routes
* do not participate in data loading, actions, code splitting, or any other
* route module features.
*
* @category Components
*/
declare function Route$1(_props: RouteProps): React.ReactElement | null;
嵌套路由
路由可以被嵌套在父路由中。
jsx
<Routes>
<Route path="dashboard" element={<Dashboard />}>
<Route index element={<Home />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
父路由的 path 会自动添加到子路由的 path 中,所以上面的例子中,会创建两个路由 "/dashboard"
和 "/dashboard/settings"
。
子路由通过父路由组件中的 <Outlet/>
渲染。
jsx
import { Outlet } from "react-router";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* 将被渲染成 <Home/> 或 <Settings/> */}
<Outlet />
</div>
);
}
<Outlet/>
子路由占位符组件,可以理解为他就是一个插槽。
常规组件渲染
jsx
import Hello from "./Hello.jsx"
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Hello />
</div>
);
}
手动引入组件并渲染。
路由组件的逻辑
jsx
import { Outlet } from "react-router";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* 将被渲染成 <Home/> 或 <Settings/> */}
<Outlet />
</div>
);
}
当匹配到/home
,对应的路由组件为<Home/>
,则<Outlet/>
被替换为<Home/>
.
jsx
//引入的实际是个插槽,会根据路由规则被真实组件替换
import { Outlet } from "react-router";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* 根据路由规则匹配到的组件进行实际渲染*/}
<Home />
</div>
);
}
布局路由
没有 path 的路由看起来会有一定的嵌套关系,但不会在路由上添加额外的路径。
jsx
<Routes>
<Route element={<MarketingLayout />}>
<Route index element={<MarketingHome />} />
<Route path="contact" element={<Contact />} />
</Route>
<Route path="projects">
<Route index element={<ProjectsHome />} />
<Route element={<ProjectsLayout />}>
<Route path=":pid" element={<Project />} />
<Route path=":pid/edit" element={<EditProject />} />
</Route>
</Route>
</Routes>
所谓布局路由,一般是提供布局样式,但是我们不改变路由路径,在合适的位置放入<Outlet/>
渲染路由,例如<MarketingLayout />
jsx
export function MarketingLayout(){
return (
<>
<siderBar/>
{/* 用于渲染路由 */}
<Outlet/>
<Footer/>
<>
)
}
索引路由
索引路由会在其父路由组件对应的 <Outlet/>
中进行渲染(就像是默认的子路由一样)。它们通过 index
属性来进行配置。
jsx
<Routes>
<Route path="/" element={<Root />}>
{/* 访问 "/" 会被渲染到 Outlet 里 */}
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />}>
{/* 访问 "/dashboard" 会被渲染到 Outlet 里 */}
<Route index element={<DashboardHome />} />
<Route path="settings" element={<Settings />} />
</Route>
</Route>
</Routes>
请注意,索引路由不能有子路由。如果你期望实现有子路由这种行为,那你可能需要一个布局路由
。
错误示范
jsx
<Route index element={<Home />}>
<Route path="settings" element={<Settings />} />
</Route>
错误原因:索引路由不能有子路由。
路由前缀
一个不带 element
属性的 <Route path>
会为其子路由添加路径前缀,无须引入父布局。
jsx
<Route path="projects">
<Route index element={<ProjectsHome />} />
<Route element={<ProjectsLayout />}>
<Route path="home" element={<Project />} />
</Route>
</Route>
可以理解为给路由进行分组,单纯添加前缀,而不影响组件布局。我们将用/projects/home
渲染出<Project />
。
动态传参
如果路由的 path 包含 :
,那么它会被视为动态参数,当路由被匹配时,动态参数将被解析并提供给其他路由 API 接口,比如 useParams
。
路由规则
jsx
<Route path="teams/:teamId" element={<Team />} />
路由组件内接收
tsx
import { useParams } from "react-router";
export default function Team() {
let params = useParams();
// params.teamId
}
在一个路由中可以有多个动态参数:
jsx
<Route path="/c/:categoryId/p/:productId" element={<Product />} />
可选参数
将结合路由传参讲解
在动态参数后面添加 ?
可以使其变为可选参数。
jsx
<Route path=":lang?/categories" element={<Categories />} />
也可以在静态字符串后面加上 ?
,使其变为可选参数:
jsx
<Route path="users/:userId/edit?" component={<User />} />
通配符
也被称为 "捕获所有"
和 *
路由,如果一个路由模式以 /*
结尾,后面的任意字符串都会匹配到,也包括其他的 /
。
路由规则
jsx
<Route path="files/*" element={<File />} />
路由组件内接收
ts
let params = useParams();
// `params["*"]` 将包含 `files/` 之后剩余的URL内容。
let filePath = params["*"];
你可以对 *
进行解构,只是必须给它赋一个新的名称。一般命名为 splat
。
jsx
let { "*": splat } = useParams();
4.导航
声明式路由导航
所谓声明式路由导航,就是在组件中添加跳转链接,点击该链接进行跳转,本质是对<a>
标签的封装。
Link
不需要激活样式时,可以使用 <Link>
组件。
jsx
import { Link } from "react-router";
export function LoggedOutMessage() {
return (
<p>
您已退出. <Link to="/login">再次登录</Link>
</p>
);
}
NavLink
该组件适用于那些需要渲染激活状态的导航链接。
jsx
import { NavLink } from "react-router";
export function MyAppNav() {
return (
<nav>
<NavLink to="/" end>
Home
</NavLink>
<NavLink to="/trending" end>
Trending Concerts
</NavLink>
<NavLink to="/concerts">All Concerts</NavLink>
<NavLink to="/account">Account</NavLink>
</nav>
);
}
当 <NavLink>
处于激活状态时,它会自动拥有一个 .active
类名,以便于使用 CSS 轻松设置样式:
css
a.active {
color: red;
}
它还在 className
(类名)、style
(样式)以及 children
(子元素)上有回调属性,这些回调属性带有活动状态,可用于内联样式设置或条件渲染。
案例1
jsx
// className
<NavLink
to="/messages"
className={({ isActive }) => (isActive ? "text-red-500" : "text-black")}
>
Messages
</NavLink>
案例2
jsx
// style
<NavLink
to="/messages"
style={({ isActive }) => ({
color: isActive ? "red" : "black",
})}
>
Messages
</NavLink>
案例3
jsx
// children
<NavLink to="/message">
{({ isActive }) => (
<span className={isActive ? "active" : ""}>
{isActive ? "👉" : ""} Tasks
</span>
)}
</NavLink>
编程式路由导航
按照编程要求进行路由跳转,例如:
- 我们点击某个按钮,然后导航到其他页面。
- 延时多少秒后,自动导航到其他页面。
核心是满足某个条件触发方法进行跳转,而不是让用户手动去点击导航链接。
该方式使得路由跳转更灵活,且不会增加新的DOM元素。
useNavigate
jsx
import { useNavigate } from "react-router";
export function LoginPage() {
let navigate = useNavigate();
return (
<>
<MyHeader />
<MyLoginForm
onSuccess={() => {
navigate("/dashboard");
}}
/>
<MyFooter />
</>
);
}
使用步骤:
1.引入hooks
jsx
import { useNavigate } from "react-router";
2.获得导航方法
jsx
let navigate = useNavigate();
3.触发导航事件,和声明式路由导航的to
一样。
jsx
navigate("/dashboard");
小结
声明式路由导航的优点
对于常规导航而言,最好使用 <Link>
或 <NavLink>
。
它们能提供更好的默认用户体验,比如键盘事件、无障碍标签、"在新窗口中打开"、右键上下文菜单等等。
编程式路由导航的优点
核心是满足某个条件触发方法进行跳转,而不是让用户手动去点击导航链接。
该方式使得路由跳转更灵活,且不会增加新的DOM元素。
5.路由传参
url查询参数
1.路由跳转时在url后用?
拼接的参数。
2.在路由组件中使用useSearchParams()
接收。
参数传递
jsx
import { useNavigate } from "react-router";
export function Father() {
let navigate = useNavigate();
const handleUrl = ()=>{
navigate("/dashboard?name=zhansan&age=19&isOk=true");
}
return (
<>
<button onClick={handleUrl}>url传参</button>
</>
);
}
实际上传递时是不带类型的,和前后端query传参一样,都是双方事先约定好了类型,接收方再根据要求自行转换。
封装方法
React及其社区都遵循极简规范,我们可以根据实际开发需求来封装方法。
例如使用navigate()
传递url查询参数时,其参数实际上就是个query字符串,我们可以用对象进行转换。
ts
//传递一个对象,然后返回query字符串
export function getNavigateURLString(url:string,params:object){
for(let key of params){
url += `${key}=${params[key]}`
}
return url
}
然后使用即可
jsx
import { useNavigate } from "react-router";
import {getNavigateURLString} from '@/utils'
export function Father() {
let navigate = useNavigate();
const handleUrl = ()=>{
let params = {
name:"zhansan",
age:"19",
isOk:true
}
navigate(getNavigateURLString("/dashboard",params));
}
return (
<>
<button onClick={handleUrl}>url传参</button>
</>
);
}
推荐结合实际需求封装一个hook
参数接收
在被导航到的路由组件,我们可通过 useSearchParams
来获取它们,该函数会返回一个URLSearchParams
的实例。
通过URLSearchParams实例.get(键)
来获取url查询参数上对应的值。
jsx
function Son() {
let [searchParams] = useSearchParams();
return (
<div>
<p>
{/* searchParams.get("age") 得到19 */}
You searched for <i>{searchParams.get("age")}</i>
</p>
</div>
);
}
适用场景
适用于参数较多,且会在url中显示的情况。
path参数
也被称为路由参数。
1.要在路由规则处设置动态参数占位符
2.跳转时会解析路由,按照/
,进行划分,将对应位置替换后得到最终的跳转链接
3.在路由组件中使用useParams
钩子中获取占位符参数。
本质上是动态路由
路由规则设置
jsx
<Route path="/" element={<App />}>
<Route path="dashboard/:val" element={<Home />}></Route>
</Route>
参数传递
jsx
import { useNavigate } from "react-router";
export function Father() {
let navigate = useNavigate();
const handlePath = ()=>{
navigate(getNavigateURLString("/dashboard/true",params));
}
return (
<>
<button onClick={handlePath}>path传参</button>
</>
);
}
参数接收
jsx
import { useParams } from "react-router";
export default function Home() {
const params = useParams();
return <div>home-{params.val}</div>;
}
原理讲解
跳转机制
1.我们调用navigate("/dashboard/true")
,导航跳转到/dashboard/true
路由,该路由存在,且匹配到/dashboard/:xx
2.我们在/dashboard/:xx
对应的路由组件中使用useParams
就可以获得对应的值。
注意事项
如果我们使用了错误的导航地址,例如:
1.调用navigate("/dashboard/true/abc")
,导航被跳转到/dashboard/true/abc
,不会去匹配/dashboard/:xx
小结
本质上是先改变路由,跳转到对应的路由组件,然后该路由组件再使用方法获取。
匹配机制
jsx
<Route path="/" element={<App />}>
<Route path="dashboard/:id" element={<About />}></Route>
<Route path="dashboard/:val" element={<Home />}></Route>
</Route>
对于路由跳转而言,dashboard/:id
和dashboard/:val
实际上是一样的,都是dashboard/:xx
.
所以按照从上到下的匹配规则,我们只能匹配到<About/>
.
小结
path参数传参的类型不够灵活,适用于参数较少的情况。
适合于文档导航或者商品导航,这种通过改变一个值就实现页面切换的需求。
state参数
1.使用state对象传递参数
2.在路由组件中使用useLocation()
获取。
参数传递
jsx
import { useNavigate } from "react-router";
export function Father() {
let navigate = useNavigate();
const handleState = () => {
navgiate("/news", {
state: {
name: "zhansan",
age: 19,
isOk: true,
},
});
};
return (
<>
<button onClick={handleState}>state传参</button>
</>
);
}
参数接收
js
import { useLocation } from "react-router";
export default function News() {
const { state } = useLocation();
console.log(state);
return <div>news-{state.name}</div>;
}
小结
1.state传参是使用navgiate()
的第二个参数,是一个配置对象,给该对象上的state
属性赋值。
js
navgiate(
"/news",
//一个配置对象,state传参是该对象上的属性
{
state: {
name: "zhansan",
age: 19,
isOk: true,
},
}
);
2.参数接收时,使用useLocation()
,他获取的值如下:
js
{
"pathname": "/news",
"search": "",
"hash": "",
"state": {
"name": "zhansan",
"age": 19,
"isOk": true
},
"key": "clc8brc7"
}
state传递的参数可以从上面解构。
js
const { state } = useLocation();
console.log(state);
小结
注意: 以上三种方式在页面刷新时都不会丢失。
6.路由重定向
1.Navigate声明式重定向
使用Navigate
组件来执行声明性重定向。
案例1
在组件中使用,每当用户导航到About页面时,Navigate组件将声明性地执行重定向到/news
。
jsx
import {
Routes,
Route,
Link,
Navigate,
} from 'react-router';
export const About = () => {
const shouldRedirect = true;
return (
<>
<h2>About</h2>
{shouldRedirect && <Navigate replace to="/news" />}
</>
);
};
案例2
直接在路由规则中使用,根据shouldRedirect
,决定是否采用重定向组件
jsx
<Route
path="about"
element={
shouldRedirect ? (
<Navigate replace to="/news" />
) : (
<About />
)
}
/>
2.useNavigate编程式重定向
本质上是使用useNavigate
的跳转功能。
在About组件中使用useEffect
,在初始渲染时跳转到其他页面,这样就实现重定向的效果。
jsx
import {useEffect} from 'react'
import {
Routes,
Route,
Link,
useNavigate,
} from 'react-router';
const About = () => {
const shouldRedirect = true;
const navigate = useNavigate();
useEffect(() => {
if (shouldRedirect) {
navigate('/home');
}
});
return (
<>
<h2>About</h2>
</>
);
};
在About
组件初始渲染时,调用navigate()
方法跳转到其他页面。
7.路由匹配
顺序匹配
在路由匹配时根据从上到下的原则按照顺序匹配,例如/home/first
.
现有路由规则
jsx
<Route path="/" element={<App />}>
<Route path="home" element={<Home />}>
<Route path="first" element={<HomeFirst />}></Route>
</Route>
<Route path="about" element={<About />}>
<Route path="first" element={<AboutFirst />}></Route>
</Route>
</Route>
在这个案例中,/home/first
在匹配时,先匹配/
,然后找home
子规则,找到后继续匹配first
。
注意事项:除了第一个入口规则以
/
开头,后续所有子规则前都没有/
。
先入为主
匹配到第一个满足条件的就返回,不会继续往下匹配,后续的会被忽略。
即相同的路由规则,只有先出现的会被匹配到。
现有路由规则
jsx
<Route path="/" element={<App />}>
<Route path="home" element={<Home />}>
<Route path="first" element={<HomeFirst />}></Route>
<Route path="first" element={<HomeSecond />}></Route>
</Route>
</Route>
在这个案例中,用/home/first
进行匹配。
jsx
//只会匹配到这条规则
<Route path="first" element={<HomeFirst />}></Route>
//这条规则将永远不会被匹配到。
<Route path="first" element={<HomeSecond />}></Route>
回溯匹配
路由匹配实际上是个回溯的过程,例如
jsx
<Route path="/" element={<App />}>
<Route path="home">
<Route path="first" element={<About />}></Route>
</Route>
<Route path="home">
<Route path="second" element={<News />}></Route>
</Route>
</Route>
1./home/second
开始匹配,他先匹配到/
,进入匹配。
2.继续匹配到home
,匹配这一组
jsx
<Route path="home">
<Route path="first" element={<About />}></Route>
</Route>
3.匹配完发现没有满足条件的,回溯到上一节点继续向下匹配.
jsx
<Route path="home">
<Route path="second" element={<News />}></Route>
</Route>
4.最终匹配到<News>
这条规则。
通配符
也称为兜底,*
可以匹配任何规则。
jsx
<Route path="/" element={<App />}>
<Route path="home">
<Route path="first" element={<About />}></Route>
{/* 此处会匹配所有的 /home/xx */}
<Route path="*" element={<Hello />}></Route>
</Route>
<Route path="home">
<Route path="second" element={<News />}></Route>
</Route>
{/* 此处会匹配所有的 /xx */}
<Route path="*" element={<About />}></Route>
</Route>
1.*
现象
现象1:我们将*
写在了最前面,但是我们仍然能匹配到/home/first
。
jsx
<Route path="/" element={<App />}>
<Route path="home">
<Route path="*" element={<Home />}></Route>
<Route path="first" element={<About />}></Route>
<Route path="second">
<Route path="news" element={<News />}></Route>
</Route>
</Route>
</Route>
现象2:我们在后面也写了一组home
,但是仍然能匹配到/home/third
jsx
<Route path="/" element={<App />}>
<Route path="home">
<Route path="*" element={<Home />}></Route>
<Route path="first" element={<About />}></Route>
<Route path="second">
<Route path="news" element={<News />}></Route>
</Route>
</Route>
<Route path="home">
<Route path="third" element={<Third />}></Route>
</Route>
</Route>
现象3:我们用/home/ppt
,匹配到的是/home/*
,而不是/*
。
jsx
<Route path="/" element={<App />}>
<Route path="*" element={<Hello />}></Route>
<Route path="home">
<Route path="*" element={<Home />}></Route>
<Route path="first" element={<About />}></Route>
<Route path="second">
<Route path="news" element={<News />}></Route>
</Route>
</Route>
<Route path="home">
<Route path="third" element={<Third />}></Route>
</Route>
</Route>
2.小结:
1.*
代表通用符号,即任意值,但他不具有拦截常规路由的功能,只能拦截同样的路由。
jsx
<Route path="*" element={<Hello />}></Route>
<Route path="*" element={<Home />}></Route>
只会匹配到第一条。
2.*
,本质上是个任意值匹配,用于兜底来替代没有的路由值,例如/home/ppt/abc
。
