基于 react 18.0.2、create-react-app 5.0.1、react router 5 讲解。

所以我们安装的时候 npm i react-router-dom@5

1. 相关理解

1.1 SPA 的理解

  1. 单页 Web 应用(single page web application,SPA)。
  2. 整个应用只有一个完整的页面
  3. 点击页面中的链接不会刷新页面,只会做页面的局部更新。
  4. 数据都需要通过 ajax 请求获取, 并在前端异步展现。

1.2 路由的解释

1.2.1 什么是路由

  1. 一个路由就是一个映射关系(key:value)
  2. key 为路径, value 可能是 function 或 component

1.2.2 路由分类

后端路由

  • 理解: value 是 function, 用来处理客户端提交的请求。

  • 注册路由: router.get(path, function(req, res))

  • 工作过程:当 node 接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据

js
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
app.get("/search/users", function (req, res) { const { q } = req.request; axios({ url: "https://api.github.com/search/users", params: { q }, }).then((response) => { res.json(response.data); }); });

前端路由

  • 浏览器端路由,value 是 component,用于展示页面内容。
  • 注册路由: <Route path="/test" component={Test}>
  • 工作过程:当浏览器的 path 变为 /test 时, 当前路由组件就会变为 Test组 件

1.3 前端路由的基石

前端路由其实是基于 history 对象实现的。

html
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"/> <title>前端路由的基石_history</title> </head> <body> <a href="http://www.atguigu.com" onclick="return push('/test1') ">push test1</a> <br/><br/> <button onClick="push('/test2')">push test2</button> <br/><br/> <button onClick="replace('/test3')">replace test3</button> <br/><br/> <button onClick="back()">&lt;= 回退</button> <button onClick="forword()">前进 =&gt;</button> <script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js" > </script> <script type="text/javascript"> let history = History.createBrowserHistory() //方法一,直接使用H5推出的history身上的API // let history = History.createHashHistory(); //方法二,hash值(锚点) function push(path) { // 添加进历史记录 history.push(path); // 阻止默认行为,禁止 a 标签跳转 return false; } function replace(path) { history.replace(path); } function back() { history.goBack(); } function forword() { history.goForward(); } history.listen((location) => { console.log("请求路由路径变化了", location); }); </script> </body> </html>

1.4 react-router-dom 的理解

react-router 库有三种实现,web、native(原生应用开发)、any (都可以使用),我们开发 web 一般使用 web,所以我们学习 react-router-dom

  1. react 的一个插件库。
  2. 专门用来实现一个 SPA 应用。
  3. 基于 react 的项目基本都会用到此库。

2. 基本路由使用

2.1 效果

2.2 准备

  1. 下载 react-router-dom: npm install --save react-router-dom@5
  2. 引入 bootstrap.css: <link rel="stylesheet" href="/css/bootstrap.css">

2.3 例子

index.js

react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<React.StrictMode> {/* 也可以使用 HashRouter */} <BrowserRouter> <App/> </BrowserRouter>, </React.StrictMode>);

App.jsx

react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
export default class App extends Component { render() { return (<div> <div className="row"> <div className="col-xs-offset-2 col-xs-8"> <div className="page-header"><h2>React Router Demo</h2></div> </div> </div> <div className="row"> <div className="col-xs-2 col-xs-offset-2"> <div className="list-group"> {/* 原生html中,靠<a>跳转不同的页面 */} {/* <a className="list-group-item" href="./about.html">About</a> <a className="list-group-item active" href="./home.html">Home</a> */} {/* 在React中靠路由链接实现切换组件--编写路由链接 */} <Link className="list-group-item" to="/about">About</Link> <Link className="list-group-item" to="/home">Home</Link> </div> </div> <div className="col-xs-6"> <div className="panel"> <div className="panel-body"> {/* 注册路由 */} {/* 若为 HashRouter,则为 # 开头 */} <Route path="/about" component={About}/> <Route path="/home" component={Home}/> </div> </div> </div> </div> </div>) } }

About/index.jsx

react
  • 01
  • 02
  • 03
  • 04
  • 05
export default class About extends Component { render() { return (<h3>我是About的内容</h3>) } }

Home/index.jsx

react
  • 01
  • 02
  • 03
  • 04
  • 05
export default class Home extends Component { render() { return (<h3>我是Home的内容</h3>) } }

2.4 总结

  1. 明确好界面中的导航区、展示区

  2. 导航区的 a 标签改为 Link 标签 <Link to="/xxxxx">Demo</Link>

  3. 展示区写 Route 标签进行路径的匹配 <Route path='/xxxx' component={Demo}/>

  4. <App> 的最外侧包裹了一个 <BrowserRouter><HashRouter>

