React路由管理
react-router-dom
react-router-dom
是PC端处理路由的解决方法,与之对应的还有移动端的路由适配处理。当代前端开发主要以SPA单页面开发为主。那么单页面和多页面的区别是什么?如图,其中SPA不适用于SEO优化,因为大部分SPA都是基于客户端渲染,也就是js动态向服务器获取数据渲染页面,这就会导致查看源代码的时候缺少很多信息,而SEO优化恰恰是需要从源代码中获取部分信息来优化。但是还有一种服务端渲染,在服务端处理的时候就将整个页面数据全部渲染完成直接返回给前端,而前端直接使用这个页面即可,在这个页面中包含了几乎所有的源代码信息,因此利于SEO优化。
哈希路由
哈希路由每一次路由跳转,都是改变当前页面的hash值,主页面是不会渲染,只针对部分内容进行局部刷新,同时监听hashchange事件,渲染不同的内容。
模拟实现哈希路由,以下是一个静态结构,每一个a标签的href属性对应一个hash路由。每次点击的时候页面的路径会变化但是页面不会重新渲染。
<nav class="nav-box">
<a href="#/">首页</a>
<a href="#/goods">商品</a>
<a href="#/center">个人中心</a>
</nav>
<div class="view-box">
<!-- 不同哈希值路由渲染不同的内容 -->
</div>
搭建基本的路由匹配机制
const viewBox = document.querySelector('.view-box')
// 创建路由表,根据页面hash值的变化去路由表中匹配对应的内容渲染显示
const routes = [
{
path: '/', //匹配的hash值
component: '首页渲染' //渲染的组件或内容
},
{
path: '/goods',
component: '商品渲染'
},
{
path: '/center',
component: '个人渲染'
}
]
location.hash = '/' //默认打开hash地址:#/,即首页的路径
location.hash
如果直接调用则获取的是当前页面的hash值,返回带有#
符号的地址,location.hash=''
如果采样这种形式则是赋值当前页面的hash值,设置的时候不用添加#
,因为会默认在首部添加hash符号。
根据hash地址匹配不同的组件渲染
function hashHandle() {
let hash = location.hash.substring(1) //获取hash地址,#/url需要截取从第二个字符开始的路由进行匹配
routes.forEach(item => {
if (item.path === hash) {
viewBox.innerHTML = item.component //渲染不同hash对应的组件
}
})
}
hashHandle() //默认执行一次
window.addEventListener("hashchange", hashHandle) //hash地址改变的时候执行一次
history路由
和hash路由一致,也是路由更新,但是整个页面不会刷新。使用的是H5提供的新内置对象histroy
,如图,这些方法和vue中路由的使用基本一致。location.pathname
用于获取当前url路径,以/
起头。
修改a标签的时候需要注意,直接这么写,标签进行跳转,会造成页面整个刷新,由于没有当前页面所以报错。
const navBox = document.querySelector('.nav-box')
function historyHandle() {
let history = location.pathname
routes.forEach(item => {
if (item.path === history) {
viewBox.innerHTML = item.component
}
})
}
history.pushState({}, '', '/') //手动跳转主页,顺序一定要在函数调用之前修改,否则路径不对
historyHandle()
navBox.addEventListener('click', e => {
let target = e.target
if (target.tagName === 'A') {
e.preventDefault() //先阻止链接的默认跳转行为
history.pushState({}, '', target.href) //获取当前标签的href属性
historyHandle()
}
})
扩展:popstate
事件当地址变化的时候会触发,调用 history.pushState()
或者 history.replaceState()
不会触发 popstate 事件
react-router-dom V5
安装第五版本:npm i react-router-dom@5.3.4
搭建如图所示的三个组件,通过路由渲染
在react-router-dom
中提供了需要用到的组件如:HashRouter
,BrowserRouter
,前者代表开始hash路由,后者代表开启浏览器history模式。这两个标签的使用规则都一致,将要渲染的内容整体包裹起来,且开启路由模式后,如果是HashRouter
,默认会配置一个 #/哈希值
。像Route
和Link
组件都需要在HashRouter
,BrowserRouter
两个路由模式中才能使用,否则会直接报错。
在代码中,如果还是基于a标签实现路由跳转,那么需要针对不同的路由模式设置不同的href值(如#
)。因此react-router-dom
提供了Link
组件标签使用,该标签必须先开启路由模式才能使用,即外部已经有HashRouter
,BrowserRouter
组件处理。Link
组件标签能够自动识别开启的路由模式,设置不同的模式,其底层是基于a标签实现的。
Link
组件标签中通过设置to
属性的值实现路由跳转,这里直接写具体路径即可,会自动设置不同路由模式需要的规则。
<Link to="/">A</Link>
<Link to='/b'>B</Link>
<Link to='/c'>C</Link>
代替下面的标签
<a href="#/">A</a>
<a href="#/b">B</a>
<a href="#/c">C</a>
最终渲染如下
从react-router-dom
中引入Route
组件标签,该标签的作用是根据路径的变化,去匹配对应的值,显示不同的组件。path
属性用于设置匹配的条件,component
属性设置最终渲染的组件
{/* 路由渲染组件放置位置 */}
<div className="container">
<Route path='/' component={A}></Route>
<Route path='/b' component={B}></Route>
<Route path='/c' component={C}></Route>
</div>
效果图如下,发现可以实现跳转,但是页面显示有一点点问题,就是A组件还在,我们希望的效果是只显示对应的组件内容。
造成这样子的原因是我们目前的路由匹配是非精准匹配,且路由匹配成功一项后还会继续往后匹配。若页面当前路径为:/b,当第一个Route
的path
进行匹配的时候,/
能够匹配成功/b
,这属于非精准匹配,于是就显示了A组件,然后继续向下匹配,第二个path
为/b
,能够精准匹配页面的/b
显示B组件。
我们首先解决上诉问题的匹配成功后,还会继续路由匹配的问题,该问题使用react-router-dom
提供的Switch
组件标签完成。该标签的作用是当一个路由匹配成功后便不会往下继续匹配。 如下代码,但是这样添加并没有解决精准匹配的问题,每次地址为/b,/c的时候都是匹配到/就结束匹配了,导致页面一直显示A组件。开启精准匹配给Route
组件标签exact
属性,代表开启精准匹配
<Switch>
<Route path='/' component={A}></Route>
<Route path='/b' component={B}></Route>
<Route path='/c' component={C}></Route>
</Switch>
开启精准匹配且匹配成功后不再继续匹配的完整代码。
<Switch>
<Route exact path='/' component={A}></Route>
<Route exact path='/b' component={B}></Route>
<Route exact path='/c' component={C}></Route>
</Switch>
扩展:如果输入一个地址跟任何一个匹配规则都匹配不上的时候,需要显示404组件,可以在使用一个额外的Route
组件标签用于什么也没有匹配到的时候显示对应的组件内容。在该标签中path='*'
或者path=''
设置即可。但是该组件通常放在Switch
组件标签的最后。
<Route path='*' component={Error}></Route>
或者没有匹配成功的时候,可以重定向到某一个组件显示内容。那么就需要使用react-router-dom'
中的Redirect
重定向组件,该组件的格式:Redirect from='从哪来' to='重定向到哪' exact开启精准匹配
代码设置如下,Redirect
组件标签通常也是放在Switch
组件标签的最后。当什么路由也没有匹配到的时候,会执行该语句,类似页面地址修改到/,会重新匹配一下路由规则进入<Route exact path='/' component={A}></Route>
<Redirect to='/'></Redirect>
在Route
组件中除了使用component
属性控制渲染的组件,还可以通过render
函数渲染组件,使用函数渲染组件是可以在返回组件之前做一些权限校验操作等。
<Route exact path='/c' render={() => {
let login = true
if (login) {
return <C></C>
}
return <Redirect to='/'></Redirect>
}}></Route>
多级路由
修改之前的代码,然后创建二级路由组件
<HashRouter>
<div className="App-box">
<NavBox className="header">
<Link to="/a">A</Link>
<Link to='/b'>B</Link>
<Link to='/c'>C</Link>
</NavBox>
<div className="container">
<Switch>
<Redirect exact from='/' to='/a'></Redirect>
<Route path='/a' component={A}></Route> //二级路由的时候一级路由不能写eact精准匹配,否则无法进入组件进行二次路由匹配
<Route path='/b' component={B}></Route>
<Route path='/c' component={C}></Route>
<Redirect to='/a'></Redirect>
</Switch>
</div>
</div>
</HashRouter>
二级路由组件,位于一级路由组件A下。
A组件中搭建二级路由,在二级路由中就不需要使用HashRouter
等组件标签了,因为二级路由作用于一级路由内。
<div className="A-box">
<div className="menu">
<Link to='/a/a1' >a1</Link>
<Link to='/a/a2' >a2</Link>
<Link to='/a/a3' >a3</Link>
</div>
{/* 如果直接输入/a直接重定向到/a/a1,但是会和/a/a2等冲突所以启动精准匹配 */}
<Switch>
<Redirect exact from='/a' to='/a/a1'></Redirect>
<Route path='/a/a1' component={A1}></Route>
<Route path='/a/a2' component={A2}></Route>
<Route path='/a/a3' component={A3}></Route>
<Redirect to='/a/a1'></Redirect>
</Switch>
</div>
以上就是一个多级路由的代码,但是发生,当路由级数增多的时候,如果都将所有的路由组件或匹配规则写在组件中,那么代码就变的难以维护,且重复代码过多。如多个Route
组件标签完全可以使用循环创建,而非手动。因此我们引入了react路由表管理机制
路由表管理机制
react的路由表管理机制需要自己手动搭建和创建规则,不像vue的路由已经是搭建好的模块,直接使用即可。
创建如图所示结构,index文件为主体路由,routes文件为路由表规则等
在routes文件中编写具体代码
// 创建路由表:为数组对象,对象中需要配置路由规则
/*
redirect:是否重定向,用于有<Redirect />的组件标签
from:去哪个路径来
to:去的目标地址
exact:是否精准匹配
path:匹配的路径 用于<Route /> 组件标签使用
component:<Route/>组件匹配成功需要渲染的组件
name:路由名称,类似vue的命名路由
meta:路由元信息,自定义相关配置
children:[]子路由
*/
import { A } from '@/views/A.jsx'
import { B } from '@/views/B.jsx'
import { C } from '@/views/C.jsx'
export const routes = [
{
redirect: true,
exact: true,
from: '/',
to: '/a',
},
{
path: '/a',
name: 'a',
component: A,
meta: {},
children: []
},
{
path: '/b',
name: 'b',
component: B,
meta: {},
},
{
path: '/c',
name: 'c',
component: C
}
]
本质是将如下代码转换为上面的规则描述。
在A组件中存在二级路由,在这里如果路由级数多,可以将children的配置信息封装一个文件(这里单独配置文件,在原先children后面直接配置报错,找不到原因。。。)
子路由声明children字段
import { A1 } from '@/views/A/A1.jsx'
import { A2 } from '@/views/A/A2.jsx'
import { A3 } from '@/views/A/A3.jsx'
export const childARoutes = [
{
redirect: true,
exact: true,
from: '/a',
to: '/a/a1'
},
{
path: '/a/a1',
name: 'a-a1',
meta: {},
component: A1
},
{
path: '/a/a2',
name: 'a-a2',
meta: {},
component: A2
},
{
path: '/a/a3',
name: 'a-a3',
meta: {},
component: A3
},
{
redirect: true,
to: '/a/a1'
}
]
routes文件中给children属性配置值
children: childARoutes
然后封装一个RouterVIew
组件,页面中需要使用到路由跳转的地方都使用该组件完成
import { Switch, Route, Redirect } from 'react-router-dom'
// 基于属性将路由表传递给函数组件使用
export const RouterView = (props) => {
let { routes } = props
return <Switch>
{
routes.map((item, index) => {
let { redirect, from, to, exact, path, component: Component } = item
let config = {}
// 是重定向组件就返回该组件
if (redirect) {
if (from) config.from = from
if (to) config.to = to
if (exact) config.exact = exact
return <Redirect key={index} {...config}></Redirect>
}
if (path) config.path = path
if (exact) config.exact = exact
return <Route key={index} {...config} render={() => {
// 使用render函数返回组件,可以在返回组件之前做特殊处理
return <Component></Component> //component存放组件,但是组件首字母需要为大写,所以进行了重命名
}}></Route>
})
}
</Switch>
}
在App组件中引入使用,并传入路由表信息
import { RouterView } from './router'
import { routes } from './router/routes'
<RouterView routes={routes}></RouterView>
在A组件中引入使用,并传入二级路由表信息
import { RouterView } from '../router'
import { childARoutes } from '../router/childARoutes'
<RouterView routes={childARoutes}></RouterView>
页面可以正常切换
路由懒加载
在我们上面的代码中存在一个问题,我们在路由配置中,将所有用到的组件全部引入进来,这就会导致初次加载js文件的时候体积非常大,会有白屏,并且通常只需要先将首屏需要显示的组件内容展示即可。其他组件当路由匹配的时候动态加载即可。
在React中提供了lazy(callback)
方法实现懒加载,该函数中传入一个回调函数并且返回一个import
导入组件的操作。 这样子就可以在每次路由匹配的时候,加载单独的js文件显示。(分割打包,需要动态加载的路由组件都是一个js文件)
注意:使用这种lazy
方法引入的组件建议是默认导出,不能是按需导出,否则报错
在按需导出的时候,可能会因为文件还没有加载完毕,但是在router文件中的index文件中已经return返回了,这就会导致页面报错。因此在最终返回的组件外部,需要使用react提供的Suspense
组件标签配合lazy()
方法实现异步加载组件效果
将路由表中除了A组件是正常引入加载,其他均采样懒加载形式
import { lazy } from 'react'
component: lazy(() => import('../views/B.jsx')),
component: lazy(() => import('../views/C.jsx'))
component: lazy(() => import('../views/A/A1'))
component: lazy(() => import('../views/A/A2'))
component: lazy(() => import('../views/A/A3'))
Suspense
组件标签通常配合lazy
方法成对使用
import { Suspense } from 'react'
return <Route key={index} {...config} render={() => {
// 使用render函数返回组件,可以在返回组件之前做特殊处理
return <Suspense>
<Component></Component>
</Suspense>
}}></Route>
初次加载,只把A跟A1组件加载使用,其余并未加载
当点击其他组件的时候,会按需加载,生成单独的js文件
已经加载过的资源不会重复获取
Suspense
组件还支持在异步组件未加载完毕前显示loading效果,该组件可以传入fallback
配置属性使用,在该属性中,返回jsx视图。
<Suspense fallback={<>加载中。。。</>}>
<Component></Component>
</Suspense>
扩展:如下资源获取,每次需要加载都会获取一个js文件,那么能不能将每个模块相关的放在一个js中获取,如和A有关的a1,a2,a3放在一个js文件中获取。
只需要给每一个a1,a2,a3的按需引入添加webpack标识,Webpack通过增加内联注释来告诉运行时,该有怎样的行为。。这样子在打包的时候,会将AChild
有关的存放一个文件中,并且会将有关的文件一起引入打包处理。
component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/A/A1'))
component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/A/A2'))
component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/A/A3'))
路由传参
基于Route
组件标签实现路由匹配渲染的组件,会默认给组件传递三个属性:history,location,match
<Route path='/a' component={A} />
基于特有属性component
指定渲染组件,会默认传递这三个属性,组件能够基于props
属性接收。<Route path='/a' render={ (props)= { return <A {...props}/> } }/>
基于特有属性render
实现组件渲染,则会将默认参数传递给要实际渲染组件的函数作为参数使用,需要自己将传递属性传给组件。
因此在函数组件或类组件中,都可以基于props
获取路由默认传递的参数
// 基于render接收属性并传递给组件接收使用
return <Route key={index} {...config} render={(props) => {
return <Suspense fallback={<>加载中。。。</>}>
<Component {...props}></Component>
</Suspense>
}}></Route>
在函数组件中,可以使用props接收属性,也可以使用react-router-dom
提供了hooks函数获取:useHistory, useLocation, useRouteMatch
在组件B中输出查看
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'
const B = (props) => {
let history = useHistory()
let location = useLocation()
let match = useRouteMatch()
console.log(history, location, match);
console.log(props);
return <div className="B-box">
我是内容B
</div>
}
其中history
具体内容如下
location
具体内容如下
match
中具体内容如下
以上是基于Route
路由组件渲染的组件,因此即可基于props
获取参数,又可以使用hook函数获取参数。那么在非Route
组件标签渲染的组件中能否获取这三个默认参数。
将几个Link
组件标签提取封装在一个组件中。并导出使用,在该组件中使用两种方式查看输出值。
const HomeHeader = (props) => {
const history = useHistory()
console.log(history);
console.log(props);
return <NavBox className="header">
<Link to="/a">A</Link>
<Link to='/b'>B</Link>
<Link to='/c'>C</Link>
</NavBox>
}
export default HomeHeader
由此可见,只能使用hook函数获取这些路由默认传递的信息。
总结:在HashRouter
或BrowserRouter
组件标签包裹的内部,非Route
标签处理,普通渲染的组件可以使用react-router-dom
提供的hook函数useHistory, useLocation, useRouteMatch
获取默认传递的路由信息,但是无法经过props属性获取。如果是经过Route
渲染的组件,那么可以使用props或者hook函数获取路由信息。
上面的总结,如果是用于函数组件,那么没有问题,如果应用于类组件,在HashRouter
或BrowserRouter
,那么一个非Route
渲染出来的组件,既不能使用props,也不能使用hook函数,该如何获取路由信息。处理方式:使用高阶组件
// 将上面的函数组件修改为类组件,想在非Route受控的组件中使用路由参数信息
class HomeHeader extends React.Component {
render() {
console.log(this.props); // 获取的是{}
return <NavBox className="header">
<Link to="/a">A</Link>
<Link to='/b'>B</Link>
<Link to='/c'>C</Link>
</NavBox>
}
}
const HomeHeaderProxy = (Component) => {
return function HOC(props) { //实际使用该函数组件,因此组件名首字母要大写
// 其他地方用到该组件,实际是使用HOC函数组件的内容,因此传递的参数会给被该函数接收
// 在函数组件内部可以使用hook函数,获取参数,然后基于属性传递给实际渲染的类组件
let histroy = useHistory()
let location = useLocation()
let match = useRouteMatch()
return <Component {...props} histroy={histroy} location={location} match={match} />
}
}
export default HomeHeaderProxy(HomeHeader)
App组件中调用
<HomeHeader name='navbar'></HomeHeader>
类组件中可以使用this.props
获取
在v5版本中,提供了一个函数withRouter
,该函数的功能就是实现上面高阶组件的功能。只需下面简短的两行就能实现上面高阶组件的功能。这样子,在一个非Route
管理的组件中,就可以基于props获取路由信息。但是withRouter
函数在V6版本的路由中删除了,因此在v6中,在一个非Route
管理的组件中,如果一个类组件想获取,依旧需要手动实现高阶组件功能。
import { withRouter } from 'react-router-dom'
export default withRouter(HomeHeader)
有两种路由跳转方式,语法几乎一模一样
Link
标签实现路由跳转,同时,在to
属性配置中可以直接写目标路由地址,也可以写配置对象,指定路由跳转时候携带的参数。在配置对象中,pathname
指明了目标路由的地址,search
指明了参数格式必须是urlencoded格式。replace
指明路由模式replace。默认push
<Link to='/c'>
<Button>点击跳转C</Button>
</Link>
<Link replace to={{
pathname: '/c',
search: ''
}}>
<Button>点击跳转C</Button>
</Link>
- 编程式路由,使用js控制路由跳转。借助原生
history
对象或useHistory
hook函数
let history = useHistory()
<Button onClick={() => {
history.push('/c')
}}>点击跳转C</Button>
search传参和使用(urlencoded)
两种方式的使用基本一致,那么传参该如何实现,这里以编程式路由为例子。
已search
配置项传递的是urlencoded参数格式。那么对应的传参如下
<Button onClick={() => {
history.push({
pathname: '/c',
search: `id=1&name=张三`
})
}}>点击跳转C</Button>
既然是这种传参方式,那么就可以直接问号拼接传参也可以
<Button onClick={() => {
history.push({
pathname: '/c?id=1&name=张三',
})
}}>点击跳转C</Button>
这两种方法传递原理一样,都是将参数拼接在地址后面,但是这种传参方式缺点很明显,如果参数过多,则地址会变得很长,且直接将参数信息暴露出来,很不安全。优点是即使目标路由刷新了,但是这些参数依旧存在地址后面
那么基于search
实现urlencoded传参,目标路由该如何获取传递的参数信息?
因为是采样search
传参,所以参数信息是存放在location
对象中保存,需要借助useLocation
hook函数获取。
import { useLocation } from 'react-router-dom'
const C = () => {
let location = useLocation()
console.log(location);
.....
}
export default C
如果想将search
配置项中的参数取出组成一个对象,有如下两种常用方法
- 借助
qs
库快速实现,但是需要注意,qs
库是直接针对key=value&key=value
这种格式进行转换,不会对?
符号进行处理,因此需要先进行截取操作。
import qs from 'qs'
// C组件
let location = useLocation()
let search = qs.parse(location.search.substring(1))
console.log(search);
- 借助
URLSearchParams
构造函数快速创建一个URLSearchParams
对象,需要在new实例化的时候传入参数信息进行初始化。在该对象身上,有许多操作对象属性的方法如delete,get,set等。其中可以使用get
方法获取对应属性的值
let search1 = new URLSearchParams(location.search.substring(1))
console.log(search1);
console.log(search1.get('id'), search1.get('name'));
路径传参(params)
路径传参,是将参数作为路径的一部分一起传递。在路由匹配中使用:
占位符占位,因此需要先修改路由表中的路由。
{
path: '/c/:id/:name',
name: 'c',
component: lazy(() => import('../views/C.jsx'))
}
<Button onClick={() => {history.push('/c/1/张三')}}>点击跳转C</Button>
path: '/c/:id/:name'
如果是这种格式的路径,那么在跳转的时候每个占位符所需要的参数都必须传递,否则无法实现跳转
修改为 history.push('/c/1')
,跳转如图,发现无法实现正常跳转功能了
因此我们可以在指定占位符的同时设置参数是否可选,使用?
代表该参数可传项,path: '/c/:id?/:name?'
,这样子在跳转的时候,/c
,/c/1
,/c/1/张三
均可以实现跳转功能。
因为是params
格式,存放在match
参数中,在目标组件中可以使用useRouteMatch
hook函数获取,也可以直接使用useParams
hook函数获取。
let match = useRouteMatch()
console.log(match);
let params = useParams()
console.log(params);
使用路径传参也是将参数放在url地址中,但是也存在不安全和参数过长问题。页面刷新路径参数信息也不会丢失。
隐式传参
利用路径跳转的state
参数实现,并在location
中存储。不会出现在url地址中,这种方式如果刷新页面,参数会丢失,需要重新获取
<Button onClick={() => {
history.push({
pathname: '/c',
state: {
id: 1,
name: '张三'
}
})
}}>点击跳转C</Button>
let location = useLocation()
console.log(location);
参数不会出现在地址中
NavLink
除了Link
标签,还有NavLink
标签,他们的具体使用语法一样,都是转换为a标签,唯一的区别就是NavLink
标签会给当前路由添加一个默认激活样式类名。将当前页面的路由和to
属性配置的字符串或pathname
进行匹配。如果匹配成功,就给当前匹配成功的NavLink
标签添加active
类名,但是无任何样式。使用NavLink
标签的好处是,可以给当前选中的路由添加样式激活信息。
可以使用activeClassName
修改默认的样式类名active
设置激活时候路由导航对应的样式信息,这里使用的是默认提供的激活类名active
a {
....
&.active {
color:red;
}
}
react-router-dom V6
首先安装第六版本的路由:npm i react-router-dom
,第六版本和第五版大致思想一致,但是语法差异很大。
在 react-router-dom V6
版本中,移除了Switch
,Redirect
,withRouter
等。
在第六版本中,所以需要路由匹配规则的全部放在Routes
组件标签中,可以理解为代替Switch
组件标签。并且Redirect
组件标签被Navigate
组件替代。同时Route
组件标签的语法也变化了。
在Route
组件标签中,使用element
属性代替原有的component
或render
属性。并且书写格式也有要求。如果按照如下书写格式,则会报错。
<Routes>
<Route path='/a' element={A}></Route>
....
</Routes>
正确写法如下,将要渲染的组件,以完整的标签形式写全。
在如下代码中,Navigate
组件可以配合在Route
标签中使用,第一段代码用于匹配/
,匹配成功由Navigate
标签实现重定向操作。
默认匹配成功,就不会再往下继续匹配,并且不在需要使用exact属性,默认开启精准匹配。
<Routes>
<Route path='/' element={<Navigate to='/a'></Navigate>}></Route>
<Route path='/a' element={<A></A>}></Route>
<Route path='/b' element={<B></B>}></Route>
<Route path='/c' element={<C></C>}></Route>
<Route path='*' element={<Navigate to='/a'></Navigate>}></Route>
</Routes>
Navigate
中to
属性不仅可以直接写跳转的地址,也可以写一个配置对象和路由跳转方式。
<Route path='*' element={<Navigate replace to={{
pathname: '/a',
search: '?from=404'
}}></Navigate>}></Route>
</Routes>
多级路由
在V6版本中,如果想实现多级路由,不再像V5一样,将路由分散到各个组件中编写。而是放在一起统一处理。
如下代码是继续在原有路由中继续添加新路由,在目标<A></A>
一级路由组件中,需要使用提供的Outlet
组件标签,告诉二级路由放置渲染的位置即可。
<Routes>
<Route path='/' element={<Navigate to='/a'></Navigate>}></Route>
<Route path='/a' element={<A></A>}>
// 二级路由
<Route path='/a' element={<Navigate to='/a/a1'></Navigate>}></Route>
<Route path='/a/a1' element={<A1></A1>}></Route>
<Route path='/a/a2' element={<A2></A2>}></Route>
<Route path='/a/a3' element={<A3></A3>} ></Route>
<Route path='*' element={<Navigate to='/a/a1'></Navigate>}></Route>
</Route>
<Route path='/b' element={<B></B>}></Route>
<Route path='/c' element={<C></C>}></Route>
<Route path='*' element={<Navigate replace to={{
pathname: '/a',
search: '?from=404'
}}></Navigate>}></Route>
</Routes>
import { NavLink, Outlet } from 'react-router-dom'
const A = () => {
return <div className="A-box">
<div className="menu">
<NavLink to='/a/a1'>a1</NavLink>
<NavLink to='/a/a2'>a2</NavLink>
<NavLink to='/a/a3'>a3</NavLink>
</div>
{/* 存放容器 */}
<Outlet></Outlet>
</div>
}
路由跳转及传参
在V6版本中,取消了所有基于props传递默认的参数。即使是被Route
匹配路由渲染的也不会使用props传递参数。统一采样了hook函数。如果不是被HashRouter
或BrowserRouter
控制的组件,无法使用路由提供的hook函数,一旦强行使用就会报错。
B组件由Route
组件标签渲染出来,想使用props获取默认参数失败,为空对象
APP不在HashRouter
路由组件的控制内,不能直接使用路由中的hook函数,否则报错。
在函数组件中统一采样hook函数,那么完全可以获取路由中的参数,但是在类组件中就无法处理。既没有属性传参,也没有hook函数,因此需要自己处理。可以在构建路由表的时候,会基于Route
匹配渲染组件的同时,基于属性将路由信息传递。并且在非Route
匹配渲染的类组件中,手写一个withRouter
方法实现高阶组件的功能,将默认的参数传递给组件使用。
在V6版本中如果想实现路由跳转有以下三种方式
- 基于
Link
或NavLink
组件标签实现路由跳转,需要手动点击触发。 - 基于
Navigate
组件标签的to
属性,实现自动跳转。
<Navigate to='/c'></Navigate>
- (编程式路由导航)基于
useNavigate
hook函数实现路由跳转。在v6版本中,取消了useHistory
函数,同时对应着history
对象也取消了。useNavigate
函数执行的返回结果也是一个函数。该返回的函数通过设置参数实现路由跳转。
let navigate = useNavigate()
console.log(navigate);
navigate({
pathname: '/c',
search: '?id:1&name:张三'
})
navigate('/c')
navigate('/c',{replace:true})
search传参
let navigate = useNavigate()
.....
<Button onClick={() => {
navigate({
pathname: '/c',
search: '?id:1&name:张三'
})
}}>跳转到C组件</Button>
或者使用qs库,将对象转换为urlencoded格式传递
search: qs.stringify({
id: 1,
name: '张三'
})
在C组件中可以基于useLocation
函数获取对象中的search
值。
let search = qs.parse(useLocation().search.substring(1))
console.log(search);
也可以基于URLSearchParams
构造函数获取。
let search = new URLSearchParams(useLocation().search)
console.log(search.get('id'), search.get('name'));
不过在v6版本中提供了useSearchParams
函数。该函数调用会返回一个数组,数组的第一个元素就是基于URLSearchParams
构造函数返回的,使用格式上上面一致
console.log(useSearchParams());
let [search] = useSearchParams()
console.log(search.get('id'), search.get('name'));
路径传参
<Route path='/c/:id?/:name?' element={<C></C>}></Route>
let navigate = useNavigate()
<Button onClick={() => {navigate('/c/1/张三')}}>跳转到C组件</Button>
在C组件中依旧使用useParams
函数接收。同时移除了useRouterMatch
函数,因此无法基于该函数获取对象中的params
参数信息。
let params = useParams()
console.log(params);
当然v6中也可以使用useMatch("/d2/:id")
这样子就可以获取完整的match信息了
隐式传参
在使用useNavigate
函数实现路由隐式传参跳转,设置state
属性的位置和v5中的useHistory
的位置不同
在接口定义中指出了state应该书写的位置
navigate('/c', { state: { id: 1, name: '张三' } })
或
navigate({ pathname: '/c' }, { state: { id: 1, name: '张三' } })
在C组件中基于useLocation
函数获取。和v5不同,路径传参不会随页面刷新而消失,但是隐式传参会。但是在v6中,隐式传参即使刷新也不会丢失。
let state = useLocation()
console.log(state);
路由表统一管理
首先封装路由表
import { Navigate } from "react-router-dom"
import { lazy } from "react"
import A from '@/views/A.jsx'
const aRoutes = [
{
path: '/a',
component: () => <Navigate to='/a/a1'></Navigate>
},
{
path: '/a/a1',
component: lazy(() => import(/*webpackChunkName:"AChild"*/'@/views/A/A1.jsx'))
},
{
path: '/a/a2',
component: lazy(() => import(/*webpackChunkName:"AChild"*/'@/views/A/A2.jsx'))
},
{
path: '/a/a3',
component: lazy(() => import(/*webpackChunkName:"AChild"*/'@/views/A/A3.jsx'))
},
{
path: '*',
component: () => <Navigate to='/a/a1'></Navigate>
}
]
const routes = [
{
path: '/',
//不能直接返回组件,否则已导入的时候就会执行跳转,并且这里也需要设置to跳转,这样子设置方便
// <Route path='/a' element={<Navigate to='/a/a1'></Navigate>}></Route> 如果直接放置组件,会直接跳转
component: () => <Navigate to='/a'></Navigate>
},
{
path: '/a',
component: A,
children: aRoutes
},
{
path: '/b',
component: lazy(() => import('@/views/B.jsx'))
},
{
path: '/c',
component: lazy(() => import('@/views/C.jsx'))
},
{
path: '*',
component: () => {
return <Navigate replace to={{
pathname: '/a',
search: '?from=404'
}}></Navigate>
}
}
]
export default routes
在index中的代码,负责实现创建Route组件标签,利用递归实现,并且最终创建一个路由容器。
import { Routes, Route, useLocation, useNavigate, useParams } from 'react-router-dom'
import routes from '@/router/routes.js'
import { Suspense } from 'react'
/* 封装组件的用处,因为v6版本取消了props默认传参,全部采样hook传参
如果是类组件,并且是经过Route组件渲染,就会造成既不能使用props传参
也不能使用hook函数。因此封装一个函数,可以在返回最终组件前,执行某些操作
如实现props传参,路由验证登 */
const Element = (props) => {
let { component: Component } = props
let location = useLocation()
let params = useParams()
let navigate = useNavigate()
return <Component location={location} params={params} navigate={navigate}></Component>
}
// 创建所有的Route
const createRoute = (routes) => {
return <>
{/* 渲染实际的Route组件 */}
{
routes.map((item, index) => {
let { path, children } = item
/* elementp配置不直接渲染组件,二次封装组件,在该组件中返回要渲染的组件 */
return <Route key={index} path={path} element={<Element {...item}></Element>}>
{/* 如果存在子路由,就递归创建Route */}
{Array.isArray(children) ? createRoute(children) : null}
</Route>
})
}
</>
}
// 创建容器,存放Route组件
const RouterView = () => {
// 因为使用了懒加载,所以会出现异步加载的情况,需要使用异步处理组件
return <Suspense fallback={<>加载中。。。</>}>
<Routes>
{createRoute(routes)}
</Routes>
</Suspense>
}
export default RouterView
// 实现withRouter,用于非Route渲染的类组件,也可以获取路由信息
// 一个项目只能一个默认导出,因此使用按需导出
export const withRouter = (Component) => {
return function HOC(props) {
let location = useLocation()
let params = useParams()
let navigate = useNavigate()
return <Component {...props} location={location} params={params} navigate={navigate}></Component>
}
}
扩展: [CreateRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
代表在 <Routes>{createRoute(routes)}</Routes>
中Routes
组件中只能直接存在Route
组件,不能是其他的自己使用的组件。
这样子在页面中可以正常实现原有功能。并且所有基于Route
路由匹配渲染的组件(无论函数组件或类组件),都可以使用props获取参数。
useRoutes
上面代码如果不想自己手动实现,那么可以基于useRoutes
函数实现,基本逻辑就递归创建Route
标签使用,不过这样子创建的话就没有我们自己的功能丰富,无法传递props,给类组件添加一些功能。该函数完全为函数组件所使用的。
<HashRouter><App></App></HashRouter>
使用useRoutes
函数的时候,必须被HashRouter
控制处理。 提前引入使用,注意在element
处,必须写组件,如果是懒加载之类引入的组件,必须再次使用一个变量接收作为组件使用
import { useRoutes, Navigate } from 'react-router-dom'
import HomeHeader from '@/components/HomeHeader.jsx'
export const App = () => {
const LazyA1 = lazy(() => import('@/views/A/A1.jsx'))
// useRoutes函数返回的是一个虚拟DOM
let element = useRoutes([
{
path: '/',
element: <Navigate to='/a'></Navigate>
},
{
path: '/a',
element: <A></A>,
children: [
{
path: '/a',
element: <Navigate to='/a/a1'></Navigate>
},
{
path: '/a/a1',
element: <LazyA1></LazyA1>
}
]
},
{
path: '/b',
element: <B></B>
},
{
path: '/c',
element: <C></C>
}
])
return <>
<HomeHeader></HomeHeader>
<Suspense> {element}</Suspense>
</>
}
页面可以正常显示
useReducer
useReducer
是对useState
的升级处理。执行useReducer
函数会返回一个状态值和dispatch
函数。useReducer
函数执行大致和redux思想一致,都是需要基于reducer函数实现具体功能。useReducer
函数会创建一个局部状态管理。
const [state, dispatch] = useReducer(reducer, initialValue)
import { useReducer } from "react"
let initialValue = {
num: 0
}
const reducer = (state, action) => {
state = { ...state }
switch (action.type) {
case '+':
state.num++;
break;
case '-':
state.num--;
break;
}
return state
}
const A1 = () => {
const [state, dispatch] = useReducer(reducer, initialValue)
return <div className="A1">
我是A1组件
<hr />
<h4>我是num的值:{state.num}</h4>
<Button onClick={() => dispatch({ type: '+' })}>加</Button>
<Button onClick={() => dispatch({ type: '-' })}>减</Button>
</div>
}
那么什么情况下需要使用useReducer
,如果单个组件中需要用到的状态值很多的情况下,可以使用useReducer
创建并统一集中管理。这个情况下如果使用useState
创建的话,会执行很多重复的代码。使用useReducer
管理多个状态的时候,如果想修改某一个状态值可以直接修改,无需像useState
一样还需要注意是否是状态整体替换之类的(一个对象中多个状态值,想修改某一个的情况)。