目录
React路由
前端路由原理
- BOM上有一个history属性 专门用于管理浏览器额路径、历史记录等 我们可以直接操作 但是写起来麻烦 所以我们借用一个库来操作
- 路由器router用于管理路由route,整个应用只能用一个路由器router来管理所有路由
- 浏览器的历史记录是一个栈的结构 你看到的页面就是栈顶的页面 你点击一个链接 就是往栈中push一个URL push的话可以后退 就是弹出当前页面 你点击一个链接 也可能是replace栈顶的URL 此时后退返回的是上上一个页面 因为上一个页面已经被取代了
- history.push可以往栈中推入一个URL
- history.listen可以监听路径的变化
- history.back后退
- history.forward前进
- 前端路由靠的就是BOM身上的history
- 锚点跳转不会刷新页面但是会留下历史记录
- history有两种工作模式:
(1)let history = History = History.createBrowserHistory()// 方法一 直接使用H5推出的history身上的API
(2)let history = History.createHashHistory()// 方法二 hash值(锚点)
react-router-dom的理解
- 安装:
npm i react-router-dom --save
- 使用路由的套路:首先找到导航区和展示区,点击导航区的不同导航选项(即路由链接),会在展示区展示对应的内容,前端路由的原理:点击导航区的导航,引起历史记录中路径的变化,被前端路由器监视到,发现路径是/home,就把相应的组件展示出来。总的来说也就两步:点击导航链接,引起路径改变;路径的变化被路由器检测到,进行匹配组件,从而展示相应的组件
- App.jsx中的代码:
import React, { Component } from 'react'
import {Link,Route} from 'react-router-dom'
import Home from './components/Home'
import About from './components/About'
// 项目一启动 展示的就是APP中的内容
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标签 link标签是react中的 所以最终link标签会转成a标签 to属性转成href属性 */}
{/* 下面一行代码会转成这个:<a class="list-group-item" href="/about">About</a> */}
<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">
{/* 注册路由:编写这个路径对应这个路由 即路由/链接与组件的关系 */}
{/* 如果你的路径变成了/about 展示About这个组件 About是你最上面用import引入的那个组件 */}
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</div>
</div>
</div>
</div>
</div>
)
}
}
Link和Route标签都必须被路由器管理(被Router包裹),路由器有两种:BrowserRouter和HistoryRouter,BrowserRouter较常用,本文除特殊说明外都使用BrowserRouter。index.js代码如下:
import React from 'react'//引入react核心库
import ReactDOM from 'react-dom'//引入ReactDOM
import {BrowserRouter} from 'react-router-dom'
import App from './App'//引入App
ReactDOM.render(
// 在APP.jsx中用到的Link标签和Route标签都必须包裹在Router中,也就是说路由需要被一个路由器管理
// 所以在index.js中 用这一个路由器直接包裹App
<BrowserRouter>
<App/>
</BrowserRouter>,
document.getElementById('root')
)
- BrowserRouter与HashRouter的区别
(1)底层原理不一样:
BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。
HashRouter使用的是URL的哈希值。
(2)path表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test
(3)刷新后对路由state参数的影响
BrowserRouter没有任何影响,因为state保存在history对象中。
HashRouter刷新后会导致路由state参数的丢失!!!因为他没有借助history对象来保存
(4)备注:HashRouter可以用于解决一些路径错误相关的问题。 - 路由组件与一般组件
(1)一般组件使用时<Home />
,路由组件使用时<Route path="/home" component={Home}/>
(2)一般组件放在components文件夹下,路由组件放在pages文件夹下
(3)在不传参数的情况下,路由组件About会收到路由器传递的参数,一般组件Header收不到任何参数
其实,history.location = location
,并且location.pathame、match.path、match.url都可以获取当前路由的路径
NavLink的使用
我们希望点击页面上的路由,给link标签追加一个类名,让选项卡在点击时呈现不一样的状态。那就不要用之前的link标签,用Link标签的升级版NavLink。App.jsx中的代码:
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 在React中靠路由链接实现切换组件--编写路由链接 */}
{/* NavLink有个属性activeClassName 当点击NavLink时,会将activeClassName的值追加到className属性中*/}
{/* 你可以新建一个.css文件 然后将atguigu这个样式写在里面
你也可以写在index.html中 因为最后所有的东西都会放在index.html中 */}
<NavLink activeClassName="atguigu" className="list-group-item" to="/about">About</NavLink>
<NavLink activeClassName="atguigu" className="list-group-item" to="/home">Home</NavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由 */}
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</div>
</div>
</div>
</div>
封装NavLink:自定义MyNavLink组件
如果我们要编写很多的路由链接,就需要写很多上面的代码,会产生很多的冗余代码,所以我们可以把NavLink封装起来,自定义MyNavLink组件,以后只用写下面的代码:
将app.jsx中编写路由链接的代码改为以下:to和title都是传给MyNavLink组件的参数
MyNavLink组件的代码如下:从props中获取参数(通过解构赋值获取的) 然后展示出来,props本身就是属性的意思,上面传参数也是作为组件的属性传过来的,props就是用来接受标签属性的
但是如果我们要传100个参数,我们又要写100个to="/about" title="About" a="a"……
,所以我们可以批量传数据过去,你在app.jsx中不管传多少数据过去,都会在props中
所以在MyNavLink组件中,可以直接通过解构赋值获得数据,但是其他数据放在标签中作为属性 ,title是放在标签体中,放的位置不一样,所以title还是需要单独获取出来。
你现在看我们之前在app.jsx中使用NavLink标签的代码:
是不是不是自闭和的标签 但是我们现在封装的MyNavLink组件是自闭和的,需要靠传入的title属性来决定标签体展示什么
但是我们要怎么接收标签体内容呢?propos只能接收标签属性,注意:标签体内容也是一个特殊的标签属性,键是children,值是标签体中的内容。
现在我们MyNavLink组件中的代码可以写成:
NavLink标签中的内容,即标签体内容{this.props.children},是标签的children属性的值,当我们写一个标签时,不写标签体内容,而是给标签写children属性,也可以指定标签体中的内容,所以MyNavLink组件中的代码可以写成,直接解构传过来的参数就完事:
switch的使用
如果我们在注册路由时,写了下面的代码,会出现什么情况:点击路由后,会展示Home、Test组件
当你的路径变成/home时,他会一个个匹配你注册的路由:第一个路径不同,继续匹配下一个,路径相同,匹配上了,展示相应的组件,虽然匹配上了,但是他还会继续匹配,下一个路径又匹配上了,又展示相应的组件
如果我写的路由特别多(如下图),按照上面的匹配规则,效率特别低,如果我们现在的路径是/about,他匹配完了第一行,又会继续匹配剩下的路由,没必要啊,我们怎么让他匹配到了就终止呢
我们可以使用switch组件,把注册的所有路由包裹起来,如果你不用switch,你找到了路径还会继续往下找,用了switch之后,你找到了路径就不会继续往下找了,当然,像下面这种情况(下面的代码在switch组件中),也就只会展示Home组件,但是一般情况下,我们就是一个路径对应一个组件啊,一般不会出现下面的情况
解决多级结构刷新页面样式丢失问题
我们现在使用的是BrowserRouter,希望路径变成/atguigu/about后展示About组件,变成/atguigu/home后展示Home组件,App.jsx中的代码:
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 在React中靠路由链接实现切换组件--编写路由链接 */}
<MyNavLink to="/atguigu/about">About</MyNavLink>
<MyNavLink to="/atguigu/home">Home</MyNavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由 */}
<Switch>
<Route path="/atguigu/about" component={About}/>
<Route path="/atguigu/home" component={Home}/>
</Switch>
</div>
</div>
</div>
</div>
虽然我们实现了这个功能,页面也可以正常展示,但是当我们刷新页面后,页面样式就没有了,页面样式是bootstrap提供的,也就是说找不到bootstrap的资源了,bootstrap在我们项目的public/css/bootstrap.css中,也就是说我们通过访问http://localhost:3000/css/bootstrap.css
获取资源,现在刷新了获取不到资源了,因为现在他会去http://localhost:3000/atguigu/css/bootstrap.css
获取资源,我们没有atguigu这个文件夹,所以获取不到。
在解决这个问题前,我们先了解一下http://localhost:3000/css/bootstrap.css
:http://localhost:3000
是我们用devServer开起来的服务器,就是我们的脚手架。react的脚手架中,通过webpack配置了一个东西,使得public文件夹就是http://localhost:3000
这台内置服务器的根路径,所以上述其实访问的是public/css/bootstrap.css。
如果你请求的资源不存在,那么我会把publi/index.html返回给你
如何让页面样式不丢失呢?
(1)public/index.html中引入bootstrap时,不要写<link rel="stylesheet" href="./css/bootstrap.css">
,要写<link rel="stylesheet" href="/css/bootstrap.css">
,因为href="./css/bootstrap.css"
表示从当前文件出发,去当前文件夹下找,此时会把/atguigu带上,请求的资源就变成了http://localhost:3000/atguigu/css/bootstrap.css
,而href="/css/bootstrap.css"
表示从http://localhost:3000
下去找/css/bootstrap.css,即请求http://localhost:3000/css/bootstrap.css
这个资源
(2)public/index.html中引入bootstrap时,不要写<link rel="stylesheet" href="./css/bootstrap.css">
,要写:<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css">
,%PUBLIC_URL%
代表public的绝对路径,所以其实还是去public/css/bootstrap.css中获取资源。(这种方法只有在react中可以使用)
(3)public/index.html中引入bootstrap时,还是写<link rel="stylesheet" href="./css/bootstrap.css">
,但是在index.js中使用HashRouter,因为HashRouter会多一个#,#后边的内容都被认为时前端的资源,不会出现在HTTP请求中,根本不会带给服务器,index.js中代码如下:(这种方法不常用,因为我们一般都用BrowserRouter)
import React from 'react';//引入react核心库
import ReactDOM from 'react-dom';//引入ReactDOM
import {BrowserRouter, HashRouter} from 'react-router-dom'
import App from './App'//引入App
ReactDOM.render(
<HashRouter>
<App/>
</HashRouter>,
document.getElementById('root')
)
路由的模糊匹配与严格匹配
假如现在点击一个链接,路径变成了/home/a/b,但是我在注册路由时,/home才展示Home组件,app.jsx中代码如下所示,此时还是能匹配上的,这就是模糊匹配。其实他在匹配的时候,会先匹配home,匹配上了展示Home组件,然后匹配a,没找到/a这个路径,结束匹配,所以如果编写路由链接时写的是<MyNavLink to="/a/home/b">Home</MyNavLink>
,一个路由也匹配不上,他一上来没找到/a路径,直接结束匹配。
<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/a/b">Home</MyNavLink>
{/* 但是如果你写的是:<MyNavLink to="/a/home/b">Home</MyNavLink>
还是匹配不上,点击后不会展示Home组件*/}
</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>
精准匹配就是:编写路由链接的路经和注册路由的路径一致
开启精准匹配:给Route标签添加exact={true}
属性或直接写exact
开启严格匹配的原则:没有耽误页面的呈现,没有引发其他的问题,不开启,如果不开启会出bug,那么就开启。
<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/a/b">Home</MyNavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由 */}
<Switch>
<Route exact path="/about" component={About}/>
<Route exact path="/home" component={Home}/>
</Switch>
</div>
</div>
</div>
</div>
Redirect的使用
希望一打开页面,就帮我默认选中一个选项卡,并展示相应的组件,使用重定向来实现。
http://localhost:3000
等同于http://localhost:3000/
,只不过在地址栏中,这个/被隐藏了,那么既然有/,他还是会去注册的路由中查找路径为/的路由的,但是我们前面的代码没注册过这种路由,所以他找不到,页面不展示任何组件。可以使用Redirect组件设置,没有匹配得上的路径时,跳转去哪里:
<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标签写在所有路由的最后面,前面的路径都不匹配,
就去Redirect标签的to指向的路径 */}
<Redirect to="/about"/>
</Switch>
</div>
</div>
</div>
</div>
嵌套路由/多级路由
我们现在想实现这样的功能,点击页面左栏Home这个选项卡,路径变为/home,展示右边的内容,在右边点击News选项卡,展示news组件的内容,点击Message选项卡,展示Message组件的内容。(如下图所示,红色框是导航区,蓝色框是展示区)
- 项目结构:(如下图所示)
(1)项目的App.jsx中编写了/about和/home路由链接并注册这两个路由
(2)项目的page文件夹中存放About和Home组件的代码,在Home组件存放News和Message组件的代码,并且在Home组件的index.js中编写News和Message组件的路由链接并注册这两个路由
- App.jsx中的代码如下:
<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>
- 在Home组件的index.js中如何编写News和Message组件的路由链接呢?如果是写
<MyNavLink to="/news">News</MyNavLink>
,那么点击后路径会变为http://localhost:3000/news
,会触发路由的匹配,每次路由的匹配都是从最开始注册到最后注册的流程走下去的,App.jsx中的路由是最先注册的,所以他会先去App.jsx中注册的路由中匹配路径,此时我们的路径是/news,但是看上面的代码我们压根没有路径为/news的组件,最后由于重定向,路径变为/about,所以子组件的路径必须带上父组件路径,写为<MyNavLink to="/home/news">News</MyNavLink>
,具体代码如下:
<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>
点击News选项卡后路径变为/home/news,触发路由的匹配,先去App.jsx中注册的路由中匹配路径,由于模糊匹配,路径/home/news和/home匹配上了,展示Home组件,Home组件就要挂载上去,Home组件中的index.js文件就执行了,index.js又注册了路由,继续匹配路由,我们的路径是/home/news,index.js注册的路由也有路径为/home/news的组件,匹配上了,展示News组件
- 嵌套路由/多级路由必须规规矩矩带上父组件的路径
向路由组件传递参数数据
我们现在有个页面,点击页面左栏Home这个选项卡,路径变为/home,展示右边的内容,在右边点击Message选项卡,展示message组件的内容(展示Message01、Message03、Message06),点击Message01/03/06,在下方展示数据,展示的数据内容不一样,但是格式一样。(如下图所示,红色框是导航区,蓝色框是展示区)
我们把下面这个组件定义为Detail,点击Message01/03/06都展示Detail组件。
项目结构如下:
由于点击Message01/03/06,在下方展示的数据内容不一样,但是格式一样。所以我们可以点击Message01/03/06的时候给Detail组件传递参数,Detail组件根据参数展示相应的数据
不论怎么传递参数都有三个步骤:
(1)怎么携带
(2)怎么声明接收
(3)怎么具体接收/怎么取出使用
传递params参数
- 编写路由链接时带上参数:
to="/home/message/detail/xx"
,xx就是参数,这样就能让链接上带上参数 - 注册路由时接收参数:
path="/home/message/detail/:id"
,以后这个参数就存在该路由(/home/message/detail
)的id变量中
Message的index.js的代码:
import React, { Component } from 'react'
import {Link,Route} from 'react-router-dom'
import Detail from './Detail'
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}>
{/* 1. 向路由组件传递params参数
之前的代码:<Link to="/home/message/detail">{msgObj.title}</Link>
to后面跟的是个字符串 但是我们现在的参数是msgObj.id是个变量,所以用模板字符串
模板字符串是js里的东西,要使用得用{}包裹 在模板字符串中字符串和变量拼接要用${msgObj.id}
*/}
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr/>
{/* 2. 声明接收params参数 声明后,Detail就能获得参数,你前面那样写只能说明点击连接后携带了参数,
下面这样写才让组件收到了参数,接收到第一个参数放在id里,第二个参数放在title里*/}
<Route path="/home/message/detail/:id/:title" component={Detail}/>
</div>
)
}
}
- 在Detail组件中打印this.props发现这个id就存在this.props.match.params中,如下图所示
Detail组件可以取出参数使用,Detail的index.js中代码如下:
import React, { Component } from 'react'
const DetailData = [
{id:'01',content:'你好,中国'},
{id:'02',content:'你好,尚硅谷'},
{id:'03',content:'你好,未来的自己'}
]
export default class Detail extends Component {
render() {
console.log(this.props);
// 3. 接收到得参数放在this.props.match.params中,获取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>
)
}
}
传递search参数
- 编写路由链接时带上参数:
to="/home/message/detail/?id=xx&title=xxx"
,xx和xxx就是参数,id和title时变量名,这样就能让链接上带上参数,?标识这是传递search参数,无需声明接收,正常注册路由即可 - 注册路由时接收参数:
path="/home/message/detail"
Message的index.js的代码:
import React, { Component } from 'react'
import {Link,Route} from 'react-router-dom'
import Detail from './Detail'
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参数,这个?就代表了要传递search参数,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>
)
}
}
- 在Detail组件中打印this.props发现传递过来的参数存在this.props.location.search中,如下图所示
但是这个参数是?id=xx&title=xxx
这种形式,这种键值对用等号分割,多个键值对用&连接的编码方式称为urlencoded,我们可以使用querystring这个库进行对象和urlencoded相互转换,react脚手架已经下载好了,直接import引入使用即可,但是由于参数是?id=xx&title=xxx
,多了个?,所以我们需要先截取再转换,Detail的index.js中代码如下:
import React, { Component } from 'react'
import qs from 'querystring'// 用来进行对象和urlencoded相互转换的库
// querystring的使用
let obj = {name:'tom',age:18}
console.log(qs.stringify(obj))// name=tom&age=18 这就是urlencoded编码
let str = "car=奔驰&price=199";
console.log(qs.parse(str))// {car:"奔驰",price:199}
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))// 因为我们获取的search参数开头还带了个? 所以这里先截取一下再转换
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参数
注意:这个state是路由组件身上独有的属性,不是组件里的状态。
- 之前编写路由链接时,to一直写的是字符串,如果你要传state参数,to要写成对象,但是在jsx中{}代表里面要写js语法,所以你要写对象要包两层{{在这里面写键值对}}(里面那个{在这里面写键值对}才是对象),对象里面的属性pathname代表点击后要跳转的链接地址,state里面存放你要传递的数据,他的值也是个对象:
to={{pathname:'/home/message/detail',state:{键值对}}}
- 因为时传递state参数,注册路由时无需声明接收,正常注册路由即可
Message的index.js的代码:
import React, { Component } from 'react'
import {Link,Route} from 'react-router-dom'
import Detail from './Detail'
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>
)
}
}
- 在Detail组件中打印this.props发现传递过来的参数存在this.props.location.state中,如下图所示
由于我们现在使用的是BrowserRouter,他一直在操作浏览器中的history,state是history的一个属性,所以他其实有记录的,所以刷新也可以保留参数,但是如果你把缓存清空了/把浏览器关了,再打开http://localhost:3000/home/message/detail
就没有参数了,并且会报错,为了避免这种情况,在获取不到参数时,我们让他为空对象。Detail的index.js中代码如下:
import React, { Component } from 'react'
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>
)
}
}
路由跳转的两种模式
路由其实就是对浏览器的历史记录进行操作,一共有两个操作,一个是push(压栈的方式,一个压一个,会留下痕迹),一个是replace(替换掉目前这个)。路由跳转默认使用push,如何让路由跳转为replace:Link、NavLink标签有个属性replace,将其设为true就是使用replace模式。Message的index.js的代码:
return (
<li key={msgObj.id}>
{/* 向路由组件传递state参数 */}
<Link replace to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link>
</li>
)
编程式路由导航
- 我们现在点击选项卡可以切换路由,靠的是Link、NavLink标签。现在我想实现:进入news组件3s后,自动跳转到message组件,如何实现?你之前用的Link、NavLink标签首先要有人点击他才会跳转,我们如何在不借助路由链接(Link、NavLink标签)的前提下,写一些代码实现路由跳转?这些代码就称为编程式路由导航。
- 我们现在有个页面,点击页面左栏Home这个选项卡,路径变为/home,展示右边的内容,在右边点击Message选项卡,展示message组件的内容(展示消息1/2/3),消息1/2/3右边有两个按钮,点击第一个push查看消息1/2/3中的内容,点击第二个replace查看消息1/2/3中的内容,在下方展示数据(如下图所示)
- 前面我们说了,我们是通过操作浏览器中的history实现的路由跳转,那么我们同样可以通过直接操作history实现路由跳转,history中的api如图所示
- 同理,你也可以写两个按钮,一个前进一个后退,通过直接操作history的API实现前进后退,无需点浏览器左上角的前进后退按钮。Message的index.js的代码:
import React, { Component } from 'react'
import {Link,Route} from 'react-router-dom'
import Detail from './Detail'
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参数 replace/push的第二参数可选,就是state参数
// {id,title}是{id:id,title:title}的缩写
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)// go(n):n>0,前几n步,n<0,后退n步
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> */}
{/* 向路由组件传递search参数 */}
{/* <Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link> */}
{/* 向路由组件传递state参数 */}
<Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link>
<button onClick={()=> this.pushShow(msgObj.id,msgObj.title)}>push查看</button>
<button onClick={()=> this.replaceShow(msgObj.id,msgObj.title)}>replace查看</button>
</li>
)
})
}
</ul>
<hr/>
{/* 声明接收params参数 */}
{/* <Route path="/home/message/detail/:id/:title" component={Detail}/> */}
{/* search参数无需声明接收,正常注册路由即可 */}
{/* <Route path="/home/message/detail" component={Detail}/> */}
{/* state参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail}/>
<button onClick={this.back}>回退</button>
<button onClick={this.forward}>前进</button>
<button onClick={this.go}>go</button>
</div>
)
}
}
- 实现:进入news组件2s后,自动跳转到message组件
import React, { Component } from 'react'
export default class News extends Component {
componentDidMount(){// 组件一创建,开启定时器,2s后跳转到message组件
setTimeout(()=>{
this.props.history.push('/home/message')
},2000)
}
render() {
return (
<ul>
<li>news001</li>
<li>news002</li>
<li>news003</li>
</ul>
)
}
}
withRouter的使用
只有路由组件才有history对象,才能像上面那样直接操作history的api实现路由的跳转,一般组件没有,我们可以通过withRouter让一般组件也有history对象。withRouter是react-router-dom身上的一个函数,他接收一个一般组件,然后就给这个一般组件的身上加上了路由组件所特有的API,返回一个新组件。比如说我们现在想把前面的前进后退按钮放在Header组件中,你就把Header组件当成路由组件来用,最后导出时用withRouter包裹以下让他变成路由组件即可,前进后退功能依旧能用
import React, { Component } from 'react'
import {withRouter} from 'react-router-dom'
class Header extends Component {
back = ()=> this.props.history.goBack()
forward = ()=> this.props.history.goForward()
go = ()=> this.props.history.go(-2)
render() {
console.log('Header组件收到的props是',this.props);
return (
<div className="page-header">
<h2>React Router Demo</h2>
<button onClick={this.back}>回退</button>
<button onClick={this.forward}>前进</button>
<button onClick={this.go}>go</button>
</div>
)
}
}
export default withRouter(Header)
antd
antd的基本使用
- 安装:
npm install antd --save
- 从库中import你要用的东西,在你要用的东西那一页都有
- 复制代码粘贴到你想放置的地方
- 可以在页面的API中进行其他配置
UI组件库:elementUI(饿了么)、antd(蚂蚁)、vant(常用于移动端,有赞团队)
antd样式的按需引入
在node_modules/antd/dist/antd.css中放着antd的所有样式,要想使用antd,就必须引入这个样式:import 'antd/dist/antd.css'
,但是他很大,没必要全部引入,因为有些样式你可能压根没用到,你可以按需引入,用到哪个组件就引入哪个组件的样式(在官网看3.x的文档更详细),需要修改create-react-app的默认配置,有两种方法:
- create-react-app是react脚手架自己生成的默认配置,我们要想修改,首先需要将脚手架的配置暴露出来,暴露出来后就隐藏不了了,操作不可逆。所以我们在桌面进入cmd,输入
create-react-app demo
创建一个新的脚手架,webstorm打开新的脚手架,在终端输入react-scripts eject
,在config/webpack.config.js中修改,但是这样一旦改错了,脚手架就起不来了 - 自己写代码,让customize-cra去帮我们改,首先要下载customize-cra,在终端输入:
yarn add create-react-app customize-cra
,然后在config-overrides.js中写代码,用customize-cra这个库按照config-overrides.js中的内容修改了脚手架,就不能再用脚手架原来的启动命令了,你必须用create-react-app来启动,在package.json中将"scripts"
修改为:
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
以后在终端还是通过npm start
来启动项目,app.jsx代码如下:
import React, { Component } from 'react'
import { Button,DatePicker } from 'antd';// 像这种不写路径直接写包名的,他会直接去node_modules中找
// 在node_modules/antd/dist/antd.css中放着antd的所有样式
// import 'antd/dist/antd.css'
import {WechatOutlined,WeiboOutlined,SearchOutlined} from '@ant-design/icons'
const { RangePicker } = DatePicker;
export default class App extends Component {
render() {
return (
<div>
App....
<button>点我</button>
<Button type="primary">按钮1</Button>
<Button >按钮2</Button>
<Button type="link">按钮3</Button>
<Button type="primary" icon={<SearchOutlined />}>
Search
</Button>
<WechatOutlined />
<WeiboOutlined />
<DatePicker/>
<RangePicker/>
</div>
)
}
}
然后在package.json的同级目录中创建config-overrides.js,在config-overrides.js中写代码:
const { override, fixBabelImports} = require('customize-cra');
module.exports = override(
fixBabelImports('import', {// 按需引入,这里些import
libraryName: 'antd',// 按需引入的库antd
libraryDirectory: 'es',// antd中用了es的模块化规范
style: css//你要对css进行按需引入
})
);
antd自定义主题
antd的样式都使用less写的,要想改antd的样式,需要自己改less文件然后编译成css,所以首先在终端安装less和less-loader:yarn add less less-loader
,然后通过customize-cra中提供的less相关函数addLessLoader来帮助加载less样式,同时修改config-overrides.js中的代码如下:
const { override, fixBabelImports,addLessLoader} = require('customize-cra');
module.exports = override(
fixBabelImports('import', {// 按需引入,这里些import
libraryName: 'antd',// 按需引入的库antd
libraryDirectory: 'es',// antd中用了es的模块化规范
style: true,// 自定义主题时写这行
}),
//自定义主题写下面的代码:
addLessLoader({
lessOptions:{
javascriptEnabled: true,// 允许用js修改底层的less文件
modifyVars: { '@primary-color': 'green' },// 修改哪些变量 其他变量参考官网
}
}),
);
redux
- redux是一个专门用于做状态管理的JS库(不是react插件库)。
- 它可以用在react, angular, vue等项目中, 但基本与react配合使用。
- 作用: 集中式管理react应用中多个组件共享的状态。
- 什么情况下需要使用redux:
(1)某个组件的状态,需要让其他组件可以随时拿到(共享)。
(2)一个组件需要改变另一个组件的状态(通信)。
(3)总体原则:能不用就不用, 如果不用比较吃力才考虑使用。 - redux工作流程:
页面上有个展示区,展示当前的数字,初始值为0,有个下拉框,下拉框中有一些数字,页面上还有四个按钮:+、-、×、÷,你点击下拉框中的数字后点某个按钮,就会执行当前数字+/-/×/÷你点击的数字。假如现在你在下拉框中选中了2,然后点了+,如果下拉框、按钮、当前数字都在组件本身,那么你可以自己处理了,但是现在当前数字在redux中,那你需要通知redux将值+2,你需要把值+2这件事告诉action creators,action creators用来创建action,action被称为动作对象,+/-/×/÷这都叫动作,动作对象包含着你本次的动作类型type及你本次操作的数据data,然后由dispatch将action分发下去,送给store,dispatch是个函数,接收的参数就是action,store相当于redux的指挥者,store将之前的状态和action一起交给reducers,reducers根据action执行完操作后将新状态返回给store,之后,组件可以通过getState()从store中获取状态。
reducers不仅可以加工状态,还可以初始化状态,前面我们讲的就是加工状态,初始化状态时action的type就是初始化状态@@init@@
,data是初始化的值(也可以不传),dispatch将action送给store,store将undefined和action一起交给reducers,传递的就不是之前的状态了。
(1)action:动作的对象,包含2个属性,例:{ type: 'ADD_STUDENT',data:{name: 'tom',age:18} }
type:标识属性, 值为字符串, 唯一, 必要属性
data:数据属性, 值类型任意, 可选属性
(2)reducer:用于初始化状态、加工状态。加工时,根据旧的state和action, 产生新的state的纯函数。每个组件都有一个reducer
(3)store:将state、action、reducer联系在一起的对象
如何得到此对象?
- import {createStore} from ‘redux’
- import reducer from ‘./reducers’
- const store = createStore(reducer)
此对象的功能?
- getState(): 得到state
- dispatch(action): 分发action, 触发reducer调用, 产生新的state
- subscribe(listener): 注册监听, 当产生了新的state时, 自动调用
- redux的安装:终端输入:
yarn add redux
求和案例
需求:页面上有个展示区,展示当前的数字,初始值为0,有个下拉框,下拉框中有一些数字,页面上还有四个按钮:+、-、当前数字为奇数则加、异步加,你点击下拉框中的数字后点+/-按钮,就会执行当前数字+/-你点击的数字
纯react版本
在组件内部实现,不适用redux,点击按钮后执行方法,该方法对组件内的数据进行操作
import React, { Component } from 'react'
export default class Count extends Component {
state = {count:0}
//加法
increment = ()=>{
const {value} = this.selectNumber// 字符串,后面*1将他转为数字
const {count} = this.state
this.setState({count:count+value*1})
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
const {count} = this.state
this.setState({count:count-value*1})
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
const {count} = this.state
if(count % 2 !== 0){
this.setState({count:count+value*1})
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
const {count} = this.state
setTimeout(()=>{
this.setState({count:count+value*1})
},500)
}
render() {
return (
<div>
<h1>当前求和为:{this.state.count}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
redux精简版
不使用action creators创建action,我们自己创建action,action就是一个对象。你在创建store的时候就已经有reducer了,你可以把store看成饭店老板,reducer就是厨师,店都开好了,老板肯定已经找好厨师了,所以我们先创建reducer。reducer可以初始化状态和加工状态,他会接收到store传给他的旧状态和action,然后对旧状态进行处理返回新状态给store,在js的世界里,谁能接收参数,然后处理逻辑,并返回结果?数组肯定不行吧,对象也不行吧,只有函数,所以reducer是个函数。reducer函数会接到两个参数:之前的状态(preState),动作对象(action)。count_reducer代码如下:
const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
// console.log(preState);
//从action对象中获取:type、data
const {type,data} = action
//根据type决定如何加工数据
switch (type) {
case 'increment': //如果是加
return preState + data
case 'decrement': //若果是减
return preState - data
default:
return preState
}
}
接下来创建store,store.js中的代码如下:
import {createStore} from 'redux'//引入createStore,专门用于创建redux中最为核心的store对象
import countReducer from './count_reducer'//引入为Count组件服务的reducer
//暴露store
//createStore中接收一个参数,这个参数就是一个reducer
export default createStore(countReducer)
以上,一个简单的redux就创建好了,接下来就在组件中写代码,点击按钮后,创建一个action对象传递给store,但是注意redux只负责操作,不负责渲染,他操作完了,数据改变了,但是数据并没有重新渲染,我们还需要手动重新渲染一下,在组件一创建时,就检测redux中状态的变化,只要变化,就调用render重新渲染、
组件的index.js中代码如下:
import React, { Component } from 'react'
import store from '../../redux/store'//引入store,用于获取redux中保存状态
export default class Count extends Component {
state = {carName:'奔驰c63'}// 这是你组件自己用的数据
/* componentDidMount(){
//检测redux中状态的变化,只要变化,就调用render
store.subscribe(()=>{// 订阅redux中状态的更改
//更新组件自己的state
//你一调setState,react就会帮你改状态,改完了他自动帮你调render重新渲染,但由于传入的是一个空对象,啥也不更新,所以骗了react一下
this.setState({})
})
} */
//但是如果你有3000个组件,每个组件里都要写上面的代码,冗余,所以我们在index.js中监听
increment = ()=>{//加法
const {value} = this.selectNumber
//把action传给store
store.dispatch({type:'increment',data:value*1})
}
decrement = ()=>{//减法
const {value} = this.selectNumber
store.dispatch({type:'decrement',data:value*1})
}
incrementIfOdd = ()=>{//奇数再加
const {value} = this.selectNumber
const count = store.getState()
if(count % 2 !== 0){
store.dispatch({type:'increment',data:value*1})
}
}
incrementAsync = ()=>{//异步加
const {value} = this.selectNumber
setTimeout(()=>{
store.dispatch({type:'increment',data:value*1})
},500)
}
render() {
return (
<div>
{/*store.getState()获取状态*/}
<h1>当前求和为:{store.getState()}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
但是如果你有3000个组件,每个组件里都要写componentDidMount(){...}
,冗余,所以我们在index.js中监听,index.js中代码如下:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './redux/store'
ReactDOM.render(<App/>,document.getElementById('root'))
//只要redux中的数据发生任何变化,我就更新整个<App/>,因为有diff算法,会找到变更的地方然后改变,局部渲染,这样并不会效率低
store.subscribe(()=>{
ReactDOM.render(<App/>,document.getElementById('root'))
})
redux完整版
使用action creators创建action,count_action.js中的代码如下:
import {INCREMENT,DECREMENT} from './constant'
/*
data:data精简为data
(data)=>{return {type:INCREMENT,data}} 精简为:
data => {type:INCREMENT,data} 这个{}会被认为是函数体那个{} 而不会认为你要返回一个对象
下面这样写 在外层包一个()他就知道你返回的是个对象
*/
export const createIncrementAction = data => ({type:'increment',data})
export const createDecrementAction = data => ({type:'decrement',data})
在组件中,通过调用action中的方法创建action,组件的index.js中代码如下:
import React, { Component } from 'react'
import store from '../../redux/store'//引入store,用于获取redux中保存状态
import {createIncrementAction,createDecrementAction} from '../../redux/count_action'//引入actionCreator,专门用于创建action对象
export default class Count extends Component {
state = {carName:'奔驰c63'}
increment = ()=>{//加法
const {value} = this.selectNumber
store.dispatch(createIncrementAction(value*1))
}
decrement = ()=>{//减法
const {value} = this.selectNumber
store.dispatch(createDecrementAction(value*1))
}
incrementIfOdd = ()=>{//奇数再加
const {value} = this.selectNumber
const count = store.getState()
if(count % 2 !== 0){
store.dispatch(createIncrementAction(value*1))
}
}
incrementAsync = ()=>{//异步加
const {value} = this.selectNumber
setTimeout(()=>{
store.dispatch(createIncrementAction(value*1))
},500)
}
render() {
return (
<div>
<h1>当前求和为:{store.getState()}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
其余代码不变
异步action版
action分为异步action和同步action:
(1)action是一个对象,就被称为同步action,因为同步action中没有异步的任务。
(2)action是一个函数,就被称为异步action,因为只有函数能开启异步任务,数组、基本数据类型都不行。
前面使用的就是同步action,我们在实现异步加这个效果时,在组件里等了500ms后将action传给store,传递的是同步action,等500ms这个动作是在组件中进行的,之前的代码如下:
incrementAsync = ()=>{
const {value} = this.selectNumber
setTimeout(()=>{
store.dispatch(createIncrementAction(value*1))
},500)
}
现在我们希望等500ms这个动作是在action中进行的,就需要异步action来实现。想象一下,假如你现在去餐厅吃饭,你可以等5分钟再点菜,然后厨师做好了立马给你端上来,你也可以对厨师说,来碗蛋炒饭,5分钟之后再上。厨师做好了等5分钟给你端上来,一个就是自身等5分钟,一个是厨师等5分钟,我们之前就是组件自身等500ms然后通知store,使用同步action实现,现在我们希望action自己等500ms创建再传给store,这就需要用到异步action。上述代码改为以下,之前store.dispatch
使用的是同步action方法,现在使用异步action方法
incrementAsync = ()=>{
const {value} = this.selectNumber
store.dispatch(createIncrementAsyncAction(value*1,500))
}
接下来在count_action.js中定义createIncrementAsyncAction
方法。首先,createIncrementAsyncAction
是一个函数,其次根据前面对异步action的定义知,异步action返回的action是一个函数,为什么是一个函数?因为只有函数能开启异步任务,所以在返回的这个函数里还需要开启异步任务,代码如下:
import store from './store'
export const createIncrementAsyncAction = (data,time) => {
return ()=>{
setTimeout(()=>{// 开启异步任务,异步action形成
store.dispatch(createIncrementAction(data))
},time)
}
}
我们在index.jsx中写的代码:store.dispatch(createIncrementAsyncAction(value*1,500))
,而createIncrementAsyncAction
这个函数返回的是一个函数,也就是向store提交的是个函数,但是store他接收对象,如果我们想让store接收函数并执行该函数需要借助中间件redux-thunk,安装:yarn add redux-thunk
,在store.js中使用中间件,让store可以接收一个函数并执行:
(1)从redux中引入applyMiddleware用于执行中间件
(2)引入redux-thunk,用于支持异步action
(3)applyMiddleware(thunk)
执行中间件redux-thunk,并作为第二个参数传入createStore,用于支持异步action
store.js中具体代码如下:
//引入createStore,专门用于创建redux中最为核心的store对象。1. applyMiddleware用于执行中间件
import {createStore,applyMiddleware} from 'redux'
import countReducer from './count_reducer'//引入为Count组件服务的reducer
import thunk from 'redux-thunk'//2. 引入redux-thunk,用于支持异步action
//暴露store。3. applyMiddleware(thunk)执行中间件redux-thunk,并作为第二个参数传入createStore,用于支持异步action
export default createStore(countReducer,applyMiddleware(thunk))
回到count_action.js中,createIncrementAsyncAction
返回的这个函数本身就是store帮你调用的,所以count_action.js不用引入store,也不用store.dispatch()
进行分发,createIncrementAsyncAction
返回的这个函数本身就是store帮你调用的,store在调用她的时候会将dispatch作为参数传入,所以count_action.js中createIncrementAsyncAction
定义如下:
export const createIncrementAsyncAction = (data,time) => {
return (dispatch)=>{
setTimeout(()=>{// 开启异步任务,异步action形成
dispatch(createIncrementAction(data))
},time)
}
}
异步action中一般都会调用同步action,异步action不是必须要用的。