3. 路由组件和一般组件

  1. 写法不同
    • 一般组件:<Demo/>
    • 路由组件:<Route path="/demo" component={Demo}/>
  2. 存放位置不同
    • 一般组件:components
    • 路由组件:pages
  3. 接收到的 props 不同:
    • 一般组件:写组件标签时传递了什么,就能收到什么
    • 路由组件:接收到三个固定的属性
      • history:
        • go: ƒ go(n)
        • goBack: ƒ goBack()
        • goForward: ƒ goForward()
        • push: ƒ push(path, state)
        • replace: ƒ replace(path, state)
      • location:
        • pathname: “/about”
        • search: “”
        • state: undefined
      • match:
        • params: {}
        • path: “/about”
        • url: “/about”

4. react-router-dom 相关 API

4.1 内置组件

  1. <BrowserRouter>
  2. <HashRouter>
  3. <Route>
  4. <Redirect>
  5. <Link>
  6. <NavLink>
  7. <Switch>

4.2 其它

  1. history 对象
  2. match 对象
  3. withRouter 函数

4.3 内置组件详解

基本的路由组件,可以做到跳转作用。

Link 的升级版,被点击的组件会自动添加 active(class),可以通过 activeClassName 指定激活时所添加的 class

react
  • 01
  • 02
  • 03
  • 04
<NavLink activeClassName="active" className="list-group-item" to="/about">About</NavLink> <NavLink activeClassName="active" className="list-group-item" to="/home">Home</NavLink>
  • 如果感觉每个 NavLink 都需要写 activeClassName 比较麻烦,可以自己封装一下 NavLink,封装公共属性。

    react
    • 01
    • 02
    • 03
    • 04
    • 05
    • 06
    • 07
    • 08
    export default class MyNavLink extends Component { render() { // console.log(this.props); // 不用写标签体,this.props 会将 children 解析出来。 return (<NavLink activeClassName="active" className="list-group-item" {...this.props}/>) } }

标签体的内容怎么传递?

标签体内容也是特殊的标签属性,标签体内容会自动传递到 props 的 children 属性。

4.3.4 Switch

当我们有一条路径存在两个匹配组件的时候,两个组件都会被展示。

react
  • 01
  • 02
  • 03
<Route path="/about" component={About}/> <Route path="/home" component={Home}/> <Route path="/home" component={Test}/>

Switch可以提高路由匹配效率(单一匹配),匹配上后就不继续匹配。

react
  • 01
  • 02
  • 03
  • 04
  • 05
<Switch> <Route path="/about" component={About}/> <Route path="/home" component={Home}/> <Route path="/home" component={Test}/> </Switch>

4.3.4 Redirect

可以用来重定向到路由

  1. 一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由

  2. 具体编码:

    react
    • 01
    • 02
    • 03
    • 04
    • 05
    <Switch> <Route path="/about" component={About}/> <Route path="/home" component={Home}/> <Redirect to="/about"/> </Switch>

4.4 BrowserRouter 与 HashRouter 的区别

  1. 底层原理不一样:

    • BrowserRouter 使用的是 H5 的 history API,不兼容 IE9 及以下版本。
    • HashRouter使用的是 URL 的哈希值。
  2. path 表现形式不一样

    • BrowserRouter 的路径中没有 #,例如:localhost:3000/demo/test
    • HashRoute r的路径包含 #,例如:localhost:3000/#/demo/test
  3. 刷新后对路由 state 参数的影响

    • BrowserRouter 没有任何影响,因为 state 保存在 history 对象中。
    • HashRouter 刷新后会导致路由 state 参数的丢失!!!

​ 4.备注:HashRouter 可以用于解决一些路径错误相关的问题。

5. 样式丢失问题

public 目录对应服务地址的根目录。

我们在 index.html 中引入 <link rel="stylesheet" href="./css/bootstrap.css"> 是没有问题的,当我们进入到路由组件 <Route path="/atguigu/about" component={About}/> 的时候也是没问题的,因为界面刷新是 ajax 局部刷新,页面并没有刷新,所以样式没有丢失。

但是当我们位于 /atguigu/about 刷新页面的时候样式就会丢失,因为当前路径下对应的 . 没有 bootstrap.css 文件,所以会丢失。

有以下解决方案

  1. public/index.html 中 引入样式时不写 .// (常用)

  2. public/index.html 中 引入样式时不写 ./%PUBLIC_URL% (常用)

  3. 使用 HashRouter

    • 使用 HashRouter 时路径为 http://localhost:3000/#/atguigu/home, # 号后面为前端资源,不带给服务器,我们刷新界面时服务器路径还是 http://localhost:3000,对应的还是 public 目录。

6. 路由匹配

路由匹配分为严格匹配和模糊匹配。

1.默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)

2.开启严格匹配:<Route exact={true} path="/about" component={About}/>

3.严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由

我们看个例子,这个例子中 Home 组件能成功展示,因为使用了模糊匹配。

react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
<div className="row"> <div className="col-xs-2 col-xs-offset-2"> <div className="list-group"> <MyNavLink to="/about">About</MyNavLink> <MyNavLink to="/home/a/b">Home</MyNavLink> </div> </div> <div className="col-xs-6"> <div className="panel"> <div className="panel-body"> {/* 注册路由 */} <Switch> <Route path="/about" component={About}/> <Route path="/home" component={Home}/> </Switch> </div> </div> </div> </div>
  • 但是 /a/home/b 无法匹配,因为顺序不一致

我们添加 exact 后便无法匹配 Home 组件

react
  • 01
<Route exact path="/home" component={Home}/>

7. 嵌套路由使用

  1. 注册子路由时要写上父路由的 path 值

  2. 路由的匹配是按照注册路由的顺序进行的

7.1 效果

7.2 例子

App.js

react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
export default class App extends Component { render() { return (<div> <div className="row"> <div className="col-xs-offset-2 col-xs-8"> <Header/> </div> </div> <div className="row"> <div className="col-xs-2 col-xs-offset-2"> <div className="list-group"> {/* 在React中靠路由链接实现切换组件--编写路由链接 */} <MyNavLink to="/about">About</MyNavLink> <MyNavLink to="/home">Home</MyNavLink> </div> </div> <div className="col-xs-6"> <div className="panel"> <div className="panel-body"> {/* 注册路由 */} <Switch> <Route path="/about" component={About}/> <Route path="/home" component={Home}/> <Redirect to="/about"/> </Switch> </div> </div> </div> </div> </div>) } }

pages/Home/index.jsx

react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
import React, {Component} from 'react' import MyNavLink from '../../components/MyNavLink' import {Route, Switch, Redirect} from 'react-router-dom' import News from './News' import Message from './Message' export default class Home extends Component { render() { return (<div> <h3>我是Home的内容</h3> <div> <ul className="nav nav-tabs"> <li> <MyNavLink to="/home/news">News</MyNavLink> </li> <li> <MyNavLink to="/home/message">Message</MyNavLink> </li> </ul> {/* 注册路由 */} <Switch> <Route path="/home/news" component={News}/> <Route path="/home/message" component={Message}/> <Redirect to="/home/news"/> </Switch> </div> </div>) } }

pages/Home/News/index.jsx

react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
import React, {Component} from 'react' export default class News extends Component { render() { return (<ul> <li>news001</li> <li>news002</li> <li>news003</li> </ul>) } }

8. 向路由组件传递参数数据

8.1 效果

8.2 params 参数

  1. 路由链接(携带参数):<Link to='/demo/test/tom/18'}>详情</Link>

  2. 注册路由(声明接收):<Route path="/demo/test/:name/:age" component={Test}/>

  3. 接收参数:this.props.match.params

