使用React这些工具所编写的项目通常都是单页应用(SPA)。单页应用中,整个应用中只含有一个页面,React会根据不同的状态在应用中显示出不同的组件。但是我们之前所编写应用还存在着一个问题,整个应用只存在一个页面,一个请求地址,这就使得用户只能通过一个地址访问应用,当我们点击组件中的不同链接时应用的地址是不会发生变化的。这又有什么问题呢?由于应用只有一个地址,所以我们通过该地址访问应用时,总会直接跳转到应用的首页。如此一来,我们便不敢随意的刷新页面,因为一旦刷新页面便直接跳转到首页。在对页面进行分享时,也只能分享网站的首页,而不能分享指定的页面。
怎么办呢?难道我们要将一个页面拆分为多个页面吗?很明显不能这么做,这么做以后应用的跳转便脱离了React的控制,增加应用的复杂度,提高了项目维护的成本。
为了解决这个问题,我们需要引入一个新的工具React Router,React Router为我们提供一种被称为客户端路由的东西,通过客户端路由可以将URL地址和React组件进行映射,当URL地址发生变化时,它会根据设置自动的切换到指定组件。并且这种切换完全不依赖于服务器。换句话说,在用户看来浏览器的地址栏确实发生了变化,但是这一变化并不由服务器处理,而是通过客户端路由进行切换。
React Router最新版本为6,版本6和版本5之间的变化跨度比较大,我们的课程会分别讲解两个版本。
版本5
安装:
npm
npm install react-router-dom@5 -S
yarn
yarn add react-router-dom@5
HelloWorld:
import ReactDOM from "react-dom/client"; import {BrowserRouter, Link, Route, Switch} from "react-router-dom"; const Home = () => { return <div>这是首页</div>; }; const About = () => { return <div>关于我们,其实没啥可说的</div> }; const App = () => { return <div> <ul> <li> <Link to="/home">首页</Link> </li> <li> <Link to="/about">关于</Link> </li> </ul> <Switch> <Route path="/home" component={Home} /> <Route path="/about" component={About} /> </Switch> </div>; }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <BrowserRouter> <App/> </BrowserRouter> );
解析
1. react-router-dom包
react router适用于web和原生项目,我们在web项目中使用,所以需要引入的包是react-router-dom。
2. BrowserRouter组件
和Redux类似,要使得路由生效,需要使用Router组件将App组件包裹起来。这里我们选择的是BrowserRouter,除了BrowserRouter外还有其他的Router,暂时我们只介绍BrowserRouter。
案例中,BrowserRouter我们是这样使用的:
import {BrowserRouter, Link, Route, Switch} from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App/>
</BrowserRouter>
);
实际开发中,也可以为BrowserRouter起一个别名Router,这样一来我们在切换Router时,只需要修改引用位置,而不需要修改其他代码,像是这样:
import {BrowserRouter as Router, Link, Route, Switch} from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Router>
<App/>
</Router>
);
3. Route组件
route组件是路由的映射组件,通过该组件将url地址和React组件进行映射,映射后当url地址变为指定地址时指定的组件就会显示,否则不显示。
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
上例中,路径/home
和<Home/>
组件进行了映射,路径/about
和<About/>
组件进行了映射。当访问http://localhost:3000/about
时,about组件会自动渲染显示,访问http://localhost:3000/home
时,home组件会自动渲染显示。
Route组件可以设置以下几个属性
- path
- exact
- strict
- component
- render
- children
- location
- sensitive
path
用来设置要映射的路径,可以是一个字符串或字符串数组。字符串用来匹配单个路径,数组可以匹配多个路径。看一个数组的例子:
<Route path={["/about", "/hello"]}>
<About/>
</Route>
使用数组映射后,当我们访问数组中的路径时都会使组件挂载。设置路径时也可以在路径中设置参数,比如:/student/:id
其中id就是一个参数,可以动态的传递:id
的值,换句话说/student/1
或/student/2
,都会触发组件挂载。
设置动态参数后,在组件的内部可以使用useParams()
钩子来读取参数:
const Student = () => { const {id} = useParams(); return <div>学生id:{id}</div> }; ...略... <Route path="/student/:id"> <Student/> </Route> ...略...
exact
路由的匹配默认并不是完整匹配,这意味着路由映射的地址是/home
时,只要我们访问的路径是以/home
开头就会触发组件的挂载,默认情况下是不会检查路由的子路径的。比如:/home/hello
、/home/abc
等都会导致home组件挂载。
exact属性用来设置路由地址是否完整匹配,它需要一个布尔值,默认为false,就像上边的情况。如果设置为true,那么只有地址和path完全一致时,组件才会挂载。
<Route path="/home" exact> <Home/> </Route>
这样一来只有访问/home
时,home组件才会挂载,差一个字母都不行哦!
strict
布尔值,默认值为false。false时,会匹配到以/
结尾的路径。比如:path设置为/home
默认情况下/home/
也会导致组件挂载。设置为true时,以/
结尾的路径不会被匹配。
component
设置路径匹配后需要挂载的组件。作用和Route的标签体类似。
<Route path="/home" component={Home}/>
和标签体指定组件不同,如果通过component属性指定组件,React Router会自动向组件中传递三个参数match、location和history。
match
对象,表示请求匹配的路径信息,其中包含四个属性:
- param —— 请求参数
- isExact —— 布尔值,请求路径是否完整匹配
- path —— 请求路径的规则
- url —— 匹配到的url地址
location
对象,表示浏览器地址栏的信息,请求完整路径、查询字符串等,可能具有的属性:
- pathname —— 请求的路径
- search —— 查询字符串
- hash —— hash字符串
- state —— 历史记录中的状态对象,可以用来在跳转时传递数据
history
对象,用来读取和操作浏览器的历史记录(页面跳转)等功能,属性:
- length —— 历史记录的数量
- action —— 当前历史记录的状态,pop(前进、后退、新记录创建、索引发生变化);push(新记录添加);replace(历史记录被替换)
- location —— location对象
- push() —— 添加新的历史记录
- replace() —— 替换历史记录
- go() —— 跳转到指定记录
- goBack() —— 回退
- goForward() —— 前进
- block() —— 用来阻止用户跳转行为,可以用Prompt组件代替
render
render也是Route组件中的属性,和component类似,也用来指定路径匹配后需要挂载的组件。只是render需要的是一个回调函数作为参数,组件挂载时,render对应的回调函数会被调用,且函数的返回值会成为被挂载的组件。render的回调函数中会接收到一个对象作为参数,对象中包含三个属性,即match、location和history,我们可以根据需要选择是否将其传递给组件。
<Route path="/student/:id" render={routeProps => <Student {...routeProps}/>} />
children
children实际上就是组件的组件体,设置方式有两种一个是通过组件体设置,一个是通过children属性设置。它的值也有两种方式,一种直接传递组件,这样当路径匹配时组件会自动挂载。一种是传递一个回调函数,这样它和render的特点是一样的。
直接设置组件:
<Route path="/student/:id" children={<Student/>} />
<Route path="/student/:id"> <Student/> </Route>
传递回调函数:
<Route path="/student/:id" children={routeProps => <Student {...routeProps}/>} />
<Route path="/student/:id"> {routeProps => <Student {...routeProps}/>} </Route>
需要注意的时,当children接收到的是一个回调函数时,即使路径没有匹配组件也会被挂载到页面中(没有使用Switch标签的情况下),这一特性可以在一些特殊应用场景下发挥作用。如果不希望出现路径不匹配时组件被挂载的情况,最好选择使用render来代替。
4. Switch组件
Switch组件是Route组件的外部容器,可以将Route组件放入到Switch组件中。放入Switch组件中后,匹配路径时会自动自上向下对Route进行匹配,如果匹配到则挂载组件,并且一个Switch中只会有一个Route被挂载。如果将Route组件单独使用,那么所有的路径匹配的Route中的组件都会被挂载。
5. Link组件
Link组件作用类似于a标签(超链接),并且Link组件在浏览器中也会被渲染为超链接。但是Link组件生成的链接点击后只会修改浏览器地址栏的url,并不会真的向服务器发送请求。这种方式有利于组件的渲染,所以在开发中应该使用Link组件而不是超链接。
其他组件
1. HashRouter组件
除了BrowserRouter以外,react router中还为我们提供了HashRouter,它是干什么用的呢?其实很简单,当我们使用BrowserRouter时,路径会直接根据url地址进行跳转,也就是我们在使用应用时在浏览器的地址栏看到的地址就和我们正常去访问网页一样。
但是,HashRouter不是这样,使用HashRouter时,组件的跳转不再是以完整的url形式,而是通过url地址中的hash值进行跳转(url地址中#后的内容为hash值)。
BrowserRouter的地址栏
HashRouter的地址栏
为什么会有这两种Router呢?首先,你要明确我们的项目在开发完成后需要进行构建,构建后的代码需要放到服务器中,以供用户访问。服务器无非就是Nginx或Apache这些东西,服务器的主要功能是将url地址和网页进行映射。传统web项目中,每一个页面都对应一个文件,当用户访问/index.html时,服务器会自动返回根目录下的index.html。当用户访问/about.html时,服务器会返回根目录下about.html。换句话说url和文件的映射都是由服务器来完成的。
但是React项目不同,React项目所有的页面都是通过React进行渲染构建的。项目中只存在一个index.html没有那么多的页面(所以才叫单页应用)。当浏览器地址发生变化时,比如用户访问/about时,此时是不需要服务器介入的,react router会自动挂载对应的组件。
当我们将React项目部署到服务器时,如果直接访问根目录,请求会直接发送给index.html。这个页面我们是有的,所以此时不会有任何问题。用户访问页面后,点击页面后的连接切换到不同的组件也没有问题,因为页面并没有真的发生跳转,而是通过react router在内存中完成了模拟跳转。但是,当我们刷新某个路由或直接通过浏览器地址栏访问某个路由时,比如:http://localhost:3000/about,此时请求会发送给服务器,服务器会寻找名为about的资源(此时并没有经过React)。显然找不到这个资源,于是返回404。
这样一来,我们的项目只能够通过首页访问,然后点击链接跳转,刷新和直接通过路由访问都是不行的,一旦进行这些操作就会出现404。
怎么办呢?两种解决方式:
- 使用HashRouter,HashRouter通过hash地址跳转,而服务器不会处理hash地址,这样地址就会交由React处理,路由便可正常跳转。缺点是url地址上总会多出一个#,但不妨碍使用。
- 修改服务器映射规则,将所有的请求交给React处理,禁止服务器自动匹配页面。以nginx为例,可以将
nginx.conf
中的配置信息修改如下:
location / {
root html;
try_files $uri /index.html;
}
两种方式都可以解决404的问题,具体采用那种方案,需要根据你自己项目的实际情况选择。
2. NavLink组件
特殊版本的Link,可以根据不同的情况设置不同的样式。
属性:
- activeClassName —— 字符串 链接激活时的class
- activeStyle —— 对象 链接激活时的样式
- isActive —— 函数,可动态判断链接是否激活
- style —— 函数,动态设置样式
- className —— 函数,动态设置class值
3. Prompt组件
prompt组件可以在用户离开页面前弹出提示。
属性:
- message 字符串/函数,设置离开前显示的提示信息
- when布尔值,设置是否显示提示
4. Redirect组件
将请求重定向到一个新的位置,经常用来进行权限的处理。例如:当用户已经登录时则正常显示组件,用户没有登录时则跳转到登录页面。
{isLogin && <SomeAuthComponent/>}
{!isLogin && <Redirect to={"/login"}></Redirect>}
上例中,如果isLogin的值为true,表示用户已经登录,若用户登录,则挂载对应组件。若isLogin值为false,则挂载Redirect组件触发重定向,重定向会使得路径跳转到登录页面。
属性:
- to —— 重定向的目标地址,可以是一个字符串也可以是一个对象
- from —— 需要重定向的地址
- push —— 布尔值,是否使用push方式对请求进行重定向
5. 钩子函数
- useHistory
- useLocation
- useParams
- useRouteMatch
版本6
安装:
npm
npm install react-router-dom@6 -S
yarn
yarn add react-router-dom@6
HelloWorld
import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import { BrowserRouter as Router, Link, Route, Routes } from ‘react-router-dom’;
const Home = ()=>{
return <div>首页</div>
};
const About = () => {
return <div>关于</div>
};
const App = () => {
return <div>App
<ul>
<li>
<Link to=”/”>home</Link>
</li>
<li>
<Link to=”/about”>about</Link>
</li>
</ul>
<Routes>
<Route path=”/” element={<Home/>}/>
<Route path=”/about” element={<About/>}/>
</Routes>
</div>;
};
const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(
<Router>
<App />
</Router>
);
Routes组件
和版本5不同,6中的Route组件不能单独使用,而是必须要放到Routes组件中。简言之Routes就是一个存放Route的容器。
Route组件
Route作用和版本5的一样,只是变得更简单了,没有了那么多复杂的属性,并且Route组件必须放到Routes中,当浏览器的地址发生变化时,会自动对Routes中的所有Route进行匹配,匹配到的则显示,其余Route则不再继续匹配。可以将Route当成是一个类似于if语句的东西,路径(path)匹配则其中的组件便会被渲染。
- path —— 要匹配的路径
- element —— 路径匹配后挂载的组件,直接传JSX
- index —— 布尔值,路由是否作为默认组件显示
Outlet组件
Outlet组件用来在父级路由中挂载子路由。
在版本6中Route组件是可以嵌套的,可以通过嵌套Route来构建出嵌套路由,像这样:
<Route path='/students' element={<StudentList/>}>
<Route path=':id' element={<Student/>}/>
</Route>
上例中,Route嵌套后,如果访问/students
则会挂载StudentList组件,如果访问/students/:id
则会自动在StudentList组件中对Student组件进行挂载。在StudentList组件中就可以使用Outlet来引用这些被挂载的组件。
const StudentList = () => {
return <div>
学生列表
<Outlet/>
</div>
};
Link组件
和版本5的类似,具体区别看视频
NavLink组件
和版本5的类似,具体区别看视频
Navigate
类似于版本5中的Redirect组件,用来跳转页面,具体看视频
部分钩子函数
- useLocation —— 获取地址信息的钩子
- useNavigate —— 获取Navigate对象的钩子
- useParams —— 获取请求参数
- useMatch —— 检查路径是否匹配某个路由
为啥不直接学6版本呢,5版本还需要学吗
6还属于预发布,虽然简化了很多但是功能还不算完整。5虽然有点乱,有点麻烦,但功能相对还是要全一些。还好,都不难。
js基础看的超哥,博客竟然改版了!前来膜拜。
超哥,useNavigate和Navigate都可以实现页面跳转,所以区别就是useNavigate可以用函数进行操作,Navigate组件是默认渲染时就被挂载。可以这么理解吧。
嗯,可以的,功能相同 使用方式不同
感谢社会我超高,超高的课讲得很火爆。麻烦问超哥一个问题:
<Routes>
<Route className=’display’ path=’/robbin/home’ element={<Home />}/>
<Route className=’display’path=’/robbin/about’ element={<About />}/>
<Route className=’display’path=’/robbin/source’ element={<Source />}/>
</Routes>
<Routes>
<Route className=’homebranch’ path=’/robbin/home/news’ element={<News />}/>
<Route className=’homebranch’ path=’/robbin/home/message’ element={<Message />}/>
</Routes>
上面是嵌套路由,结果得到一下错误提示。
You rendered descendant <Routes> (or called
useRoutes()
) at “/robbin/home” (under <Route path=”/robbin/home”>) but the parent route path has no trailing “*”. This means if you navigate deeper, the parent won’t match anymore and therefore the child routes will never render.Please change the parent <Route path=”/robbin/home”> to <Route path=”/robbin/home/*”>
按照错误提示做相应修改后(在home后的路径后加 /*),不再有错误提示,但子路由组件仍然不挂载。没有错误提示,也不挂载组件,这可咋整?
这个东西你这么写就麻烦了,routes没必要整两个,然后路由可以直接嵌套就好了:
李老师,我使用useRoutes配置路由,但是我想对路由进行重定向应该怎么配置啊?比如:首页(/)重定向为/home
可以试试在组件内使用navigate