redux与antd
redux是一个可以用在react上的全局状态管理
antd是蚂蚁金服开发的一套ui框架
使用antd快速搭建页面
安装包
npm i antd --save
简单测试是否安装成功
新建list.js
import React, { Component } from 'react'
import { Button } from 'antd';
import 'antd/dist/reset.css';
export default class list extends Component {
render() {
return (
<div>
<Button type="primary">Button</Button>
</div>
)
}
}
在router中设置List组件的跳转路由
import { BrowserRouter, Switch, Route } from 'react-router-dom';
//......
import List from '../views/list';
//react当中的路由管理对象是一个组件,所以我们这里采用函数组件编写
const Routers = () => {
return (
<BrowserRouter>
<Switch>
<Route path="/list" component={List}></Route>
......
</Switch>
</BrowserRouter>
)
}
export default Routers;
然后在浏览器手动输入list地址,没有问题的话我们可以看到一个蓝色的按钮(初次加载会稍微慢点)
案例制作
在assets文件夹中新建css文件夹用来储存不同组件的样式文件
这里我们新建list.css然后导入到list.js中
import React, { Component } from 'react'
import { Button,Input,List } from 'antd'; //导入需要使用的antd组件
import 'antd/dist/reset.css'; //导入antd组件样式
import '../assets/css/list.css' //导入list组件的样式
export default class list extends Component {
constructor(props){
super(props);
//准备需要被渲染的数据
this.state = {
arr:['zhangsan','lisi','wangwu']
}
}
render() {
return (
<div className='box'>
<div className="top">
<Input placeholder='请输入内容'></Input>
<Button className='m-l' type="primary">添加</Button>
</div>
<div className="bottom">
<List
bordered
dataSource={this.state.arr}
renderItem={item => (
<List.Item>{item}</List.Item>
)}
/>
</div>
</div>
)
}
}
list.css
.box{
width:600px;
margin:100px auto;
}
.top{
display: flex;;
}
.m-l{
margin-left:10px;
}
.bottom{
margin-top:15px;
}
代码分析:
上面一套组件代码中,我们主要是关注两个点
1、在父组件中修改子组件的样式:
我们之前在学习vue的时候知道,vue的组件有一个特性,可以通过style标签的scoped属性让样式封闭在组件中,但是这样也就意味着外部也无法修改,如果要修改需要通过v-deep实现,而react中没有这个情况,我们可以直接找到组件中标签的类名来直接修改,也可以添加新的类名来修改
2、antd的List组件的使用
这里antd组件的使用其实和elementPlus基本一模一样,所以对于已经学会使用elementPlus的人来说基本是直接就拿来用,
同时注意,我们现在使用的antd是针对PC端的,如果要适配移动端可以使用antd mobile
将Input组件制作成受控组件
现在Input组件输入的数据并不受React的state控制,其数据还是受DOM自身控制,所以我们现在要Input制作成受控组件
import React, { Component } from 'react'
import { Button,Input,List } from 'antd';
import 'antd/dist/reset.css';
import '../assets/css/list.css'
export default class list extends Component {
constructor(props){
super(props);
this.state = {
arr:['zhangsan','lisi','wangwu'],
textVal:'' //设置textVal储存Input的value的值
}
}
//制作获取Input的value值的方法,让其成为受控组件
getInputVal(event){
this.setState({
textVal:event.target.value
})
}
//将每次Input输入的内容添加到arr数组的前面,从而实时渲染新值的列表项
addItem(){
this.state.arr.unshift(this.state.textVal);
this.setState({
arr:this.state.arr,
textVal:'' //每次新增之清除之前输入的内容
})
}
render() {
return (
<div className='box'>
<div className="top">
<Input placeholder='请输入内容' value={this.state.textVal} onChange={this.getInputVal.bind(this)}></Input>
<Button className='m-l' type="primary" onClick={this.addItem.bind(this)}>添加</Button>
</div>
<div className="bottom">
<List
bordered
dataSource={this.state.arr}
renderItem={item => (
<List.Item>{item}</List.Item>
)}
/>
</div>
</div>
)
}
}
redux全局状态管理
安装包
npm i redux
新建store文件夹,然后新建两个js文件,index.js 和 reducer.js
先来看reducer.js 管理数据的文件
//reducer文件只负责两件事
//1、声明默认状态
//2、导出一个修改状态的函数
//defaultState默认数据
const defaultState = {
msg:"hello"
}
//导出一个函数,这个函数有两个参数,其实state的默认值是默认状态,这个方法会返回一个state
//这个导出的函数就是修改状态的方法
export default (state = defaultState,action) => {
return state
}
index.js 全局状态管理入口文件
import {createStore} from "redux" //导入创建store实例的方法
import reducer from './reducer' //导入要在实例中引入使用的数据
const store = createStore(reducer); //创建store实例,然后引用reducer方法
export default store //导出store实例
代码分析:
两个文件的关系有点类似于数据与数据管理员的关系,reducer.js负责储存操作数据,index.js负责管理reducer的数据
现在我们把上面案例中的数据使用全局状态管理的方式进行调用
把原来list中的数据删掉改写到reducer中,然后从reducer中再把数据调回到list中
//......
import store from '../store'
export default class list extends Component {
constructor(props){
super(props);
this.state = store.getState()
}
//......
}
代码分析:
从导入的store中使用getState方法来调出全局的状态在当前组件中使用,但是上面的写法有点问题,如果直接覆盖掉原组件中的state,可能会把组件自己的状态也覆盖掉,所以我们可以通过展开运算来获取全局状态
this.state = { ...store.getState( }
redux-devtools调试工具
我们可以给浏览器安装插件redux-devtools,这里我们就可以在开发工具中实时观察到全redux内的状态
edge浏览器直接在插件扩展的商店里面搜索redux-devtools安装
安装好之后,我们在store的index.js中要添加如下代码
import {createStore} from "redux" //导入创建store实例的方法
import reducer from './reducer' //导入要在实例中引入使用的数据
const store = createStore(reducer,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); //创建store实例,然后引用reducer方法
export default store //导出store实例
给createStore的第二个参数设置如下
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
配置好之后,我们继续会到list上操作之前做好的添加功能会发现,功能是可以正常运行的,但是新增的数据缺没有不同到redux中,相当于是现在新增的数据还是在list组件内部
原因:
我们在getInputVal方法中使用的是this.setState方法修改,这个方法的修改只能是针对组件自己的状态,而无法修改redux中调用的状态,所以我们需要改用redux的方法来进行修改
dispatch派发方法
我们从store中调用dispatch方法来修改
getInputVal(event){
//使用dispatch方法必须要传入一个action对象,所以我们创建一个action对象
let action = {
type:"textVal",
value:event.target.value
}
store.dispatch(action)
}
代码分析:
action对象中,有两个固定的属性分别是type和value
- type:表示本次修改的行为
- value:表示本次修改的实际值
然后把设置好的action传入dispatch方法的参数中
这里的action会传入到reducer.js中导出的函数的action参数中
在进入reducer.js中,在这个文件中导出的方法我们之前说过就是用来修改全局状态的方法,所以在方法内部开始编写修改业务
const defaultState = {
arr:['zhangsan','lisi','wangwu'],
textVal:''
}
export default (state = defaultState,action) => {
if(action.type === "textVal"){
state.textVal = action.value
}
return state
}
代码分析:
上面导出的方法中的action形参,会接收到在组件中的dispatch方法传入的action实参,然后我们在方法内部制作一个if判断,通过action中的type来决定当前action中的value要赋值给全局state中的哪个属性值上
我们可以通过刚才安装的redux-devtool工具来实时查看修改情况
注意:
- 这里还是会有问题,因为我们在组件中dispatch是通过onChange,所以每在Input中输入一个字符就会修改一次全局state中的textVal,导致我们没有办法将完整的数据写入到全局state中
- 因为我们现在取消了组件中的setState,所以导致Input中的数据无法双向绑定更新,永远都是初始值的 “ ” 空字符串
subscribe订阅方法
解决上面的问题我们会使用到subscribe订阅方法,之前我们在vue3中使用pinia的时候就有讲过这个方法,vuex里面也有这个方法,这个方法的本质就是一个监听器,用于监听全局状态的变化,当全局状态发生变化的时候执行一个操作
在list组件中我们再创建一个方法用于获取每次修改全局state修改之后的数据
viewChange(){
this.setState(store.getState())
}
代码分析:
现在的情况是redux中的state现在虽然可以更新了,但是渲染在界面上的数据还没有更新,所以我们这里需要获取到最新的redux中的state,然后通过setState进行双向绑定
然后我们需要从store中调用subscribe方法,当redux中的全局state放生变化的时候执行 viewChange
方法,让组件中的state进行实时更新,再通过setState方法实时渲染到界面上
//......
constructor(props){
super(props);
this.state = {
...store.getState()
}
store.subscribe(this.viewChange.bind(this))
}
//......
代码分析:
我们之前在讲生命周期的时候就讲过constuctor就是一个挂载阶段的生命周期函数,所以我们在list组件被载入的时候就执行了 subscribe 方法开始监听store中的全局state,而我们只要在Input中输入了数据,就会触发onChange事件从而执行 getInputVal 方法,而getInputVal方法的执行会向redux中传入action修改全局state,而全局state被修改就会触发 subscribe 方法去执行 viewChange 方法,从而完成了这样一个反复循环的过程
实现了全局state和页面数据的实时修改之后,我们就开始把数组的部分也进行修改,把界面中的列表部分也实现同步更新
修改addItem方法
addItem(){
let action = {
type:"arr"
}
store.dispatch(action)
}
代码分析:
这里不需要设置value,因为需要新增到数组中的数据已经在之前的Input的输入过程中已经同步到全局state中的textVal中了
进入reducer.js中修改导出方法的业务逻辑
//defaultState默认数据
const defaultState = {
arr:['zhangsan','lisi','wangwu'],
textVal:''
}
export default (state = defaultState,action) => {
if(action.type === "textVal"){
state.textVal = action.value
}else if(action.type === "arr"){
state.arr.unshift(state.textVal)
//把新输入的数据放入数组之后,把textVal清空,这样同步的界面中的input也会被清空
state.textVal = ""
}
return state
}
制作删除功能
因为我们这里的数据都是从全局状态中拿来的,所以删除就相当于是把原本添加到全局arr中的数据再删除掉,所以需要把之前的添加逻辑再跑一遍,只不过在reducer中的业务逻辑要做点修改
在list的标签结构上添加删除按钮
<List.Item>
<span>{item}</span>
<Button type="primary" danger>删除</Button>
</List.Item>
制作删除方法
delItem(index){
let action = {
type:"del",
value:index
}
store.dispatch(action)
}
代码分析:
这里需要通过delItem方法删除的是全局状态,所以必须要使用dispatch方法,而删除全局状态中额数组元素时需要对应的索引,所以我们将索引通过action的value传递给reducer
绑定delItem方法
<List
bordered
dataSource={this.state.arr}
renderItem={(item,index) => (
<List.Item>
<span>{item}</span>
<Button onClick={this.delItem.bind(this,index)} type="primary" danger>删除</Button>
</List.Item>
)}
/>
代码分析:
上面有说到删除需要用到索引,而索引的获取我们可以通过antd组件List提供的renderItem获取到,这个renderItem的使用就类似与vue中的v-for指令,可以在遍历过程中得到当前遍历项的index,然后通过bind将索引传入到delItem方法中
打开reducer.js 修改导出方法
//defaultState默认数据
const defaultState = {
arr:['zhangsan','lisi','wangwu'],
textVal:''
}
export default (state = defaultState,action) => {
if(action.type === "textVal"){
state.textVal = action.value
}else if(action.type === "arr"){
state.arr.unshift(state.textVal)
state.textVal = ""
}else if(action.type === "del"){
state.arr.splice(action.value,1)
}
return state
}
reducer函数写法优化
在reducer函数中我们根据action的type来做判断决定后续修改的全局state,那么if后面的else if就会越来越多,我们改成switch语句
打开reducer.js
//defaultState默认数据
const defaultState = {
arr:['zhangsan','lisi','wangwu'],
textVal:''
}
export default (state = defaultState,action) => {
switch(action.type){
case "textVal":
state.textVal = action.value;
break;
case "arr":
state.arr.unshift(state.textVal);
newState.textVal = ""
break;
case "del":
state.arr.splice(action.value,1)
break;
default:
break;
}
return state
}
同时为了避免后续因为数据类型导致的赋值深浅拷贝问题,
//defaultState默认数据
const defaultState = {
arr:['zhangsan','lisi','wangwu'],
textVal:''
}
export default (state = defaultState,action) => {
//对state做一个深拷贝,并且下面所有的state全部替换成newState
let newState = JSON.parse(JSON.stringify(state))
switch(action.type){
case "textVal":
newState.textVal = action.value;
break;
case "arr":
newState.arr.unshift(state.textVal);
newState.textVal = ""
break;
case "del":
newState.arr.splice(action.value,1)
break;
default:
break;
}
return newState
}
为后期维护提供便利
1、把type值提取成公用部分
现在我们action中的type如果要修改,那么我们就需要把reducer函数中的判断也做修改,并且,我们这里是全局state,就意味着可能不只有一个组件调用,那么这个时候各种地方都需要逐一修改,极度不方便后期维护,所以我们需要把这些type值提取成公共部分来使用
在store文件夹内新建actionTypes.js
注意:这个文件名是固定的不能随便取
export const TEXT_VAL = "textVal";
export const ARR = "arr";
export const DEL = "del";
这里action的type在取名的时候都是大写,这个是一个约定熟成的规则
然后在需要调用的地方解构导入对应需要的type值即可
import {TEXT_VAL,ARR,DEL} from '../store/actionTypes'
//把原来list和reducer中的使用type替换成上面导入的值
2、把action与dispatch部分提取
我们可以看到在list组件中,只要是涉及到修改全局state的方法,内部的语法都是大差不多,都需要创建action执行dispatch
在store文件夹中新建actionCreators.js,把之前list组件中用于操作全局state的方法体提取出来
import store from './'
export default (type,value) => {
let action = {
type,
value
}
store.dispatch(action)
}
然后在list中导入调用并传入对应的实参
//......
import {TEXT_VAL,ARR,DEL} from '../store/actionTypes'
import actionCreators from '../store/actionCreators';
export default class list extends Component {
//......
getInputVal(event){
actionCreators(TEXT_VAL,event.target.value)
}
addItem(){
actionCreators(ARR)
}
delItem(index){
actionCreators(DEL,index)
}
//......
}
React-redux
我们上面所讲的其实可以理解成是原生redux,redux作为全局状态管理是可以应用在很多库开发的前端全局状态管理上,比如JQ,但是这种原生的写法非常麻烦,我们现在通过React-redux对上面的写法进行优化
我们现在先新建react项目,通过原生redux实现一个简单的累加过程
新建好的react项目我们先做以下操作:
- src文件夹下面只保留App.js和index.js,其他全部删掉
- 把index.js内部引入的已经被删除的文件导入和调用都删除掉,把严格模式的标签也删除掉
- 把App.js中的代码全部删除,替换成类式组件代码
安装原生redux的方式把创建好store
新建reducer.js
//设置全局状态(全局数据)
const defaultState = {
num:20
}
//导出reducer函数(操作全局数据的方法)
export default (state = defaultState,action) => {
let newState = JSON.parse(JSON.stringify(state));
switch (action.type) {
case value:
break;
default:
break;
}
return newState
}
在store内新建index.js
import { createStore } from "redux";
import reducer from "./reducer";
//创建store实例,并让redux开发工具捕获到实时的全局状态
const store = createStore(reducer,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
//导出store
export default store
新建views文件夹创建一个Page1.js当做调用操作全局状态num的组件实现累加
import React, { Component } from 'react'
import store from '../store' //导入store实例
export default class Page1 extends Component {
constructor(props){
super(props);
//将全局状态导入到Page组件的state中
this.state = store.getState();
}
//制作修改全局状态的方法
toAdd(){
//根据action的type值来决定reducer函数内部执行哪种操作修改全局状态
let action = {
type:"toAdd"
}
//将action派发给全局状态
store.dispatch(action)
}
render() {
return (
<div>
<p>{this.state.num}</p>
<button onClick={this.toAdd.bind(this)}>按钮</button>
</div>
)
}
}
在reducer.js中修改switch判断为“toAdd” 时,将全局状态中的num执行递增运算
//......
export default (state = defaultState,action) => {
let newState = JSON.parse(JSON.stringify(state));
switch (action.type) {
case "toAdd":
newState.num++;
break;
//......
}
return newState
}
现在我们可以在页面中点击按钮执行toAdd方法,然后通过redux开发工具查看到实时的全局状态num的变化,只不过现在还缺少一步就是Page组件需要实时获取修改之后的num值,做成双向绑定的状态,然页面可以实时渲染修改后的num
通过subscribe订阅实时变化
import React, { Component } from 'react'
import store from '../store'
export default class Page1 extends Component {
constructor(props){
super(props);
this.state = store.getState();
//通过订阅监听store中的num变化,如果发生变化就执行viewChange方法
store.subscribe(this.viewChange.bind(this))
}
//获取store中的全局状态,并双向绑定到Page组件的state中
viewChange(){
this.setState(store.getState())
}
//......
}
通过react-redux针对性优化redux在react中的写法
react-redux其实是对redux的二次封装,因为redux的作者发现redux在react中写起来非常难受
安装包
npm i react-redux
react-redux核心就是一个组件和一个方法
- Provider提供器组件
- connect连接器方法
Provider组件
先在index.js入口文件中导入Provider作为顶层组件使用
//......
import App from './App';
import { Provider } from 'react-redux';
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
代码分析:
Provider作为一个提供器组件,通过其store属性将全局数据提供给其所有的子组件,所以我们需要把Provider组件写在最外层
在需要使用全局状态的组件中导入connect方法,针对这个方法我们单独先介绍一下:
connect方法
connect方法在调用的时候需要连续调用,因为该方法的返回值也是一个方法,我们在执行了connect方法之后还要执行其返回的方法,并且每次调用时需要传入特定的参数,如下
export default connect(state的映射函数,dispatch的映射函数)(组件名)
第一次执行时传入两个回调函数:
参数1:这里的映射的state指的reducer中的defaultState
参数2:其实可以理解成就用于派发修改行为的dispatch方法映射了一份作为connect的参数使用
第二次执行时传入一个组件名作为参数:
当第一次调用执行完之后会返回一个带有全局状态和修改行为的函数,就好比我们之前在组件的 constructor
中写的 this.state = store.getState()
和用于派发修改行为的方法(比如上面累加例子中的toAdd方法),单独提取出来做了一个打包成了connect方法执行后的返回值,连续的第二次执行我们就可以理解成把打包的内容注入到组件中,这样该组件就能使用全局state的数据
这里,我们在Page组件中导入connect方法
在第一次执行的时候先传入一个state的映射函数,让Page组件先能调用到全局状态的数据
import React, { Component } from 'react'
import store from '../store'
import { connect } from 'react-redux';
class Page1 extends Component {
//把原来的constructor和viewChange直接可以删除掉了
toAdd(){
let action = {
type:"toAdd"
}
store.dispatch(action)
}
render() {
return (
<div>
<!-- connect注入的全局数据要从props中调用 -->
<p>{this.props.num}</p>
<button onClick={this.toAdd.bind(this)}>按钮</button>
</div>
)
}
}
//创建一个映射全局状态的函数,这里state形参会映射到reducer.js中的defaultState
const mapStateToProps = state => {
//这里return的匿名对象我们可以看成当前组件内的数据
return {
//这里需要使用全局state中哪个数据就return哪个
num:state.num
}
}
//
export default connect(mapStateToProps)(Page1)
注意:
把mapStateToProps作为connect实参传入,那么mapStateToProps中return的对象就会成为第二次执行时,作为参数传入的组件内部的state使用,但是数据实际上要从props里面调用
全局数据要从组件的props里面调用的原因:
因为connect方法注入的全局数据是通过Provider组件提供的,而Provider组件现在是作为顶层组件使用的,所以我们可以理解成这里其实就是一个父组件向子组件传值的过程,那么传入到子组件中的数据自然要从props中调用
再传入dispatch的映射函数
//......
//这里的形参dispatch会被映射入store中的dispatch方法作为实参
const mapDispatchToProps = dispatch => {
return {
toAdd(){
let action = {
type:"toAdd"
}
dispatch(action)
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Page1)
其实就是把所有派发修改行为的方法封装到mapDispatchToProps函数的return对象中即可,然后把mapDispatchToProps传入到connect的第二个参数中,这样所有的修改全局state的方法都会挂到props对象上面,所以现在在调用toAdd方法的时候需要从props中调用
render() {
return (
<div>
<p>{this.props.num}</p>
<!-- toAdd方法要从props中调用 -->
<button onClick={this.props.toAdd.bind(this)}>按钮</button>
</div>
)
}
补充内容
当完成了上述操作之后,我们其实就可以不需要在组件内导入store了,因为这个store已经通过Provider组件提供给了通过connect方法返回并导出的组件中了
包括为什么mapStateToProps的state形参可以获取到defaultState作为实参,mapDispatchToProps的dispatch形参可以获取store的dispatch方法作为实参,其实都是Provider组件中将全局状态实例store作为该组件的store属性值传入并提供出来
备注:
其实react-redux只是对我们组件内操作全局state的写法,对原本的redux中的reducer和index文件并没有影响