react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
export default class Message extends Component { state = { messageArr: [{id: '01', title: '消息1'}, {id: '02', title: '消息2'}, {id: '03', title: '消息3'},] } render() { const {messageArr} = this.state return (<div> <ul> {messageArr.map((msgObj) => { return (<li key={msgObj.id}> {/* 向路由组件传递params参数 */} <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link> </li>) })} </ul> <hr/> {/* 声明接收params参数 */} <Route path="/home/message/detail/:id/:title" component={Detail}/> </div>) } }
react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
const DetailData = [{id: '01', content: '你好,中国'}, {id: '02', content: '你好,尚硅谷'}, {id: '03', content: '你好,未来的自己'}] export default class Detail extends Component { render() { console.log(this.props); // 接收params参数 const {id, title} = this.props.match.params const findResult = DetailData.find((detailObj) => { return detailObj.id === id }) return (<ul> <li>ID:{id}</li> <li>TITLE:{title}</li> <li>CONTENT:{findResult.content}</li> </ul>) } }

8.3 search 参数

  1. 路由链接(携带参数):<Link to='/demo/test?name=tom&age=18'}>详情</Link>

  2. 注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>

  3. 接收参数:this.props.location.search

  4. 备注:获取到的 search 是 urlencoded 编码字符串,需要借助 querystring 解析

react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
export default class Message extends Component { state = { messageArr: [{id: '01', title: '消息1'}, {id: '02', title: '消息2'}, {id: '03', title: '消息3'},] } render() { const {messageArr} = this.state return (<div> <ul> {messageArr.map((msgObj) => { return (<li key={msgObj.id}> {/* 向路由组件传递search参数 */} <Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link> </li>) })} </ul> <hr/> {/* search参数无需声明接收,正常注册路由即可 */} <Route path="/home/message/detail" component={Detail}/> </div>) } }
react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
const DetailData = [{id: '01', content: '你好,中国'}, {id: '02', content: '你好,尚硅谷'}, {id: '03', content: '你好,未来的自己'}] export default class Detail extends Component { render() { console.log(this.props); // 接收search参数 const {search} = this.props.location const {id, title} = qs.parse(search.slice(1)) const findResult = DetailData.find((detailObj) => { return detailObj.id === id }) return (<ul> <li>ID:{id}</li> <li>TITLE:{title}</li> <li>CONTENT:{findResult.content}</li> </ul>) } }

8.4 state 参数

  1. 路由链接(携带参数):<Link to={{pathname:'/demo/test',state:{name:'tom',age:18}}}>详情</Link>

  2. 注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>

  3. 接收参数:this.props.location.state

  4. 备注:state 传递参数不会在地址栏显示,刷新也可以保留住参数

react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
export default class Message extends Component { state = { messageArr: [{id: '01', title: '消息1'}, {id: '02', title: '消息2'}, {id: '03', title: '消息3'},] } render() { const {messageArr} = this.state return (<div> <ul> {messageArr.map((msgObj) => { return (<li key={msgObj.id}> {/* 向路由组件传递state参数 */} <Link to={{ pathname: '/home/message/detail', state: {id: msgObj.id, title: msgObj.title} }}>{msgObj.title}</Link> </li>) })} </ul> <hr/> {/* state参数无需声明接收,正常注册路由即可 */} <Route path="/home/message/detail" component={Detail}/> </div>) } }
react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
const DetailData = [{id: '01', content: '你好,中国'}, {id: '02', content: '你好,尚硅谷'}, {id: '03', content: '你好,未来的自己'}] export default class Detail extends Component { render() { console.log(this.props); // 接收state参数 const {id, title} = this.props.location.state || {} const findResult = DetailData.find((detailObj) => { return detailObj.id === id }) || {} return (<ul> <li>ID:{id}</li> <li>TITLE:{title}</li> <li>CONTENT:{findResult.content}</li> </ul>) } }

为什么刷新界面 state 不会丢失?

因为 history 包含 location,刷新界面 history 还在,location 不会丢失,所以 state 还在。

9. 编程式路由导航

9.1 效果

9.2 路由跳转的方式

有 push 和 replace 两种模式,默认是 push。

replace 的开启方式

react
  • 01
<Link replace to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{*msgObj*.title}</Link>

9.3 history api

借助 this.prosp.history 对象上的 API 对操作路由跳转、前进、后退

  1. this.prosp.history.push()

  2. this.prosp.history.replace()

  3. this.prosp.history.goBack()

  4. this.prosp.history.goForward()

  5. this.prosp.history.go()

9.4 例子

react
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
export default class Message extends Component { state = { messageArr: [{id: '01', title: '消息1'}, {id: '02', title: '消息2'}, {id: '03', title: '消息3'},] } replaceShow = (id, title) => { //replace跳转+携带params参数 //this.props.history.replace(`/home/message/detail/${id}/${title}`) //replace跳转+携带search参数 // this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`) //replace跳转+携带state参数 this.props.history.replace(`/home/message/detail`, {id, title}) } pushShow = (id, title) => { //push跳转+携带params参数 // this.props.history.push(`/home/message/detail/${id}/${title}`) //push跳转+携带search参数 // this.props.history.push(`/home/message/detail?id=${id}&title=${title}`) //push跳转+携带state参数 this.props.history.push(`/home/message/detail`, {id, title}) } back = () => { this.props.history.goBack() } forward = () => { this.props.history.goForward() } go = () => { this.props.history.go(-2) } render() { const {messageArr} = this.state return (<div> <ul> {messageArr.map((msgObj) => { return (<li key={msgObj.id}> {/* 向路由组件传递state参数 */} <Link to={{ pathname: '/home/message/detail', state: {id: msgObj.id, title: msgObj.title} }}>{msgObj.title}</Link> &nbsp; <button onClick={() => this.pushShow(msgObj.id, msgObj.title)}>push查看 </button> &nbsp; <button onClick={() => this.replaceShow(msgObj.id, msgObj.title)}>replace查看 </button> </li>) })} </ul> <hr/> {/* state参数无需声明接收,正常注册路由即可 */} <Route path="/home/message/detail" component={Detail}/> <button onClick={this.back}>回退</button> &nbsp; <button onClick={this.forward}>前进</button> &nbsp; <button onClick={this.go}>go</button> </div>) } }

10. withRouter

还是以上面例子为例。

当我们想把按钮转移到 Header 中时,会发现 header 获取不到 this.props.history 对象,因为 Header 非路由组件,这时候可以使用 withRouter 解决。

react
  • 01
export default withRouter(Header)

withRouter 可以加工一般组件,让一般组件具备路由组件的所有特有 API