ReactRouter
前端路由的原理
前端路由是如何做到 URL
和内容进行映射呢?监听 URL
的改变
URL
发生变化,同时不引起页面的刷新有两个办法:- 通过
URL
的hash
改变URL
- 通过
HTML5
中的history
模式修改URL
- 通过
当监听到
URL
发生变化时,我们可以通过自己判断当前的URL
,决定到底渲染什么样的内
hash
URL
的 hash
也就是锚点(#), 可以通过 location.hash
来改变 href
, 但是页面不发生刷新
注意:
hash
的优势就是兼容性更好,在老版 IE(最低兼容到 IE3)中都可以运行但是缺陷是有一个#,显得不像一个真实的路径,或者说有点丑 💩
<body>
<div id="app">
<a href="#/home">主页</a>
<a href="#/about">关于</a>
<div id="router-view"></div>
</div>
<script>
const routerViewEl = document.getElementById('router-view')
window.addEventListener('hashchange', () => {
console.log(location.hash)
switch (location.hash) {
case '#/home':
routerViewEl.innerHTML = '首页'
break
case '#/about':
routerViewEl.innerHTML = '关于'
break
default:
routerViewEl.innerHTML = ''
}
})
</script>
</body>
HTML5 的 history
history
接口是HTML5
新增的, 它有六种模式改变 URL
而不刷新页面:
- replaceState:替换原来的路径
- pushState:使用新的路径
- popState:路径的回退
- go:向前或向后改变路径
- forward:向前改变路径
- back:向后改变路径
<div id="app">
<a href="/home">首页</a>
<a href="/about">关于</a>
<div class="router-view"></div>
</div>
<script>
// 1.获取router-view的DOM
const routerViewEl = document.getElementsByClassName('router-view')[0]
// 获取所有的a元素, 自己来监听a元素的改变
const aEls = document.getElementsByTagName('a')
for (let el of aEls) {
el.addEventListener('click', (e) => {
e.preventDefault()
const href = el.getAttribute('href')
history.pushState({}, '', href)
urlChange()
})
}
// 调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在 JavaScript 中调用 history.back() 方法)。即,在同一文档的两个历史记录条目之间导航会触发该事件。
// 无论是浏览器的前进还是后退都会触发这个popstate事件,所以只能起到一个监听页面变化的作用。
window.addEventListener('popstate', urlChange)
// 监听URL的改变
function urlChange() {
switch (location.pathname) {
case '/home':
routerViewEl.innerHTML = '首页'
break
case '/about':
routerViewEl.innerHTML = '关于'
break
default:
routerViewEl.innerHTML = ''
}
}
</script>

React-Router V5
React Router
的版本 4 开始,路由不再集中在一个包中进行管理了:
react-router
是router
的核心部分代码react-router-dom
是用于浏览器的react-router-native
是用于原生应用的
目前我们使用的 React Router
版本是@5.2.0 的版本
安装 react-router-dom
会自动帮助我们安装react-router
的依赖:yarn add react-router-dom
基本使用
react-router
最主要的 API
是给我们提供的一些组件:
BrowserRouter
或HashRouter
BrowserRouter
使用history
模式HashRouter
使用hash
模式
Link
:不太常用,通常在项目中使用编程式导航- 路径的跳转是使用
Link
组件,最终会被渲染成a
元素 to
属性:Link
中最重要的属性,用于设置跳转到的路径
- 路径的跳转是使用
Route
:Route
用于路径的匹配path
属性:用于设置匹配到的路径component
属性:设置匹配到路径后,渲染的组件exact
:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件
import { Component } from 'react'
import { BrowserRouter, Link, Route } from 'react-router-dom'
import Home from './pages/home'
import About from './pages/about'
class App extends Component {
render() {
return (
<div>
<BrowserRouter>
<Link to='/'>首页</Link>
<Link to="/about">关于</Link>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</BrowserRouter>
</div>
)
}
}
export default App
Switch
我们来看下面的路由规则:
- 当我们匹配到某一个路径时,我们会发现有一些问题
- 比如
/about/xxxx
路径匹配到的同时,/about
也被匹配到了,且*
总是匹配到
<Route exact path='/' component={Home}/>
<Route path='/about/xxxx' component={Aboutx}/>
<Route path='/about' component={About}/>
<Route path="*"><NoMatch /></Route>
原因是什么呢?默认情况下,react-router
中只要是路径被匹配到的 Route
对应的组件都会被渲染
但是实际开发中,我们往往希望有一种排他的思想:只要匹配到了第一个,那么后面的就不应该继续匹配了;这个时候我们可以使用 Switch
来将所有的 Route
进行包裹即可
<Switch>
<Route exact path='/' component={Home}/>
<Route path='/about' component={About}/>
<Route path="*"><NoMatch /></Route>
</Switch>
Redirect
Redirect
用于路由的重定向,当这个组件出现时,就会执行跳转到对应的 to
路径中:
我们这里使用这个的一个案例:
用户跳转到 User
界面,但是在 User
界面有一个 isLogin
用于记录用户是否登录:
true
:显示用户的名称false
:直接重定向到登录界面
// user.jsx
import { Component } from 'react'
import { Redirect } from 'react-router-dom'
class User extends Component {
constructor() {
super()
this.state = {
isLogin: false,
}
}
render() {
return (
<div>
{this.state.isLogin ? <h2>用户:Frank</h2> : <Redirect to={'/login'} />}
</div>
)
}
}
export default User
404
必须放在最后一个,且必须使用
Switch
包裹
<Route path="*" component={404}></Route>
路由嵌套
在开发中,路由之间是存在嵌套关系的
这里我们假设 about
页面中有两个页面内容:
- 商品列表和消息列表
- 点击不同的链接可以跳转到不同的地方,显示不同的内容
import { Component } from 'react'
import { Link, Switch, Route } from 'react-router-dom'
function GoodList() {
return (
<ul>
{[1, 2, 3].map((item) => {
return <li>{`商品${item}`}</li>
})}
</ul>
)
}
function NewsList() {
return (
<ul>
{[1, 2, 3].map((item) => {
return <li>{`消息${item}`}</li>
})}
</ul>
)
}
class Abouts extends Component {
render() {
return (
<div>
<Link to={'/about'}>商品列表</Link>
<Link to={'/about/news'}>消息列表</Link>
<Switch>
<Route exact path={'/about'} component={GoodList} />
<Route path={'/about/news'} component={NewsList} />
</Switch>
</div>
)
}
}
export default Abouts
编程式导航
通过 JavaScript
代码进行跳转有一个前提:必须获取到 history
对象
如何可以获取到 history
的对象呢?两种方式:
- 如果该组件是通过路由直接跳转过来的,那么可以直接获取
history
、location
、match
对象
提示
history.push 这个方法会向 history 栈里面添加一条新记录,这个时候用户点击浏览器的回退按钮可以回到之前的路径。
history.go 这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)
history.replace 跟 history.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
import { Component } from 'react'
import { NavLink, Switch, Route } from 'react-router-dom'
import styled from 'styled-components'
const AboutWrapper = styled.div`
.about-active {
color: orange;
}
`
function JoinUs() {
return <div>加入我们:zfhblog.top</div>
}
function GoodList() {
return (
<ul>
{[1, 2, 3].map((item) => {
return <li key={item}>{`商品${item}`}</li>
})}
</ul>
)
}
function NewsList() {
return (
<ul>
{[1, 2, 3].map((item) => {
return <li key={item}>{`消息${item}`}</li>
})}
</ul>
)
}
class Abouts extends Component {
render() {
return (
<div>
<AboutWrapper>
<NavLink exact to={'/about'} activeClassName={'about-active'}>
商品列表
</NavLink>
<NavLink to={'/about/news'} activeClassName={'about-active'}>
消息列表
</NavLink>
<button
onClick={() => {
this.joinUs()
}}
>
加入我们
</button>
</AboutWrapper>
<Switch>
<Route exact path={'/about'} component={GoodList} />
<Route path={'/about/news'} component={NewsList} />
<Route path={'/about/join'} component={JoinUs} />
</Switch>
</div>
)
}
joinUs() {
this.props.history.push('/about/join')
}
componentDidMount() {
console.log(this.props.history)
}
}
export default Abouts
- 如果该组件是一个普通渲染的组件,那么不可以直接获取 history、location、match 对象
那么如果普通的组件也希望获取对应的对象属性应该怎么做呢?
前面我们学习过高阶组件,可以在组件中添加想要的属性;react-router 也是通过高阶组件为我们的组件添加相关的属性的:
如果我们希望在 App 组件中获取到 history 对象,必须满足以下两个条件:
- App 组件必须包裹在 Router 组件之内
- App 组件使用 withRouter 高阶组件包裹
// app.js
import { Component } from 'react'
import { NavLink, Route, withRouter } from 'react-router-dom'
import Home from './pages/home'
import Abouts from './pages/abouts'
import Order from './pages/order'
import styled from 'styled-components'
const NavLinkWrapper = styled.div`
width: 100vw;
height: 100vh;
display: flex;
.link {
padding-top: 50px;
width: 100px;
background-color: #f1f1f1;
display: flex;
align-items: center;
flex-direction: column;
}
a {
text-decoration: none;
margin-right: 20px;
font-size: 20px;
}
a.link-active {
color: red;
}
`
class App extends Component {
render() {
return (
<NavLinkWrapper>
<div className="link">
<NavLink exact to="/" activeClassName={'link-active'}>
首页
</NavLink>
<NavLink to="/about" activeClassName={'link-active'}>
关于
</NavLink>
<button
to="/order"
onClick={() => {
this.toOrder()
}}
>
订单
</button>
</div>
<Route exact path="/" component={Home} />
<Route path="/about" component={Abouts} />
<Route path="/order" component={Order} />
</NavLinkWrapper>
)
}
toOrder() {
this.props.history.push('/order')
}
}
export default withRouter(App)
// index.js
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
参数传递
动态路由 Param
动态路由的概念指的是路由中的路径并不会固定:
比如/detail 的 path 对应一个组件 Detail。如果我们将 path 在 Route 匹配时写成/detail/:id,那么 /detail/abc、/detail/123 都可以匹配到该 Route,并且进行显示这个匹配规则,我们就称之为动态路由。通常情况下,使用动态路由可以为路由传递参数
import {Component} from 'react';
import {NavLink, Route, withRouter} from "react-router-dom";
import Home from "./pages/home";
import Abouts from "./pages/abouts";
import Order from "./pages/order";
import styled from "styled-components";
const NavLinkWrapper = styled.div`
width: 100vw;
height: 100vh;
display: flex;
.link {
padding-top: 50px;
width: 100px;
background-color: #f1f1f1;
display: flex;
align-items: center;
flex-direction: column;
}
a {
text-decoration: none;
margin-right: 20px;
font-size: 20px;
}
a.link-active {
color: red;
}
`
class App extends Component {
render() {
return (
<NavLinkWrapper>
<div className='link'>
<NavLink exact to='/' activeClassName={'link-active'}>首页</NavLink>
<NavLink to='/about' activeClassName={'link-active'}>关于</NavLink>
<button to='/order' onClick={() => {
this.toOrder()
}}>订单
</button>
</div>
<Route exact path='/' component={Home}/>
<Route path='/about' component={Abouts}/>
<Route path='/order/:id' component={Order}/>
</NavLinkWrapper>
);
}
toOrder() {
this.props.history.push('/order/123')
}
}
export default withRouter(App);
// ------------------------------------------
// order.jsx
// ------------------------------------------
import {Component} from 'react';
class Order extends Component {
render() {
return (
<div>
<h2>订单:{this.props.match.params.id}</h2>
</div>
);
}
}
export default Order;
查询参数 Query
// app.js
import { Component } from 'react'
import { NavLink, Route, withRouter } from 'react-router-dom'
import Home from './pages/home'
import Abouts from './pages/abouts'
import Order2 from './pages/order2'
import styled from 'styled-components'
const NavLinkWrapper = styled.div`
width: 100vw;
height: 100vh;
display: flex;
.link {
padding-top: 50px;
width: 100px;
background-color: #f1f1f1;
display: flex;
align-items: center;
flex-direction: column;
}
a {
text-decoration: none;
margin-right: 20px;
font-size: 20px;
}
a.link-active {
color: red;
}
`
class App extends Component {
render() {
return (
<NavLinkWrapper>
<div className="link">
<NavLink exact to="/" activeClassName={'link-active'}>
首页
</NavLink>
<NavLink to="/about" activeClassName={'link-active'}>
关于
</NavLink>
<button
to="/order"
onClick={() => {
this.toOrder()
}}
>
订单
</button>
</div>
<Route exact path="/" component={Home} />
<Route path="/about" component={Abouts} />
<Route path="/order" component={Order2} />
</NavLinkWrapper>
)
}
toOrder() {
this.props.history.push('/order?id=123&name=frank&test=2&test2=1231231')
}
}
export default withRouter(App)
import { Component } from 'react'
class Order2 extends Component {
render() {
return (
<div>
<h2>订单:{this.props.match.params.id}</h2>
</div>
)
}
componentDidMount() {
let query = {}
const search = this.props.location.search.split('&')
search[0] = search[0].split('?')[1]
search.forEach((item) => {
const kv = item.split('=')
query[kv[0]] = kv[1]
})
console.log(query)
}
}
export default Order2
路由配置
import {BrowserRouter, Link, Route, Switch} from "react-router-dom";
import {Component} from "react";
// 一些用于展示用的组件
// ==========================================================
class Home extends Component {
render() {
return (
<div>
<h2>主页</h2>
</div>
);
}
}
function GoodList() {
return (
<ul>
{
[1, 2, 3].map(item => {
return <li key={item}>{`商品${item}`}</li>
})
}
</ul>
)
}
function NewsList() {
return (
<ul>
{
[1, 2, 3].map(item => {
return <li key={item}>{`消息${item}`}</li>
})
}
</ul>
)
}
class About extends Component {
render() {
return (
<div>
<h2>关于</h2>
<Link to='/about'>商品</Link>
<Link to='/about/news'>新闻</Link>
<Switch>
{this.props.routes.map((route, i) => (
<RouteWithSubRoutes key={i} {...route} />
))}
</Switch>
</div>
);
}
}
// ==========================================================
// 路由配置数组 模板代码抽离
const routes = [
{
path: "/",
exact: true,
component: Home
},
{
path: "/about",
component: About,
routes:[
{
path: "/about",
exact:true,
component: GoodList
},
{
path: "/about/news",
component: NewsList
}
]
}
]
// 核心路由组件转换函数 render-props:https://zh-hans.reactjs.org/docs/render-props.html#gatsby-focus-wrapper
function RouteWithSubRoutes(route) {
return (
<Route
path={route.path}
render={props => {
return <route.component {...props} routes={route.routes} />
}
}
/>
);
}
export default function App() {
return (
<BrowserRouter>
<div>
<Link to='/'>首页</Link>
<Link to='/about'>关于</Link>
<Switch>
{
routes.map((route, i) => {
return <RouteWithSubRoutes key={i} {...route} />
}
)
}
</Switch>
</div>
</BrowserRouter>
)
}
React-Router V6
基本使用
不同于vueRouter
作为一个Vue
插件进行注册使用,ReactRouter
需要使用BrowserRouter
(history
模式)或HashRouter
( hash
模式)对根App
组件进行包裹使用
<HashRouter>
<App />
</HashRouter>
路由映射配置
Routes
:包裹所有的Route
,在其中匹配一个路由
v5使用的Switch
组件,或者Route
可以单独存在
Route
:Route
用于路径的匹配
path
属性:用于设置匹配到的路径element
属性:设置匹配到路径后,渲染的组件 v5使用的是component属性exact
:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件 v6不再支持该属性,自动精准匹配
路由配置和跳转
通常路径的跳转是使用Link
组件,最终会被渲染成a
元素
to
属性:Link
中最重要的属性,用于设置跳转到的路径
import {Link, Route, Routes} from "react-router-dom";
import About from "./About";
import Home from "./Home";
function App(props) {
return (
<div>
<header>
<Link to={'/'}>首页</Link>
<Link to={'/about'}>关于</Link>
</header>
<div className="main">
<Routes>
<Route path='/' element={<Home/>}></Route>
<Route path='/about' element={<About/>}></Route>
</Routes>
</div>
</div>
);
}
export default App;
v5的Redirect
Navigate导航navigate
用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to
路径中。
例如判断当前用户登录状态,未登录跳转到登录页:
function Home(props) {
const {isLogin,changeLoginStatus}=useState(false)
return (
<div>
{
isLogin?'这是首页':<Navigate to={'/login'}></Navigate>
}
</div>
);
}
404页面配置
设置这样一个Route
,当前面的路由都没有匹配上就会匹配这个路由:
<Routes>
<Route path='/' element={<Home/>}></Route>
<Route path='/about' element={<About/>}></Route>
<Route path='*' element={<NotFound />}></Route>
</Routes>
路由嵌套
让父路由包裹子路由:
<Routes>
<Route path='/' element={<Navigate to={'/home'}/>}></Route>
<Route path={'/home'} element={<Home/>}>
{/*子路由匹配时,/home默认跳转到'/home/shop' 防止页面空白*/}
<Route path='/home' element={<Navigate to={'/home/shop'}/>}></Route>
<Route path={'/home/shop'} element={<Cart/>}></Route>
<Route path={'/home/news'} element={<New/>}></Route>
</Route>
<Route path='/about' element={<About/>}>
</Route>
<Route path='/login' element={<Login/>}></Route>
<Route path='*' element={<NotFound/>}></Route>
</Routes>
在home
组件下:
<div>
<Link to={'/home/shop'}>商品列表</Link>
<Link to={'/home/news'}>消息列表</Link>
</div>
{/*用于在父路由元素中作为子路由的占位元素*/}
<Outlet/>
编程式导航
函数组件
const navigate=useNavigate()
function goAbout(){
navigate('/about')
}
<button onClick={goAbout}>
编程式导航去关于
</button>
类组件
通过高阶组件实现
import {useNavigate} from "react-router-dom";
export default function (WrapperCompontent){
return (props)=>{
const navigator=useNavigate()
return (
<WrapperCompontent {...props} router={navigator}/>
)
}
}
路由参数传递
动态路由Params
生产
<Route path={'/home/shop/details/:id'} element={<Details/>}></Route>
import {useNavigate} from "react-router-dom";
export default function Cart(props) {
const navigator=useNavigate()
return (
<div>
<ul>
{
// 传递params id参数
[1,2,3].map(item=><li onClick={()=>{navigator(`/home/shop/details/${item}`)}} key={item}>商品{item}</li>)
}
</ul>
</div>
);
}
消费
import {useParams} from "react-router-dom";
function Details(props) {
let { id } = useParams();
return (
<div>
商品ID为:{id}
</div>
);
}
export default Details;
查询参数Query
生产
function goAbout() {
navigate('/about?name=frank&age=19')
}
消费
import { useSearchParams } from "react-router-dom";
function About(props) {
let [searchParams, setSearchParams] = useSearchParams();
const query=Object.fromEntries(searchParams)
console.log(query)
return (
<div>
这是关于页面
我的姓名:
{searchParams.get('name')}
我的年龄:
{searchParams.get('age')}
</div>
);
}
export default About;
路由配置文件
import About from "../About";
import Home from "../Home";
import NotFound from "../NotFound";
import Login from "../Login";
import New from '../New'
import Cart from "../Cart";
import Details from "../Details";
import {Navigate} from "react-router-dom";
export default [
{
path:'/',
element:<Navigate to={'/home'}/>
},
{
path:"/home",
element: <Home />,
children:[
{
path:'/home',
element:<Navigate to={'/home/shop'}/>
},
{
path:'/home/shop',
element: <Cart/>
},
{
path:'/home/news',
element: <New />
},
{
path:'/home/shop/details/:id',
element: <Details/>
},
],
},
{
path:'/about',
element: <About/>
},
{
path:'/login',
element: <Login/>
},
{
path:"*",
element: <NotFound/>
}
]
使用
{
useRoutes(routes)
}
页面懒加载
const About=React.lazy(()=>import('../About'))
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Suspense fallback={<div>Loading...</div>}>
<BrowserRouter>
<App/>
</BrowserRouter>
</Suspense>
)