React路由妈宝级教程:十五分钟,扫盲router6路由相关API!
最近在搭一个新项目的架子,刚好基于react-router6重新设计了一套路由系统。咱也趁着这个机会好好回顾下react-router6的使用。当然,如果篇幅允许,咱就直接把动态菜单、路由鉴权、路由缓存、路由过渡这些个小功能给大家伙唠唠,让xdm对搭建B端后台路由系统这块不再烦恼!
前言
在介绍相关API之前,我们需要先了解下前置知识:
我们在项目里配置路由,其实就是在配置我们url路径与我们要展示的页面组件的对应关系,我们只要按照第三方库的配置规则去映射我们的组件和url即可。
除此之外,我们也要清楚我们项目中所使用的路由一般都包含两种模式:history模式和hash模式。这两者很好区分:如果我们在地址栏发现url里包含 # 那这个项目一定是用hash模式无疑了;反之,那就是用的history模式。
需要注意的是,当我们选定使用history模式时,会出现部署到服务端后,刷新子路由报404的问题,解决方法也很简单:改用hash模式或者配置Nginx(怎么配咱们先按下不表,后续有需要咱会专门出一篇文章来说明)。好了,话不多说,咱这就开始介绍react-router6相关的API。
一、新手上路-router6初使用
下载安装:
npm i react-router-dom
1.1 前置工作:选定模式
在react-router6包含两种路由:BrowserRouter和HashRouter,BrowserRouter使用history模式;而HashRouter从字面上就能看出它使用的是hash模式。
我们在配置路由之前,首先要做的是选定当前项目的路由模式 BrowserRouter 或 HashRouter。一旦选定后,我们要用此组件去包裹整个项目的根组件(一般是App组件)如下所示:
import { BrowserRouter } from 'react-router-dom';
...
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
)
...
1.2 中期准备:引入包裹路由映射的容器
我们都知道,路由的本质就是映射关系。而在我们router6中,用 Routes 组件(就是router5里的Switch组件)去盛放这层路由映射关系。
import { Routes } from "react-router-dom";
const App =()=>{
return (
<div>
<Routes>
路由映射
</Routes>>
</div>>
)
}
1.3 落地实行:配置映射关系
选定了路由模式,引入了包裹容器,接下来的工作就是匹配我们url路径与页面展示组件的关系了。router6中给我们提供了 Route 组件,专门用来帮助我们进行配置路由关系。Route属性如下所示:
-
path属性:用于设置匹配到的路径;
-
element属性:设置匹配到路径后要渲染的组件;(在Router5里是使用component属性)
在router6中,我们不必再特别去设置exact属性去进行精确匹配组件了,因为router6中已经帮我们内置进去了。到这里我们其实就能配置出一个简单的路由系统了。下面我们就来调整一下App组件里的内容:
import { Routes, Route } from "react-router-dom";
import Home from "@/pages/home/home";
import Login from "@/pages/login/login";
import NotFound from "@/components/404/notFound";
import "./index.less";
function App() {
return (
<div className="App">
<div className="header">这是固定不变的header,下面是会改变的content</div>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
);
}
export default App;
好了,我们完善代码后就可以在地址进行测试我们的路由配置关系了:
1.4 路由跳转:Link和NavLink
在上面一part中,我们已经配置了一个简单的路由映射关系。但有小伙伴发现了问题,我们在进行跳转时都是手动去地址栏更改路径从而更换我们要展示的页面内容。这种方式在实际开发中显示是不现实的。对此,router6给我们提供了Link 和 NavLink 组件(这些组件最终都会被渲染成a元素)来帮助我们进行路由跳转。
属性介绍:
这两个组件主要包含两个属性:to属性和replace属性。
-
to属性: 用来设置跳转到哪个路径,相当于是push操作;
-
replace属性:和to类似,也会跳转到目标路径,但其执行的是replace操作;
Link组件代码实操:
知道了组件的内部属性后,下面我们就来编写一段代码再深入感受下Link组件。
import { useEffect } from "react";
import { Routes, Route, Link } from "react-router-dom";
import Home from "@/pages/home/home";
import Login from "@/pages/login/login";
import NotFound from "@/components/404/notFound";
import "./index.less";
function App() {
return (
<div className="App">
<div className="jumpBtn">
<Link to="/">跳转Home</Link>
<Link to="/login">
跳转Login
</Link>
<Link to="/abc" replace={true}>
跳转notFound
</Link>
</div>
<div className="header">这是固定不变的header,下面是会改变的content</div>
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
);
}
export default App;
to属性 和 replace属性 区别:
跳转notFound时,我们设置了replace属性,也就是说我们/abc路径会把路由栈里当前路由替换掉。对应这里的操作是:我们依次点击跳转 Home --> 跳转Login --> 跳转notFound ; 路由栈里的变化是 / --> /login --> /abc 但由于我们设置跳转notFound的replace属性为true,所以实际上路由栈的变化是 / --> /login (当点击跳转notFound时 /login 被替换成了 /abc )
这里咱又重申了to 和 replace的区别,目的是在于给xdm加深下印象,咱不能在项目里无脑push或者to,因为有些情况是需要用replace来代替当前路由路径的。
NavLink组件介绍:
介绍完了Link组件,接下来我们就来介绍下NavLink,其实router6里的NavLink就是Link组件的样式增强版,它与Link的用法基本,只不过就是多了几个属性可以让我们进行设置样式,并且当前选中的NavLink组件上会多出一个active类名:
-
style 属性:接收一个函数,函数接收一个对象,包含isActive属性,表示当前是否被选中;
-
className 属性:接收一个函数,函数接收一个对象,包含isActive属性,表示当前是否被选中;
好了,既然清楚了NavLink的额外属性后,现在我们来实现一个小需求:当选中标签时,标签的文字要显示成蓝色!
现在我们App组件里的Link部分替换成NavLink:
...
<div className="jumpBtn">
<NavLink to="/"> 跳转Home </NavLink>
<NavLink to="/login" > 跳转Login </NavLink>
<NavLink to="/abc" > 跳转notFound </NavLink>
</div>
...
1. 利用style/className属性实现:
...
<div className="jumpBtn">
<NavLink to="/"
style={({ isActive }) => {
return isActive ? { color: "blue" } : {};
}}
> 跳转Home </NavLink>
<NavLink to="/login"
className={({ isActive }) => {
return isActive ? "linkActive" : "";
}}
> 跳转Login </NavLink>
<NavLink to="/abc"
style={({ isActive }) => {
return isActive ? { color: "blue" } : {};
}}
> 跳转notFound </NavLink>
</div>
...
2. 利用active类名实现
其实我们也可以利用NavLink选中时类名为active这个特性来实现:
这是app文件里相关组件内容:
import style from "./index.module.less";
...
<div className={style.App}>
...
<div className="jumpBtn">
<NavLink to="/">跳转Home</NavLink>
<NavLink to="/login"> 跳转Login</NavLink>
<NavLink to="/abc" >跳转notFound</NavLink>
</div>
...
</div>
...
然后在index.module.less 文件中这么写即可:
.App{
:global{
.active{
color: blue;
}
}
}
至此,我们就实现了当选中标签时,标签的文字要显示成蓝色这个需求,如下图示:
1.5 路由重定向:Navigate
我们在router6中使用Navigate 组件来实现路由的重定向,只要这个组件出现,就会执行重定向跳转,并跳到其对应的to路径中。
使用Navigate也很简单,只要将想要被重定向的Route匹配关系中将element属性里的原组件替换成Navigate组件即可;至于我们想要被重定向到哪个路径,只需要将这个路径写入到我们Navigate组件中的to属性即可!
属性介绍:
-
to属性: Navigate 的核心属性,重定向后将要跳转到的目标路径,在路由栈中的表现也是push操作;
-
replace:重定向后,用to属性里的路径值来替换掉当前路由栈栈顶的路径值,在路由栈中的表现是replace操作;
代码演练:
好了,既然咱已经了解了Navigate组件的使用方法,下面就来写段代码实操一下:
对之前代码的Routes部分简单做些修改,增加重定向功能
...
<div className="content">
<Routes>
<Route path="/" element={<Navigate to="/home" />} />
<Route path="/home" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
...
上面这段代码就实现了当我们访问 '/' 时,页面被重定向到 路径 '/home' 并且展示的是 Home 组件。
实际案例:登录权限控制
老观众可能都感觉到了,咱一般介绍完基本使用后,都会引入一个实际案例来去帮助各位巩固一下知识,这把也不例外。好了,产品经理不请自来,让我们实现一个小需求:针对于我们的Home页面只有在登录状态下才能访问,如果没有登录则重定向到登录页中。这个案例在我们实际开发中简直是再正常不过了,咱话不多说,直接冲!
一般来说针对于这些权限控制咱免不了要引入高阶函数或者高阶组件来帮助我们进行一层拦截:在跳转页面时不要直接进行render我们的目标组件,而是做一层判断-->如果有访问权限,咱就直接render;如果没有访问权限,咱就使用Navigate重定向组件。好了,聊到这里,咱们的代码逻辑就已经呼之欲出了。该说不说,it's your show time!
编写高阶组件:AuthComp.jsx(这里也可以直接用高阶函数)
import React from "react";
import { Navigate } from "react-router-dom";
// 这里的isLogin就是用来标识是否登录,实际项目一般是取redux里面的相关值
const isLogin = false;
const AuthComp = (props) => {
const { comp:Comp } = props;
// 如果登录就放行
if (isLogin) return Comp;
// 如果没有登录就重定向到Login登录页
return <Navigate to="/login" replace/>;
};
export default AuthComp;
调整匹配关系:高阶组件包裹权限组件
高阶组件的逻辑编写完成后,接下来我们只需要调整下App组件里的匹配关系,用高阶组件包裹我们有登录权限控制的目标组件即可。这里我们把Home组件用AuthComp组件包裹进行登录权限控制,如下所示:
...
<div className="content">
<Routes>
<Route path="/" element={<AuthComp comp={<Home />} />} />
<Route path="/login" element={<Login />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
...
至此,我们就完成了对Home也得登录权限控制。在实际开发中,我们判断当前是否有权限的值一般都是在redux等状态管理库中存放的,到时候根据业务需要更改即可!
二、初露锋芒-router6进阶应用
2.1 加载优化:路由懒加载
2.1.1 问题引申
在进行路由懒加载之前我们需要先提出两个问题:什么是路由懒加载?为什么要用路由懒加载?
answer:
-
路由懒加载的概念:懒加载就是延迟加载,也即在需要的时候才会进行加载所需组件。
-
使用路由懒加载的原因:我们在路由中通常会定义很多不同的页面。如果不应用懒加载的话,很多页面都会打包到同一个js文件中,文件将会异常的大。造成进入首页时,需要加载的内容过多,时间过长,在浏览器中可能会出现短暂的空白页,从而降低用户体验,而运用路由懒加载是将各个模块分开打包,用户查看的时候再加载对应的模块,减少加载用时。
一句话总结是: 一开始进入页面时不需要一次性把资源都加载完,需要时再加载对应的资源。
懒加载作用:1、 延迟加载; 2、 分包;3、添加加载过渡;
使用方法:我们用的是esm的用法即 import(路径) 。
2.1.2 react中使用懒加载
在react中我们借助React.lazy方法来进行懒加载,用法如下:
React.lazy(() => import("懒加载组件的路径"))
下面我们对项目代码做下调整,将直接 import 导入的Home等组件采用lazy懒加载的形式开启路由懒加载:
import React, { useEffect, Suspense } from "react";
import { Routes, Route, Link, NavLink, Navigate } from "react-router-dom";
import AuthComp from "@/components/authComp/AuthComp";
import style from "./index.module.less";
const Home = React.lazy(() => import("@/pages/home/home"));
const Login = React.lazy(() => import("@/pages/login/login"));
const NotFound = React.lazy(() => import("@/components/404/notFound"));
function App() {
useEffect(() => {
console.log("pro>>", process.env);
}, []);
return (
<div className={style.App}>
<div className="jumpBtn">
<NavLink to="/">跳转Home</NavLink>
<NavLink to="/login">跳转Login</NavLink>
<NavLink to="/abc">跳转notFound</NavLink>
</div>
<div className="header">这是固定不变的header,下面是会改变的content</div>
<div className="content">
<Routes>
<Route path="/" element={<AuthComp comp={<Home />} />} />
<Route path="/login" element={<Login />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
);
}
export default App;
2.6.2 suspense 组件的引入
当我们在上一part完成对引入组件的懒加载改造后,再点击路径跳转,控制台会报如下错误:
这是因为当我们是用lazy去懒加载时,应该同时引入Suspense去包裹懒加载的组件。也就是说,lazy处理的懒加载组件一定要处于Suspense组件包裹下。
下面我们就来引入Suspense组件并包裹App根组件来解决这个报错。
当我们引入Suspense之后,再去点击跳转控制台就不会再报错了。但此时我们又发现一个问题,当网络不好的时候,我们进行页面跳转,会出现长时间的白屏现象:
出现这种问题的原因在于:我们网速不好的情况下,加载项目资源的速度会受影响,此时只有等我们要展示的页面资源加载完成后才会显示页面。所以在等待加载资源的这一过程中,我们会发现网页白屏的现象。
既然知道了问题产生的原因,那我们就很好对症下药了:1、减少页面资源;2、增加加载资源时的 loading 效果来改善用户体验。这里咱选择方案2,利用Suspense组件的fallback属性来进行改善用户体感。
fallback属性:fallback是Suspense组件的属性,其作用是当lazy处理的懒加载组件还没有加载出来时要显示的内容。一般地,我们都会往fallback属性里传入一个loading动画,用来缓解加载白屏的问题。
这里我们将fallback的值设置为antd的Spin组件:
<React.Suspense fallback={
<Spin
size='large'
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
/>
}>
<BrowserRouter>
<App />
</BrowserRouter>
</React.Suspense>
效果如下:
2.6.3 Suspense 组件包裹层级的选择
我们在上一part中容易知道,Suspense组件只要能包裹lazy处理的组件即可。那问题来了,我Suspense组件包裹的层级有没有具体的要求呢?我们来拿上面的代码举例:我们的Suspense是包裹整个App组件的,从gif图我们也看到了,当组件处于加载状态时,我们fallback组件是替换了整个页面了,此时我们页面只显示了Spin组件。但实际上,如下图中的部分在路由切换的时候是不应该被Suspense的fallback替代的,因为我路由切换时,这部分是不变的。
而造成此部分也被替换的原因是,Suspense组件包裹的范围太广了。此处我们包裹了App组件,这也就代表着只要App组件内部发生加载组件的情况,我们整个App组件都会暂时被fallback替换掉。但按照正常逻辑,我们只想要动态加载的那部分被替换,其余不变的部分就仍然展示。对此,我们需要缩小Suspense组件的包裹范围,只让它包裹住我们懒加载的组件即可。
这时候我们的老演员高阶函数有上场了,这里我们封装lazyLoad,用来配合处理我们的懒加载组件:
import React from "react";
import { Spin } from "antd";
const lazyLoad = (Comp) => {
return (
<React.Suspense
fallback={
<Spin
size="large"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
/>
}
>
<Comp />
</React.Suspense>
);
};
export default lazyLoad;
删除包裹App组件的Suspense组件:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { Spin } from 'antd'
import App from './App';
import './assets/style/global.css'
import 'antd/dist/reset.css'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
#将原本此处的React.Suspense删除
<BrowserRouter>
<App />
</BrowserRouter>
);
调整App.jsx文件内的相关组件用法,这里我们只需要将原来element属性里的组件作为参数传给lazyLoad函数即可,其他的保持原样:
<div className="content">
<Routes>
<Route path="/" element={<AuthComp comp={lazyLoad(Home)} />} />
<Route path="/login" element={lazyLoad(Login)} />
<Route path="*" element={lazyLoad(NotFound)} />
</Routes>
</div>
如上图所示,调整过后,我们只有懒加载的组件部分会被fallback替换,上面不变的部分就不会被加载组件替换掉了!
至此,我们路由懒加载部分就差不多进入尾声了,但这里咱要补充说明一下:React.lazy 和 React.Suspense 是React 16.6+ 版本给我们提供的组件懒加载能力。并且,它们并不支持服务端渲染(SSR)。如果想在低版本的react或者使用ssr的情况下开启懒加载,咱可以考虑借助 react-loadable、react-lazyload 等第三方库。
2.2 路由匹配规则:路由通配符
在新手上路那一part其实我们就已经见过了路由里的一个通配符 * 这里我们再详细去介绍下router里面其他的通配符。
在router6中,支持如下的几种通配符。了解这些通配符的使用后,有助于帮助咱理解当前路径与页面的匹配关系:
-
/xxx
确定的路径名,如 : /home 表示home页面组件能匹配上路径只能是 /home ; -
/xxx/:xxx
动态路径名,:xxx会动态变化的 。如:/home/:id 表示home页面能匹配上 /home/11、/home/22、/home/abc、/home/xxx 等路径; -
/xxx/:xxx/xxx
动态路径名后跟确定路径,如: /home/:id/abc 表示home页面能匹配上 /home/11/abc、/home/22/abc 、/home/aa/abc 等路径; -
/xxx/*
确定路径名,后面可以跟多个子路径,如:/home/* 表示home页面能匹配上 /home、/home/12、/home/ab、/home/cd/123 等路径; -
/xxx/:xxx/*
动态路径名后不限制子路径,如:/home/:id/* 表示home页面匹配 /home/11/abc/bcd、 /home/22/qwe 等路径;
2.3 嵌套路由:开发常用
在实际开发中,尤其是开发后台系统中,我们常见的布局如下所示:
一般的,我们的header部分和侧边栏都是不会随路径的变化而发生变化,只有主要内容区发生变化。而实现我们的这种需求就需要用到路由嵌套,即我们进行路径映射的组件要被嵌套在主要内容区。
2.3.1 结构调整
为了配合我们进行路由嵌套的讲解,现在我们对项目的结构进行调整: 整体布局按照上图的布局进行并取名为Layout组件,login和notFound组件不用改变。
在pages目录下新建layout文件夹并创建相关组件:
Layout.jsx文件:
import React from "react";
import { NavLink } from "react-router-dom";
import style from "./layout.module.less";
const Layout = () => {
return (
<div className={style.layout_wrap}>
<div className={style.header}>
<NavLink to="/">跳转Home</NavLink>
<NavLink to="/login">跳转Login</NavLink>
<NavLink to="/abc">跳转notFound</NavLink>
</div>
<div className={style.container_wrap}>
<div className={style.side_tab}>侧边栏</div>
<div className={style.container_content}>主要内容区</div>
</div>
</div>
);
};
export default Layout;
layout.module.less文件:
.layout_wrap{
color: #fff;
.header{
background-color: rgb(231, 174, 174);
height: 60px;
}
.container_wrap{
height: calc(100vh - 60px);
display: flex;
.side_tab{
width: 60px;
background-color: rgb(6, 39, 90);
}
.container_content{
flex:1;
background-color: rgb(6, 90, 45);
text-align: center;
}
}
}
调整App.jsx 文件:
import React, { useEffect, Suspense } from "react";
import { Routes, Route, Link, NavLink, Navigate } from "react-router-dom";
import AuthComp from "@/components/authComp/AuthComp";
import lazyLoad from "@/components/lazyLoad";
import style from "./index.module.less";
const Layout = React.lazy(() => import("@/pages/layout/Layout"));
const Home = React.lazy(() => import("@/pages/home/home"));
const Login = React.lazy(() => import("@/pages/login/login"));
const NotFound = React.lazy(() => import("@/components/404/notFound"));
function App() {
return (
<div className={style.App}>
<div className="content">
<Routes>
<Route path="/" element={<AuthComp comp={lazyLoad(Layout)} />} />
<Route path="/login" element={lazyLoad(Login)} />
<Route path="*" element={lazyLoad(NotFound)} />
</Routes>
</div>
</div>
);
}
export default App;
修改home.jsx文件:
import React from "react";
import style from "./index.module.less";
const Home = () => {
return <div className={style.home_wrap}>这是home页</div>;
};
export default Home;
取消Layout组件的懒加载
从上图我们发现layout的样式有时没有生效(原因待确认)。此时改下App文件,不要让Layout组件进行懒加载!
...
注意,这里要对应直接引入,还用React.lazy的方式会报错的,因为lazy和Suspense成对出现!
import Layout from "@/pages/layout/Layout";
...
<div className="content">
<Routes>
<Route path="/" element={<AuthComp comp={<Layout />} />
<Route path="/login" element={lazyLoad(Login)} />
<Route path="*" element={lazyLoad(NotFound)} />
</Routes>
</div>
2.3.2 进行嵌套
现在我们有这么个需求:要求我们的home组件和testContent组件是嵌套在Layout组件中的。并且,当我们的路径为 '/' 时,应该展示的是home组件。好了,现在开始我们的代码实现:
新增testContent组件:
TestContent.jsx
import React from "react";
import style from "./testContent.module.less";
const TestContent = () => {
return <div className={style.wrap}>这是testContent内容区</div>;
};
export default TestContent;
调整App.jsx文件,嵌套相关组件:
现在我们找到Layout组件所在的Route组件,并在其内部新增两个Route组件分别配置Home和TestContent的路由匹配关系。
App.jsx
import React, { useEffect } from "react";
import { Routes, Route } from "react-router-dom";
import AuthComp from "@/components/authComp/AuthComp";
import Layout from "@/pages/layout/Layout";
import lazyLoad from "@/components/lazyLoad";
import style from "./index.module.less";
const Home = React.lazy(() => import("@/pages/home/home"));
const TestContent = React.lazy(() => import("@/pages/testContent/TestContent"));
const Login = React.lazy(() => import("@/pages/login/login"));
const NotFound = React.lazy(() => import("@/components/404/notFound"));
function App() {
return (
<div className={style.App}>
<div className="content">
<Routes>
<Route path="/" element={<AuthComp comp={<Layout />} />}>
<Route path="/home" element={lazyLoad(Home)} />
<Route path="/testContent" element={lazyLoad(TestContent)} />
</Route>
<Route path="/login" element={lazyLoad(Login)} />
<Route path="*" element={lazyLoad(NotFound)} />
</Routes>
</div>
</div>
);
}
export default App;
Outlet组件:在父路由元素中作为子路由的占位元素
在我们上面的步骤操作完成之后,当访问testContent路径时,我们预想的testContent页面并没有出现。
原因分析:因为我们的子路由没有在父路由进行占位! Router6.X 修改了嵌套规则,我们之前在Router5中是将嵌套的子路由直接写在父路由对应的组件内部的。这样有个弊端就是Route太分散了,各个组件里都有Route。我们Router6.X对此做了一个调整,现在我们不用分散嵌套子路由,取而代之的是路由嵌套统一在一个 Routes组件下维护,如下图所示:
但除此之外,我们想要子路由生效,必须还得在其所属的父路由里引入一个Outlet组件进行路由占位!如下所示:
当然,Outlet组件的放置位置也是有讲究的,我们想要让子路由在哪个盒子里展示,就把Outlet放到其内部即可。上面的代码就表明,当匹配到我们的子路由时,子路由对应的组件就会在Outlet组件所占的位置展示!
涉及路由嵌套时的权限管理
我们在上文中封装了一个AuthComp 来进行登录权限认证,即如果没有登录,用户访问首页时,会直接重定向到login登录页。那现在问题来了,我们没有进行路由嵌套之前是一个Home组件,那么我们直接用AuthComp包裹此Home组件即可,那么现在一旦涉及到路由嵌套我们又该怎么做呢?比如,我们现在的代码:
<div className="content">
<Routes>
{/* 父路由 */}
<Route path="/" element={<Layout />} />}>
{/* 子路由 */}
<Route index element={lazyLoad(Home)} />
<Route path="/testContent" element={lazyLoad(TestContent)} />
</Route>
<Route path="/login" element={lazyLoad(Login)} />
<Route path="*" element={lazyLoad(NotFound)} />
</Routes>
</div>
如果现在我们的layout组件需要登录认证,并且Home、TestContent同样也需要登录认证。而Home、TestContent作为layout组件的子路由仍然需要用AuthComp组件包裹吗?
答案显然是不用的,在路由嵌套中如果我们涉及到权限认证,遵循的原则:
-
父路由需要权限认证,则其包裹的子路由都会进行权限认证。(也就是说,我们只要在父路由包裹AuthComp即可);
-
子路由需要权限认证,则只要在那一个子路由上包裹AuthComp即可。(因为我们一旦在父路由上包裹了AuthComp,则一但权限不通过,则该父路由下所有的子路由都进不去!)
代码如下:
<div className="content">
<Routes>
{/* 父路由 */}
<Route path="/" element={<AuthComp comp={<Layout />} />}>
{/* 子路由 */}
<Route path="/home" element={lazyLoad(Home)} />
<Route path="/testContent" element={lazyLoad(TestContent)} />
</Route>
<Route path="/login" element={lazyLoad(Login)} />
<Route path="*" element={lazyLoad(NotFound)} />
</Routes>
</div>
下面我们来看两张图来加深下印象:
AuthComp里权限放开:
AuthComp里权限不通过:
从上面两张图我们可以看出,Layout整个路由都需要权限认证,我们不必在Home和TestContent组件里也用AuthComp包裹,只将作为父路由的Layout包裹即可。在权限不过的情况下,我们访问子路由/home 、 /testContent 也会重定向到login组件。
如何设置默认显示的子路由(这里用重定向也可以)
上面我们说明了如何进行路由嵌套,以及路由嵌套涉及权限认证该怎么做。不过还有一个小细节没有搞定:我们发现,当我们访问 '/' 路径时,页面是下图这种:
但实际在我们开发中,我的layout都要默认展示一个首页页面的,也即对应到本项目里是要默认展示home组件的。目前我们只做到了点击跳转Home,或者访问 /home 才会出现Home组件:
这明显是不合适,现在我要求当我访问 '/' 时,就要显示home 组件。也就是说我要给路由嵌套的父路由设置一个默认展示的子路由!
方法: 我们只需要在想要被设为默认展示的子路由上写一个 index属性即可(注意,被设置index的子路由不能再设置path属性),那么当我们访问 '/' 就能做到默认展示子路由home了。
<div className="content">
<Routes>
{/* 父路由 */}
<Route path="/" element={<AuthComp comp={<Layout />} />}>
{/* 子路由 */}
// 我们将Home组件设为默认展示的子路由,注意,有index就不能设置path属性
<Route index element={lazyLoad(Home)} />
<Route path="/testContent" element={lazyLoad(TestContent)} />
</Route>
<Route path="/login" element={lazyLoad(Login)} />
<Route path="*" element={lazyLoad(NotFound)} />
</Routes>
</div>
相应的,我们跳转Home按钮也要做调整:
<div className={style.header}>
<NavLink to="/">跳转Home</NavLink>
...
子路由路径名的设置规则
我们在进行路由嵌套时,子路由的path路径要以父路由的path名开头,否则会报如下错误:
<div className="content">
<Routes>
<Route path="/" element={<AuthComp comp={<Layout />} />}>
<Route index element={lazyLoad(Home)} />
<Route path="/testContent" element={lazyLoad(TestContent)}>
<Route path="/notFound" element={lazyLoad(NotFound)} />
</Route >
</Route>
<Route path="/login" element={lazyLoad(Login)} />
<Route path="*" element={lazyLoad(NotFound)} />
</Routes>
</div>
报错信息解释的很清楚了,我们子路由的path要以它的父路由的path名开头!!正确的代码如下:
...
<Route path="/testContent" element={lazyLoad(TestContent)}>
<Route path="/testContent/notFound" element={lazyLoad(NotFound)} />
</Route >
...
三、牛刀小试-router6传参及配置化
在上面部分我们介绍了路由嵌套、重定向、路由跳转等知识。但有细心的小伙伴可能就有个疑问,上面部分中路由跳转都是用NavLink、Link这类组件进行组件触发跳转的。
但在我们实际项目中会有一些手动跳转的逻辑,比如:当我们跳进一个页面后,处理完一些耗时逻辑再去跳到另一个页面。虽然我们也能通过操作Link组件的方式模拟点击跳转,但实际不用这么麻烦,react-router6给我们提供了通过js代码进行跳转的API:useNavigate。
3.1 useNavigate:路由跳转
如果我们希望通过JS代码跳转,需要通过useNavigate的Hook获取到navigate对象进行操作,我们函数式组件里可以直接使用这个钩子去获取navigate对象然后进行后续操作;
但如果咱是使用类组件开发则需要封装高阶函数,也即手动封装withRouter。如下所示:
import { useNavigate } from 'react-router-dom';
export default function withRouter(WrapperComponent){
return props=>{
const navigate=useNavigate()
return <WrapperComponent {...props} router={{navigate}}/>
}
}
使用:
class TestDemo extends Component{
constructor(props) {
super(props);
this.props.router....
}
render() {
return (
<div>test</div>
)
}
}
export default withRouter(TestDemo)
3.2 使用方法:
我们可以直接调用 useNavigate 钩子函数去返回一个navigate,然后调用navigate并将跳转路径作为参数传进去,例如:navigate('url路径')。 我们navigate里也可以传第二个options参数: {replace,state} 。replace参数代表替换当前路由栈中的路由,state代表路由传参。
navigate直接写入跳转路径:
我们js路由跳转可以直接在函数参数中传入url路径: navigate("/login");
现在我们又来个小需求:当咱跳到testContent页面时会出现Button按钮,点击按钮后跳到login登录组件。
testContent.jsx:
import React from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "antd";
import style from "./testContent.module.less";
const TestContent = () => {
const navigate = useNavigate()
const jumpToLogin=()=>{
navigate('/login')
}
return (
<div className={style.wrap}>
这是testContent内容区
<br />
<Button onClick={jumpToLogin}>点击跳转登录页</Button>
</div>
);
};
export default TestContent;
navigate参数传入数字
当我们的navigate传入数字时(一般是-1),是跳到路由栈当前路由前面几个对应的路由。
eg : navigate(-1) 这就代表着回退一个页面
我们调整下上面代码中的jumpToLogin函数:
const jumpToLogin=()=>{
navigate(-1)
}
navigate 参数传入第二个options参数
我们的navigate函数还能传入第二个参数来进行一些配置:
...
const navigate = useNavigate()
const jumpToLogin=()=>{
navigate('/login',{replace:true, state:{data:'这是传给新页面的参数'}})
}
...
-
replace属性很好理解,就是我们路由栈里当前路由路径会被跳转的路径替换掉。
-
state属性则是和路由传参有关,state里的数据会被传给我们的新页面,这一部分我们放到路由传参那一part来展开讲讲。
路径以 / 开头与不以 / 开头的区别:
路径如果以/ 开头代表 根路由+当前路径拼接成新路由; 不以 / 开头表示在当前路由后再跟上路径拼接成新路由路径
eg:当前路径为 http://localhost:8888/home,在home组件里分别执行如下跳转:
import { useNavigate } from 'react-router-dom';
...
const navigate=useNavigate()
navigate("/register"); --跳转后路径为 http://localhost:8888/register
navigate("register"); --跳转后路径为 http://localhost:8888/home/register
3.3 路由传参
在我们实际项目中,也常常会用到跳到新页面的同时进行传递参数。一般用到三种方式:① 动态路径传参 ② search参数传递 ③ state传递参数。
3.3.1 动态路径传参
动态路径设置
动态路径传参的概念指的是路由中的路径不会固定:比如 /detail 的这个path对应一个组件Detail;
如果我们将path在Route匹配时写成/detail/:id
那么 /detail/abc 、 /detail/123 都可以匹配到该Route对应的组件,并进行显示;如果不写成/detail/:id
那么/detail/abc 、 /detail/123匹配不上,如果我们配了notFound此时都会落入notFound里。这里我们拿项目里的testContent举例:
不写成动态路径:
<div className="content">
<Routes>
{/* 父路由 */}
<Route path="/" element={<AuthComp comp={<Layout />} />}>
{/* 子路由 */}
<Route index element={lazyLoad(Home)} />
// 这里我们先不写成动态路径
<Route path="/testContent" element={lazyLoad(TestContent)} />
</Route>
<Route path="/login" element={lazyLoad(Login)} />
<Route path="*" element={lazyLoad(NotFound)} />
</Routes>
</div>
调整为动态路径:
注意,我们写成
"/testContent/:id"
动态路径形式时,当我们输入/testContent/123、/testContent/abc是能够匹配上页面的,但是输入/testContent、/testContent/反而匹配不上了
<div className="content">
<Routes>
{/* 父路由 */}
<Route path="/" element={<AuthComp comp={<Layout />} />}>
{/* 子路由 */}
<Route index element={lazyLoad(Home)} />
// 这里我们写成动态路径
<Route path="/testContent/:id" element={lazyLoad(TestContent)} />
</Route>
<Route path="/login" element={lazyLoad(Login)} />
<Route path="*" element={lazyLoad(NotFound)} />
</Routes>
</div>
参数获取
针对动态路径传参的这种方式,我们常用useParams 钩子来进行获取参数。
import { useParams } from "react-router-dom";
...
const params=useParams()
...
下面我们对TestContent组件进行改造
import React, { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Button } from "antd";
import style from "./testContent.module.less";
const TestContent = () => {
const navigate = useNavigate()
const params=useParams()
const jumpToLogin=()=>{
navigate(-1)
}
useEffect(()=>{
console.log('params>>>>',params);
})
return (
<div className={style.wrap}>
这是testContent内容区
<br />
<Button onClick={jumpToLogin}>点击跳转登录页</Button>
</div>
);
};
export default TestContent;
注意,此处之所以params对象里的key是id 是因为我们设置动态路径时是 :id , 如果我们设置的是 :testID ,那么此处的key也就成了testID。
动态路径补充
我们在这一小节着重说下细节问题:
-
如果url里包含多段/xxx/xxx 那么我们一个动态路径:id能匹配上吗?
-
useParams获取的是动态路径的哪段参数。
关于问题1,我们这里举个例子来说明:
如果我们Route里的path如代码所示:那么TestContent组件只能匹配
/testContent/123
、/testContent/abc
之类的路径。/testContent/
、/testContent/123/456
、/testContent/abc/efc
等都无法匹配上!!
<Route path="/testContent/:id" element={lazyLoad(TestContent)} />
如果我们想要匹配上/testContent/123/abc 这种的可以做如下调整:
<Route path="/testContent/:id/:nextId" element={lazyLoad(TestContent)} />
关于问题2,我们就以/testContent/:id/:nextId
这种来举例:
地址栏输入 /testContent/123/abc ,如下图所示,我们的params参数里把path里所有动态路径部分的参数都拿到了。现在我们得出结论,useParams获取的是所有动态变化部分的路径参数。
至此,我们动态路径传参部分就宣告结束了,下一part咱来唠一唠search传参。
3.3.2 search传递参数;
设置search参数
我们上面介绍了路由动态路径传参的方式,这一part我们介绍search参数传值。
seatch 传递参数就是把我们要传递的参数拼接到url上进行传递,具体方式是以?开头,键值对的形式传参,每个参数之间用&连接。eg: http://localhost:8888/list?page=1&pagesize=100 。
navigate('/login?name=ztc&age=18')
现在我们对testContent组件做下调整
import React from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "antd";
import style from "./testContent.module.less";
const TestContent = () => {
const navigate = useNavigate()
const jumpToLogin=()=>{
navigate('/login?name=我是张添财&age=18')
}
return (
<div className={style.wrap}>
这是testContent内容区
<br />
<Button onClick={jumpToLogin}>点击跳转登录页</Button>
</div>
);
};
export default TestContent;
好了,我们search传参部分就结束了,下面我们来聊聊怎么获取search参数。
获取search参数
我们一般使用useSearchParams这个钩子来获取search参数(也可用useLocation,不过得额外再处理下search参数),注意:useSearchParams返回的是个数组,且数组里是一个当前值和set方法。并且我们取值时常借助Object.fromEntries这个方法:
import { useSearchParams } from "react-router-dom";
...
const [searchParams]=useSearchParams()
const params=Object.fromEntries(searchParams)
...
好了,我们对login组件进行改造,获取传进来的search值
import React, { useEffect } from "react";
import { useSearchParams } from "react-router-dom";
const Login = () => {
const [searchParams]=useSearchParams()
const params=Object.fromEntries(searchParams)
useEffect(()=>{
console.log('params>>', params);
},[])
return <div>Login</div>;
};
export default Login;
3.3.3 state传参
上面两种方式的传参都有一个缺点,即我们传递的参数都在url路径上体现了,并且涉及到复杂类型的参数传递就显得很麻烦了。如果我们不想参数在url上面,且想传些对象时就可以采取这种state传参的方式。
参数设置
我们可以在navigate的第二个参数里传入state属性,如下所示
navigate('/login',{state:{name:"张添财",age:18}})
<Link to='/login' state={{name:"张添财",age:18}}>跳转Login页面</Link>
我们对testContent组件进行调整:
...
const jumpToLogin=()=>{
navigate('/login',{state:{name:"张添财",age:18}})
}
...
获取state参数
我们使用useLocation钩子来获取state参数,用法如下:
import { useLocation } from "react-router-dom";
const state=useLocation()
好了,我们现在对login组件做下调整:
import React, { useEffect } from "react";
import { useLocation } from "react-router-dom";
const Login = () => {
const state=useLocation()
useEffect(()=>{
console.log('state>>', state);
},[])
return <div>Login</div>;
};
export default Login;
进行到这里,我们就已经掌握了router6里面所有的路由传参方式,妈妈再也不用担心咱不会获取路由参数里的值了。
3.4 useRoutes: 配置化路由
我们在上文中都是用Route组件的方式来实现路由配置的,这种方式也不能算不好。但现在都提倡配置化,咱能不能把routes相关的配置都维护到一串数据里,不用以组件-属性这种方式管理呢?
既然咱敢这么问,那答案必然是可以的。咱还记得大明湖畔router5中,引入的react-router-config吗?曾几何时,咱想配置化路由还得引入这个三方库来帮我们维护。但在Router6,咱腰板硬起来了!官方给我们提供了useRoutes这个钩子函数。凭此,咱可以将routes对象以参数形式传入useRoutes中,此钩子会根据routes里的匹配关系对应渲染成相关的路由组件。
3.4.1 routes配置
首先我们要清楚routes配置,routes其本身是一个数组,数组里维护各个匹配关系的对象。如下所示:
const routes=[{ ...匹配关系},{...匹配关系},{...匹配关系}]
其次,匹配关系这个对象里一般都有path、element、children、index属性。
-
path属性就是组件对应的路径;
-
element属性就是要对应的组件;
-
index属性就是默认要展示的页面组件;
-
children属性是路由嵌套时需要用也是一个数组,children数组里的属性和外层一样,子路由的配置就是在children属性里维护的。
好了,下面我们把原来Route形式的路由匹配关系换成用routes配置来表示:
import React from 'react';
import AuthComp from "@/components/authComp/AuthComp";
import Layout from '@/pages/layout/Layout';
import lazyLoad from "@/components/lazyLoad";
const Home = lazyLoad(React.lazy(() => import("@/pages/home/home")));
const TestContent = lazyLoad(React.lazy(() => import("@/pages/testContent/TestContent")));
const Login = lazyLoad(React.lazy(() => import("@/pages/login/login")));
const NotFound = lazyLoad(React.lazy(() => import("@/components/404/notFound")));
const routes=[
{
path:'/',
element:<AuthComp comp={<Layout/>}/>,
children: [
{
index: true,
element: Home,
},
{
path:"/testContent",
element: TestContent,
},
]
},
{
path:'/login',
element:Login,
},
{
path:'*',
element:NotFound,
},
]
export default routes
3.4.2 useRoutes的使用
我们routes对象维护好之后,将routes传到useRoutes即可。下面我们对App.jsx 文件做下调整,将原来整个Routes部分用useRoutes钩子替换掉。
import React, { useEffect } from "react";
import { Routes, Route,useRoutes } from "react-router-dom";
import routes from '@/router'
import style from "./index.module.less";
function App() {
return (
<div className={style.App}>
<div className="content">
{useRoutes(routes)}
</div>
</div>
);
}
export default App;
3.4.3 useRoutes的rerender问题
我们换成useRoutes方式来进行管理路由后,有细心的小伙伴可能会发现一个问题:每当我们切换路由时,useRoutes所在的组件都会进行一次rerender。而我们使用Routes组件的方式则不会出现这种rerender的问题,为方便查看是否rerender,我们在App.jsx文件的加一个console来验证。
...
function App() {
console.log('app组件rerender了>>>>>');
...
Routes组件的方式:
我们看到log只在初始化打印了一次,切换路由时没有进行rerender。
useRoutes配置化路由:
...
<div className="content">
{useRoutes(routes)}
</div>
...
我们发现app组件会随着我们路由的切换而进行rerender。这是什么原因呢?
想知道答案,咱还得看看useRoutes是怎么实现的。我们直接在项目里node-modules中找到react-router(react-router-dom 也是用的这里暴露出来的钩子 ),并在此文件夹里找到.development.js 文件看下router的源码
我们找到useRoutes之后发现,此函数里竟然有三个useContext。我们都知道,Context有个特性,只要其共享的value有一个变化了,都会触发相关Consumer子组件的变化。
但我们再去仔细看这些相关的context对象发现这些共享出来value值并不主要是引起这里rerender的原因,既然这样,我们接着往下看:
终于,我们看到了这个钩子,再去找此钩子里的代码时发现了React.useContext(LocationContext).location
这么一段代码。顺着LocationContext再往下找我们就不难发现为什么会rerender了。由于切路由时在LocationContext共享出来的value值发生了变化,从而使得我们使用到这个context的组件触发了rerender。这也就造成了我们使用useRoutes的App组件在切换路由时重新render了!
至此,我们router6相关API的介绍就到此结束了。但是本篇介绍的只是路由应用的一部分,咱 动态菜单、路由鉴权、路由缓存、路由过渡 等部分还没有去阐述。不过各位小伙伴不要着急,添财的架子里都有这些功能的应用。到时,咱会一一进行阐述router6的各种关联业务功能。各位小伙伴敬请期待,咱下次见!
转载自:https://juejin.cn/post/7203156544878542904