目录
一、前言
这篇文章是我在学习慕课视频的过程中做的课程总结,边看视频边总结。内容比较多,可参考目录
- 《React 16.4 开发简书项目从零基础人门到实战》
二、Ant Design
可参考官网:https://ant.design/index-cn
antd
是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。
用这个框架来构建我们TodoList的界面
- 安装
cnpm install antd --save
- 使用antd步骤:
- 引入antd的样式
- 引入antd的input,button,list组件
- 定义数据
- 使用input,button,list组件
import React, { Component } from 'react'; //引入React及其Component
import 'antd/dist/antd.css'; //引入antd的样式
import { Input,Button,List } from 'antd'; //引入antd的列表
//定义数据
const data=[
"this is first TodoList",
"this is second TodoList",
"this is third TodoList",
"this is fourth TodoList",
"this is fifth TodoList"
]
class App extends Component {
render() {
return (
<div style={{marginTop:"10px",marginLeft:"10px"}}>
<div>
<Input
placeholder="请输入"
style={{width:"300px",marginRight:"10px"}}
/>
<Button type="primary">提交</Button>
</div>
<List
style={{width:"300px",marginTop:"10px"}}
bordered
dataSource={data}
renderItem={item => (<List.Item>{item}</List.Item>)}
/>
</div>
);
}
}
export default App;
- 效果展示
三、Redux 初学
3.1 Redux简介
react可以做一些小型项目,是一个视图层的框架,用于构建用户界面的JavaScript库,当我们在React中,如果兄弟组件需要传值,例如左图中深色圆圈发送数据到顶部的圆圈,需要一层一层的传上去,导致数据通讯很麻烦。
而Redux很好的弥补了这种通讯方式,在Redux中我们用Store 来保存数据,实现数据的共享,你可以把它看成一个容器。整个应用只能有一个 Store。当某个组件的值改变时,store就会接收到改变值,并传给需要的组件。接下来我们就看Redux的工作流程。
3.2 Redux工作流程
我们用这个图来给大家讲解,将整个工作流程比作一个图书馆借书的流程
- react 是借书的用户
- Store是图书管理员
- reducer是图书馆的系统
- Action Creators 是借书的动作
流程:react对Store说 我要借xxx书,react说的这句话就是Action Creators,图书管理员Store并不知道书的具体情况,那他就要查阅资料,也就是从Reducer图书馆的系统里中查找资料,并且将书的具体信息再反馈给Store,最后Store再将数据传递给React,这就是redux的工作流程。
3.3 Chrome安装Redux插件
要通过翻墙在Chrome中的网上应用商店添加插件(Redux DevTools2.17.0 )就可以使用控制台来调试redux的代码。
在Chrome添加好插件以后,要在index.js组件的createStroe函数中传入第二个参数就可以正常使用控制台
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
3.4 使用Redux
3.4.1 安装redux
cnpm install --save redux
3.4.2 创建目录
在 src 目录下创建 store 目录用来存储 Redux 数据,该目录下有 reducer.js 以及 index.js 两个文件
1. index.js组件:Redux 提供createStore
这个函数,用来生成 Store即创建数据仓库,并将仓库导出去给 TodoList.js 使用。
//创建仓库
import { createStore } from "redux";
const store=createStore();
export default store;
在使用Store时 有一些常用的API:
- store.getState() 拿到当前时刻State中的所有数据
- store.diapatch() 派发action
- store.subscribe() 监听Store 当数据发生改变时,就会执行
Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。
2. reducer组件:该组件用来定义数据,存储数据
Reducer 是一个函数,它接受 action 和当前 state 作为参数,返回一个新的 State。
// 整个应用的初始状态,可以作为 State 的默认值
const defaultState={
inputValue:"这是reducer初始的默认数据",
list:[1,2,3]
}
export default (state=defaultState,action)=>{
return state;
}
此时就要在index.js中导入reducer,作为参数传给createStore
//创建仓库,并传入全部的数据
import { createStore } from "redux";
import reduce from "./reducer"
const store=createStore(reduce,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
四、Redux 进阶
4.1 使用Redux 显示数据
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input,Button,List } from 'antd';
//导入redux
import store from "../store/index";
class App extends Component {
constructor(props){
super(props);
//获取到Store中的所有数据
this.state=store.getState();
}
render() {
return (
<div style={{marginTop:"10px",marginLeft:"10px"}}>
<div>
<Input
value={this.state.inputValue}
placeholder="请输入"
style={{width:"300px",marginRight:"10px"}}
/>
<Button type="primary">提交</Button>
</div>
{/* 将原来的data换成 state中的数据 */}
<List
style={{width:"300px",marginTop:"10px"}}
bordered
dataSource={this.state.list}
renderItem={item => (<List.Item>{item}</List.Item>)}
/>
</div>
);
}
}
export default App;
效果展示:
4.2 使用Redux 操作数据
4.2.1 Input输入数据
TodoList.js组件
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import { Input,Button,List } from 'antd';
//导入redux
import store from "../store/index";
class App extends Component {
constructor(props){
super(props);
//store.getState() 方法来获取数据,并赋值为 state
this.state=store.getState();
// 定义handleInputChange 改变this指向
this.handleInputChange=this.handleInputChange.bind(this)
// 定义handleStoreChange 来处理 Redux 返回回来的数据
this.handleStoreChange=this.handleStoreChange.bind(this)
//监听数据改变,调用handleStoreChange函数重新渲染界面
store.subscribe(this.handleStoreChange)
}
render() {
return (
<div style={{marginTop:"10px",marginLeft:"10px"}}>
<div>
<Input
value={this.state.inputValue}
placeholder="请输入"
style={{width:"300px",marginRight:"10px"}}
onChange={this.handleInputChange}
/>
<Button type="primary">提交</Button>
</div>
<List
style={{width:"300px",marginTop:"10px"}}
bordered
dataSource={this.state.list}
renderItem={item => (<List.Item>{item}</List.Item>)}
/>
</div>
);
}
handleInputChange=(e)=>{
console.log(e.target.value)
const action ={
type:"change_input_value",
value:e.target.value
}
//传给store
store.dispatch(action);
}
handleStoreChange(){
this.setState(store.getState())
}
}
export default App;
reducer.js组件
// 整个应用的初始状态,可以作为 State 的默认值
const defaultState={
inputValue:"这是reducer初始的默认数据",
list:[1,2,3]
}
export default (state=defaultState,action)=>{
console.log(state,action);
//接收到数据后,将新的newState返回去
if(action.type==='change_input_value'){
//深拷贝
const newState=JSON.parse(JSON.stringify(state));
newState.inputValue=action.value;
return newState;
}
return state;
}
当我们修改input框中的内容时,控制台输出的内容就会改变
分析总结:
- 在input组件中绑顶onChange事件,并为其添加handleInputChange方法
- 定义编写handleInputChange方法
- 在handleInputChange()中定义action,通过store的dispatch()方法将action传给Redux中的Stroe(index.js),并直接传给Reducer(reducer,js)
- 在reducer中打印state和action,state存储的是原来的数据 ,action中存储的改变后的数据
- 用深拷贝将原来state中数据复制给newState,并将action中的新数据赋值给newState,通过这样的方法newState中存储的就是修改后的数据,最后将newState返回给TodoList.js
- 在 TodoList 的
constructor
中通过store.subscribe
监听 Redux 传回来的数据,使用处理方法handleStoreChange
。 - 在
handleStoreChange方法中,我们只需要使用setState()重新设置state即可,即
this.setState(store.getState())
4.2.2 Button 提交数据
index.js组件
import React, { Component } from 'react'; //引入React及其Component
import 'antd/dist/antd.css'; //引入antd的样式
import { Input,Button,List } from 'antd'; //引入antd的列表
import store from "../store/index"
class TodoList extends Component {
constructor(props){
super(props)
this.state=store.getState()
this.handleValueChange=this.handleValueChange.bind(this)
this.handleStateChange=this.handleStateChange.bind(this)
this.handleBtnClick=this.handleBtnClick.bind(this)
this.handleTnputKey=this.handleTnputKey.bind(this)
store.subscribe(this.handleStateChange)
}
render() {
return (
<div style={{marginTop:"10px",marginLeft:"10px"}}>
<div>
<Input
value={this.state.inputValue}
placeholder="请输入"
style={{width:"300px",marginRight:"10px"}}
onChange={this.handleValueChange}
//添加回车事件
onKeyDown={this.handleTnputKey}
/>
<Button type="primary" onClick={this.handleBtnClick}>提交</Button>
</div>
<List
style={{width:"300px",marginTop:"10px"}}
bordered
dataSource={this.state.list}
renderItem={item => (<List.Item>{item}</List.Item>)}
/>
</div>
);
}
handleValueChange(e){
const action ={
type:"change_input_value",
value:e.target.value
}
//通过 dispatch(action),将数据传递给 store
store.dispatch(action) ;
}
handleStateChange(){
this.setState(store.getState())
}
handleBtnClick(){
const action={
type:"change_todo_item"
}
store.dispatch(action) ;
}
handleTnputKey(e){
if(e.keyCode===13){
this.handleBtnClick()
}
}
}
export default TodoList;
reducer.js
const defaultState={
inputValue:"这是reducer的默认值",
list:[1,2,3]
}
export default (state=defaultState,action)=>{
console.log(state,action);
if(action.type==="change_input_value"){
const newState=JSON.parse(JSON.stringify(state));
newState.inputValue=action.value;
return newState;
}
if(action.type==="change_todo_item"){
const newState=JSON.parse(JSON.stringify(state));
newState.list.push(newState.inputValue);
newState.value=""
return newState;
}
return state;
}
分析总结:
做完input的功能以后,在写button的功能就比较容易理解
- 为button按钮绑定点击事件onClick,添加handleBtnClick方法
- 定义编写方法,改变this的指向(可以使用箭头函数)
- 通过
dispatch(action)
,将数据传递给store
- 与input的类似 在 reducer.js 中获取数据,并 return 回去处理结果
- 为input 绑定回车事件onKeyDown
- 在写回车事件handleInputKey的方法时,只需要再调用一次handleBtnClick()即可
注意:在写input方法时已经设置了监听事件,只要页面发生改变,就会执行handleStateChange()获取最新数据,并且渲染界面
4.3 使用Redux 删除数据
index.js
import React, { Component } from 'react'; //引入React及其Component
import 'antd/dist/antd.css'; //引入antd的样式
import { Input,Button,List } from 'antd'; //引入antd的列表
import store from "../store/index"
class TodoList extends Component {
constructor(props){
super(props)
this.state=store.getState()
this.handleValueChange=this.handleValueChange.bind(this)
this.handleStateChange=this.handleStateChange.bind(this)
this.handleBtnClick=this.handleBtnClick.bind(this)
this.handleTnputKey=this.handleTnputKey.bind(this)
store.subscribe(this.handleStateChange)
}
render() {
return (
<div style={{marginTop:"10px",marginLeft:"10px"}}>
<div>
<Input
value={this.state.inputValue}
placeholder="请输入"
style={{width:"300px",marginRight:"10px"}}
onChange={this.handleValueChange}
//添加回车事件
onKeyDown={this.handleTnputKey}
/>
<Button type="primary" onClick={this.handleBtnClick}>提交</Button>
</div>
<List
style={{width:"300px",marginTop:"10px"}}
bordered
dataSource={this.state.list}
renderItem={(item,index) => (<List.Item onClick={this.handleItemDelet.bind(this,index)}>{item}</List.Item>)}
/>
</div>
);
}
handleValueChange(e){
const action ={
type:"change_input_value",
value:e.target.value
}
store.dispatch(action) ;
}
handleStateChange(){
this.setState(store.getState())
}
handleBtnClick(){
const action={
type:"change_todo_item"
}
// 通过 dispatch(action),将数据传递给 store
store.dispatch(action) ;
}
handleTnputKey(e){
if(e.keyCode===13){
this.handleBtnClick()
}
}
handleItemDelet(index){
console.log(0)
const action={
type:"delete_todo_item",
index
}
store.dispatch(action);
}
}
export default TodoList;
reducer.js
const defaultState={
inputValue:"这是reducer的默认值",
list:[1,2,3]
}
export default (state=defaultState,action)=>{
console.log(state,action);
if(action.type==="change_input_value"){
const newState=JSON.parse(JSON.stringify(state));
newState.inputValue=action.value;
return newState;
}
//添加列表项
if(action.type==="change_todo_item"){
const newState=JSON.parse(JSON.stringify(state));
newState.list.push(newState.inputValue);
newState.value=""
return newState;
}
//删除列表项
if(action.type==="delete_todo_item"){
const newState=JSON.parse(JSON.stringify(state));
newState.list.splice(action.index,1);
return newState;
}
return state;
}
分析总结:
- 列表点击事件绑定 handleItemDelet 方法。此时,由于需要绑定
this
,并且传递值index
,即两个值,所以我们直接在代码中:this.handleDeleteItem.bind(this, index)
- 编写 handleItemDelet 方法
- 通过 dispatch(action),将数据传递给 store
- 在 reducer.js 中获取数据,并 return 回去处理结果
五、进行优化
在前面的部分中我们完成了TodoList的基本功能,这章我们要对其进行优化
5.1 actionTypes的拆分
在TOdoList.js和reducer.js 的组件中我们都写了action的type,只有二者相等时才会满足条件,进行数据的修改,但是如果我们在二者中写错任意一个action的type,就无法达到预期的效果,控制台也不会报错,所以可能会因为一些小失误让我们找好久的bug(omygod)
所以我们需要进行下 type 的处理,我们在 store 目录下新增一个 actionTypes.js的组件
export const CHANGE_INPUT_VALUE = 'change_input_value';
export const CHANGE_TODO_ITEM = 'change_todo_item';
export const DELETE_TODO_ITEM = 'delete_todo_item';
然后在TodoList.js中引入
import React, { Component } from 'react'; //引入React及其Component
import 'antd/dist/antd.css'; //引入antd的样式
import { Input,Button,List } from 'antd'; //引入antd的列表
import store from "../store/index"
import { CHANGE_INPUT_VALUE, CHANGE_TODO_ITEM, DELETE_TODO_ITEM } from '../store/actionTypes'; // 引用 actionTypes
class TodoList extends Component {
constructor(props){
super(props)
this.state=store.getState()
this.handleValueChange=this.handleValueChange.bind(this)
this.handleStateChange=this.handleStateChange.bind(this)
this.handleBtnClick=this.handleBtnClick.bind(this)
this.handleTnputKey=this.handleTnputKey.bind(this)
store.subscribe(this.handleStateChange)
}
render() {
return (
<div style={{marginTop:"10px",marginLeft:"10px"}}>
<div>
<Input
value={this.state.inputValue}
placeholder="请输入"
style={{width:"300px",marginRight:"10px"}}
onChange={this.handleValueChange}
//添加回车事件
onKeyDown={this.handleTnputKey}
/>
<Button type="primary" onClick={this.handleBtnClick}>提交</Button>
</div>
<List
style={{width:"300px",marginTop:"10px"}}
bordered
dataSource={this.state.list}
renderItem={(item,index) => (<List.Item onClick={this.handleItemDelet.bind(this,index)}>{item}</List.Item>)}
/>
</div>
);
}
handleValueChange(e){
const action ={
type:CHANGE_INPUT_VALUE,
value:e.target.value
}
store.dispatch(action) ;
}
handleStateChange(){
this.setState(store.getState())
}
handleBtnClick(){
const action={
type:CHANGE_TODO_ITEM
}
// 通过 dispatch(action),将数据传递给 store
store.dispatch(action) ;
}
handleTnputKey(e){
if(e.keyCode===13){
this.handleBtnClick()
}
}
handleItemDelet(index){
console.log(0)
const action={
type:DELETE_TODO_ITEM,
index
}
store.dispatch(action);
}
}
export default TodoList;
reducer.ja组件中引入
import { CHANGE_INPUT_VALUE, CHANGE_TODO_ITEM, DELETE_TODO_ITEM } from './actionTypes'; // 引用 actionTypes
const defaultState={
inputValue:"这是reducer的默认值",
list:[1,2,3]
}
export default (state=defaultState,action)=>{
console.log(state,action);
if(action.type===CHANGE_INPUT_VALUE){
const newState=JSON.parse(JSON.stringify(state));
newState.inputValue=action.value;
return newState;
}
if(action.type===CHANGE_TODO_ITEM){
const newState=JSON.parse(JSON.stringify(state));
newState.list.push(newState.inputValue);
newState.value=""
return newState;
}
if(action.type===DELETE_TODO_ITEM){
const newState=JSON.parse(JSON.stringify(state));
newState.list.splice(action.index,1);
return newState;
}
return state;
}
5.2 使用actionCreate统一创建action
在我们的工作流程中 Action Creator并不是在React中的,而是由React的某个动作产生的,也就是说不应该在TodoList.js组件中中直接定义action,我们可以通过actionCreators统一管理页面上所有的action,也就是说用actionCreators创建action ,便于代码的管理维护。
在store文件夹下创建actionCreators.js组件
import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM} from "./actionTypes";
export const getInputChangeAcion=(value)=>({
type:CHANGE_INPUT_VALUE,
value
})
export const getAddItemAcion=(value)=>({
type:ADD_TODO_ITEM,
})
export const getDeleteItemAcion=(index)=>({
type:DELETE_TODO_ITEM,
index
})
TodoList.js组件
import React, { Component } from 'react'; //引入React及其Component
import 'antd/dist/antd.css'; //引入antd的样式
import { Input,Button,List } from 'antd'; //引入antd的列表
import store from "../store/index";
import {getInputChangeAcion,getAddItemAcion,getDeleteItemAcion} from "../store/actionCreators" //引入actionCreators
class TodoList extends Component {
constructor(props){
super(props)
this.state=store.getState()
this.handleValueChange=this.handleValueChange.bind(this)
this.handleStateChange=this.handleStateChange.bind(this)
this.handleBtnClick=this.handleBtnClick.bind(this)
this.handleTnputKey=this.handleTnputKey.bind(this)
store.subscribe(this.handleStateChange)
}
render() {
return (
<div style={{marginTop:"10px",marginLeft:"10px"}}>
<div>
<Input
value={this.state.inputValue}
placeholder="请输入"
style={{width:"300px",marginRight:"10px"}}
onChange={this.handleValueChange}
//添加回车事件
onKeyDown={this.handleTnputKey}
/>
<Button type="primary" onClick={this.handleBtnClick}>提交</Button>
</div>
<List
style={{width:"300px",marginTop:"10px"}}
bordered
dataSource={this.state.list}
renderItem={(item,index) => (<List.Item onClick={this.handleItemDelete.bind(this,index)}>{item}</List.Item>)}
/>
</div>
);
}
handleValueChange(e){
const action =getInputChangeAcion(e.target.value)
store.dispatch(action) ;
}
handleStateChange(){
this.setState(store.getState())
}
handleBtnClick(){
const action=getAddItemAcion()
// 通过 dispatch(action),将数据传递给 store
store.dispatch(action) ;
}
handleTnputKey(e){
if(e.keyCode===13){
this.handleBtnClick()
}
}
handleItemDelete(index){
const action=getDeleteItemAcion(index)
store.dispatch(action);
}
}
export default TodoList;
注意:我们在actionCreators中导入并使用了 actionTypes,那么在TodoList中就不需要再导入
这样,我们就把整个 action
抽取出来了,在大型项目中,对我们的工作会非常方便。
六、UI组件和容器组件
- UI组件——傻瓜组件,负责页面的渲染
- 容器组件——聪明组件,负责页面的逻辑
要在store文件夹下创建TodoListUI.js组件,将它看做UI组件,TodoList.js做为容器组件,这俩个组件需要传值,也就是我们在React中学习的子父组件传值。
现在我们拆分这俩个组件
TodoList.js组件代码
import React, { Component } from 'react'; //引入React及其Component
import TodoListUI from "./TodoListUI";
import store from "../store/index";
import {getInputChangeAcion,getAddItemAcion,getDeleteItemAcion} from "../store/actionCreators" //引入actionCreators
class TodoList extends Component {
constructor(props){
super(props)
this.state=store.getState()
this.handleValueChange=this.handleValueChange.bind(this)
this.handleStateChange=this.handleStateChange.bind(this)
this.handleBtnClick=this.handleBtnClick.bind(this)
this.handleTnputKey=this.handleTnputKey.bind(this)
this.handleItemDelete=this.handleItemDelete.bind(this)
store.subscribe(this.handleStateChange)
}
render() {
return <TodoListUI
inputValue={this.state.inputValue}
list={this.state.list}
handleValueChange={this.handleValueChange}
handleTnputKey={this.handleTnputKey}
handleBtnClick={this.handleBtnClick}
handleItemDelete={this.handleItemDelete}
/>;
}
handleValueChange(e){
const action =getInputChangeAcion(e.target.value)
store.dispatch(action) ;
}
handleStateChange(){
this.setState(store.getState())
}
handleBtnClick(){
const action=getAddItemAcion()
// 通过 dispatch(action),将数据传递给 store
store.dispatch(action) ;
}
handleTnputKey(e){
if(e.keyCode===13){
this.handleBtnClick()
}
}
handleItemDelete(index){
const action=getDeleteItemAcion(index)
store.dispatch(action);
}
}
export default TodoList;
TodoListUI.js组件的代码
import React,{Component} from "react";
import 'antd/dist/antd.css'; //引入antd的样式
import { Input,Button,List } from 'antd'; //引入antd的列表
class TodoListUI extends Component{
render (){
return (
<div style={{marginTop:"10px",marginLeft:"10px"}}>
<div>
<Input
value={this.props.inputValue}
placeholder="请输入"
style={{width:"300px",marginRight:"10px"}}
onChange={this.props.handleValueChange}
//添加回车事件
onKeyDown={this.props.handleTnputKey}
/>
<Button type="primary" onClick={this.props.handleBtnClick}>提交</Button>
</div>
<List
style={{width:"300px",marginTop:"10px"}}
bordered
dataSource={this.props.list}
renderItem={(item,index) => (<List.Item onClick={this.props.handleItemDelete.bind(this,index)}>{item}</List.Item>)}
/>
</div>
)
}
}
export default TodoListUI;
分析总结:
- 我们将TodoList.js组件,render函数中的内容复制到TodoListUI.js组件render中
- 在TodoListUI.js组件中引入antd以及input,button,list的样式
- 在TodoList.js中传递数据,在TodoListUI.js接收数据
注意:在处理handleItemDelete方法时要对index做特殊处理
这样,我们就完成了页面的抽取,当我们页面过多的时候,我们就将内容独立到 UI 组件中。而容器组件,则可以包含无数个 UI 组件。所以:容器组件是聪明组件,它对整体进行了一个把控;而 UI 组件是傻瓜组件,只需要执行容器组件传递过来的事件并渲染页面即可。
七、无状态组件
当一个组件中,只有 render() 函数,没有任何逻辑操作的时候 ,我们就把它叫做无状态组件。其实无状态组件就是一个函数。
无状态组件只需要执行一个函数,性能比较高,普通组件在执行时会执行函数也会执行生命周期函数,性能比较低
import React,{Component} from "react";
import 'antd/dist/antd.css'; //引入antd的样式
import { Input,Button,List } from 'antd'; //引入antd的列表
//无状态组价
const TodoListUI=(props)=>{
return (
<div style={{marginTop:"10px",marginLeft:"10px"}}>
<div>
<Input
value={props.inputValue}
placeholder="请输入"
style={{width:"300px",marginRight:"10px"}}
onChange={props.handleValueChange}
//添加回车事件
onKeyDown={props.handleTnputKey}
/>
<Button type="primary" onClick={props.handleBtnClick}>提交</Button>
</div>
<List
style={{width:"300px",marginTop:"10px"}}
bordered
dataSource={props.list}
renderItem={(item,index) => (<List.Item onClick={props.handleItemDelete.bind(this,index)}>{item}</List.Item>)}
/>
</div>
)
}
export default TodoListUI;
分析总结:
- 我们不需要 react 中的
Component
,进行无状态组件定义,定义一个箭头函数,参数为props。 - 通过 props 获取父组件传递过来的数据。
- 我们不需要render 函数,直接 return 返回JSX就可以。
- 修改 this.props 为 props。
八、Redux 中发送异步请求获取数据
- 安装axios
cnpm install axios --save
- 安装Charles
Charles是一个HTTP代理/HTTP监视器/反向代理(抓包工具),它允许开发人员查看他们的机器和Internet之间的所有HTTP和SSL/HTTPS通信,包括请求、响应和HTTP头(包含cookie和缓存信息)。
基本原理就是将自己作为代理服务器,浏览器、手机app等客户端进行代理设置,配置成Charles监听的端口,客户端将请求发给Charles,Charles再将请求发送给真正服务器,结果返回时,由Charles转发给浏览器、手机等客户端。
在本案例中创建一个list.json文件,文件内容为
["苹果","香蕉","西瓜"]
我们使用axios异步请求获取list.json中的内容
- 在componentDidMount()生命周期中获取接口数据,并最终渲染到界面上
TodoList.js组件
import React, { Component } from 'react'; //引入React及其Component
import axios from "axios" //引入axios
import TodoListUI from "./TodoListUI";
import store from "../store/index";
import {getInputChangeAcion,getAddItemAcion,getDeleteItemAcion,initListAction} from "../store/actionCreators" //引入actionCreators
class TodoList extends Component {
constructor(props){
super(props)
this.state=store.getState()
this.handleValueChange=this.handleValueChange.bind(this)
this.handleStateChange=this.handleStateChange.bind(this)
this.handleBtnClick=this.handleBtnClick.bind(this)
this.handleTnputKey=this.handleTnputKey.bind(this)
this.handleItemDelete=this.handleItemDelete.bind(this)
store.subscribe(this.handleStateChange)
}
render() {
return <TodoListUI
inputValue={this.state.inputValue}
list={this.state.list}
handleValueChange={this.handleValueChange}
handleTnputKey={this.handleTnputKey}
handleBtnClick={this.handleBtnClick}
handleItemDelete={this.handleItemDelete}
/>;
}
// 在 componentDidMount() 中进行 axios 接口调用
componentDidMount(){
axios.get("./list.json").then((res)=>{
const data=res.data;
// 将接口数据 dispatch 到 action 中,所以需要先前往 actionCreators.js 中创建 action
const action =initListAction(data);
// 创建好action 并 dispatch 到 reducer.js 中
store.dispatch(action)
})
}
handleValueChange(e){
const action =getInputChangeAcion(e.target.value)
store.dispatch(action) ;
}
handleStateChange(){
this.setState(store.getState())
}
handleBtnClick(){
const action=getAddItemAcion()
// 通过 dispatch(action),将数据传递给 store
store.dispatch(action) ;
}
handleTnputKey(e){
if(e.keyCode===13){
this.handleBtnClick()
}
}
handleItemDelete(index){
const action=getDeleteItemAcion(index)
store.dispatch(action);
}
}
export default TodoList;
actionsCreators.js组件
import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM,INIT_LIST_ACTION} from "./actionTypes";
export const getInputChangeAcion=(value)=>({
type:CHANGE_INPUT_VALUE,
value
})
export const getAddItemAcion=(value)=>({
type:ADD_TODO_ITEM,
})
export const getDeleteItemAcion=(index)=>({
type:DELETE_TODO_ITEM,
index
})
// 创建要导出的 initListAction,所以需要先在 actionTypes 中引入 INIT_LIST_ACTION
export const initListAction=(data)=>({
type:INIT_LIST_ACTION,
data
})
actionTypes组件
export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_TODO_ITEM = 'add_todo_item';
export const DELETE_TODO_ITEM = 'delete_todo_item';
export const INIT_LIST_ACTION='init_list_action'
reducer.js组件
//reducer 一定是一个纯函数 指给定固定的输入,就一定会有固定的输出,而且不会有任何副作用
import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM,INIT_LIST_ACTION } from './actionTypes'; // 引用 actionTypes
const defaultState={
inputValue:"这是reducer的默认值",
list:[1,2,3]
}
export default (state=defaultState,action)=>{
console.log(state,action);
if(action.type===CHANGE_INPUT_VALUE){
const newState=JSON.parse(JSON.stringify(state));
newState.inputValue=action.value;
return newState;
}
if(action.type===ADD_TODO_ITEM){
const newState=JSON.parse(JSON.stringify(state));
newState.list.push(newState.inputValue);
newState.value=""
return newState;
}
if(action.type===DELETE_TODO_ITEM){
const newState=JSON.parse(JSON.stringify(state));
newState.list.splice(action.index,1);
return newState;
}
// 接受 TodoList 传递过来的数据,并进行处理与返回
if(action.type===INIT_LIST_ACTION){
const newState=JSON.parse(JSON.stringify(state));
newState.list=action.data;
return newState;
}
return state;
}
分析总结:
通过以上代码使用Charles抓包工具和axios,我们完成异步请求的数据,并最终渲染到界面上。
- TodoList——在引入axios后,在 componentDidMount() 中进行 axios 接口调用
- actionTypes——创建INIT_LIST_ACTION
- actionsCreators——编写initListAction函数创建action
- TodoList——将创建好的action dispatch到reducer中
- reducer——接受 TodoList 传递过来的数据,并进行处理与返回
九、Redux中间件
- 中间件 即是安排在二者之间的插件。
- Redux 中间件 即是处于Action与Store之间的中间件
中间件的工作流程图
在上面图中我们可以看出,通过 Dispatch 将 Action 派发到 Store 的时候,我们在 Dispatch 中引用了中间件做处理。它对 Dispatch 做了封装升级,从而使得我们不仅可以在 Dispatch 使用对象,而且可以使用方法函数。
直接使用redux可以传递对象,若使用了 Redux-Thunk 或者 Redux-Saga ,就会传递给 Dispatch 一个函数,中间件会对函数进行处理,我们也可以调用函数
因此,简单来说,Redux 的中间件,就是对 Dispatch 的封装升级。
9.1 redux-thunk 实现ajax数据请求
在上一章中我们使用axios异步请求数据,但随着axios的增多,如果我们将所有的异步请求都放在TodoList的组件中,就会显得页面臃肿,因此使用Redux-Thunk 可以把异步请求及复杂业务逻辑抽取到其他地方处理。
- 安装redux-thunk
cnpm install redux-thunk --save
- GitHub上的教程案例 https://github.com/reduxjs/redux-thunk
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
// Note: this API requires redux@>=3.1.0
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
- 仿照GitHub在本案例中使用thunk
store文件夹中的index.js组件
// 从 redux 中引入 applyMiddleware,其的作用是应用 redux 中间件,即使用redux中间件就必须引入applyMiddleware
// 引入 compose 函数,因为我们用到了两个中间件:redux-thunk 以及 redux-devtools-extension,需要 compose 辅助
import { createStore, applyMiddleware,compose} from 'redux';
import reducer from "./reducer.js";
// 从 redux-thunk 中引入 thunk
import thunk from 'redux-thunk';
const composeEnhancers =
// 使用 redux-devtools-extension 中间件
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
// 使用 applyMiddleware 对此进行扩展
const enhancer = composeEnhancers(
applyMiddleware(thunk),
);
const store = createStore(reducer, enhancer);
export default store;
分析总结:
- 从redux中导入applyMiddleware,applyMiddleware作用是应用 redux 中间件,即使用redux中间件就必须引入applyMiddleware
- 从redux-thunk中导入thunk
- 从redux中引入compose,因为我们用到了两个中间件:redux-thunk 以及 redux-devtools-extension,需要 compose 辅助
- 使用
redux-devtools-extension
中间件 - 使用
applyMiddleware
对此进行扩展,即redux-thunk
中间件加上redux-devtools-extension
中间件 - 在
createStore
进行enhancer
调用
通过以上步骤,我们就可以同时使用redux的redux-thunk中间件和redux-devtoolds-extension
actionCreators.js组件
import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM,INIT_LIST_ACTION} from "./actionTypes";
import axios from "axios" //引入axios
export const getInputChangeAcion=(value)=>({
type:CHANGE_INPUT_VALUE,
value
})
export const getAddItemAcion=(value)=>({
type:ADD_TODO_ITEM,
})
export const getDeleteItemAcion=(index)=>({
type:DELETE_TODO_ITEM,
index
})
// 创建要导出的 initListAction,所以需要先在 actionTypes 中引入 INIT_LIST_ACTION
export const initListAction=(data)=>({
type:INIT_LIST_ACTION,
data
})
//将TodoList.js,componentDidMount中的axios.get()的内容全部剪切到actionCreators.js中
export const getTodoList=()=>{
// 当我们使用 getTodoList 的时候,在这里能传递 store 的 dispatch,从而直接在下面代码中使用
return (dispatch)=>{
axios.get("./list.json").then((res)=>{
const data=res.data;
// 直接使用 actionCreators.js 中的 initListAction方法
const action =initListAction(data);
// 创建好action 并 dispatch 到 reducer.js 中
dispatch(action)
})
}
}
TodoList.js组件
import React, { Component } from 'react'; //引入React及其Component
import TodoListUI from "./TodoListUI";
import store from "../store/index";
import {getTodoList,getInputChangeAcion,getAddItemAcion,getDeleteItemAcion} from "../store/actionCreators" //引入actionCreators
class TodoList extends Component {
constructor(props){
super(props)
this.state=store.getState()
this.handleValueChange=this.handleValueChange.bind(this)
this.handleStateChange=this.handleStateChange.bind(this)
this.handleBtnClick=this.handleBtnClick.bind(this)
this.handleTnputKey=this.handleTnputKey.bind(this)
this.handleItemDelete=this.handleItemDelete.bind(this)
store.subscribe(this.handleStateChange)
}
render() {
return <TodoListUI
inputValue={this.state.inputValue}
list={this.state.list}
handleValueChange={this.handleValueChange}
handleTnputKey={this.handleTnputKey}
handleBtnClick={this.handleBtnClick}
handleItemDelete={this.handleItemDelete}
/>;
}
// 在 componentDidMount() 中进行 axios 接口调用
componentDidMount(){
// 在 componentDidMount 中调用 getTodoList。如果我们没使用 redux-thunk,我们只能使用对象,但是现在我们可以使用函数了。
const action =getTodoList();
// 当我们 dispatch 了 action 的时候,我们就调用了上边的 getTodoList(),从而获取了数据
store.dispatch(action);
}
handleValueChange(e){
const action =getInputChangeAcion(e.target.value)
store.dispatch(action) ;
}
handleStateChange(){
this.setState(store.getState())
}
handleBtnClick(){
const action=getAddItemAcion()
// 通过 dispatch(action),将数据传递给 store
store.dispatch(action) ;
}
handleTnputKey(e){
if(e.keyCode===13){
this.handleBtnClick()
}
}
handleItemDelete(index){
const action=getDeleteItemAcion(index)
store.dispatch(action);
}
}
export default TodoList;
分析总结:
- 在actionCreators.js中引入axios
- 在没有引入redux-thunk中间件之前,只能在actionCreators中返回对象,引入以后就可以返回函数getTodoList()
- 将TodoList.js,componentDidMount中的axios.get()的内容全部剪切到getTodoList()中
- 当我们使用
getTodoList()
的时候,传递参数为store
的dispatch
,从而在下面代码中直接调用dispatch(action) - 直接使用 actionCreators.js 中的
initListAction
方法,并dispatch
该action
- 在 TodoList.js 中引用 actionCreators.js 中的
getTodoList()
,并去除没再引用的initListAction
- 当我们
dispatch
了action
的时候,我们就调用了getTodoList()
,从而获取数据
通过以上步骤,我们就使用 redux-thunk
,将 axios
的接口调用抽取到了 actionCreators.js 中了。
原因总结:
我们为什么要将 axios
的接口调用抽取到 actionCreators.js 中呢?可以假设一下,当我们的项目足够复杂,逻辑复杂,代码很多以及数据接口有很多时,如果我们的接口调用都在容器组件中,页面就会很臃肿,而且在项目最后如果我们需要改动某个接口,我们就很难快速的找到接口,这无疑会增加工作量,浪费时间。
但是通过 redux-thunk
的调用,我们就把接口代码从容器组件中抽取出来,从而做到:接口代码逻辑是接口代码逻辑,业务代码逻辑是业务代码逻辑。
而且,通过 redux-thunk
的抽取,可以方便我们的自动化测试。当然,自动化测试是什么东东,我们还不清楚,咱也不知道咱也不敢说(哈哈)
9.2 thunk-saga 实现ajax数据请求
- 安装thunk-saga
cnpm install --save redux-saga
- 在GitHub上的案例 https://github.com/redux-saga/redux-saga
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import reducer from './reducers'
import mySaga from './sagas'
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
// then run the saga
sagaMiddleware.run(mySaga)
// render the application
- 在本案例中引入redux-saga
Store文件夹下的index.js组件
// 引入 applyMiddleware 和 compose 进行多个中间件的处理
import { createStore, applyMiddleware,compose} from 'redux';
import reducer from "./reducer.js";
import createSagaMiddleware from 'redux-saga'; //引入 redux-saga 的 createSagaMiddleware
import TodoSagas from './sagas'; //引入sagas文件
const sagaMiddleware = createSagaMiddleware() // 调用 createSagaMiddleware 方法创建redux-saga中间件
const composeEnhancers =
// 使用 redux-devtools-extension 中间件
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(sagaMiddleware), // 使用 applyMiddleware 对此进行扩展
);
const store = createStore(reducer, enhancer);
sagaMiddleware.run(TodoSagas) //使用 TodoSaga
export default store;
Store文件夹下的sagas.js 组件
function* TodoSagas() {
}
export default TodoSagas;
分析总结:
- 引入
applyMiddleware
和compose
进行多个中间件的处理 - 引入
redux-saga
的createSagaMiddleware方法
调用createSagaMiddleware方法创建saga中间件
sagaMiddleware- 创建并引入Store文件下的sagas.js组件
- 通过sagaMiddleware的run 方法运行sagas.js组件
- 使用ES6的
generator
函数定义 sagas.js 文件 - 将
generator
函数导出去
- 使用redux-saga调用接口数据
TodoList.js组件
import React, { Component } from 'react'; //引入React及其Component
import TodoListUI from "./TodoListUI";
import store from "../store/index";
import {getInputChangeAcion,getAddItemAcion,getDeleteItemAcion,getInitList} from "../store/actionCreators" //引入actionCreators
class TodoList extends Component {
constructor(props){
super(props)
this.state=store.getState()
this.handleValueChange=this.handleValueChange.bind(this)
this.handleStateChange=this.handleStateChange.bind(this)
this.handleBtnClick=this.handleBtnClick.bind(this)
this.handleTnputKey=this.handleTnputKey.bind(this)
this.handleItemDelete=this.handleItemDelete.bind(this)
store.subscribe(this.handleStateChange)
}
render() {
return <TodoListUI
inputValue={this.state.inputValue}
list={this.state.list}
handleValueChange={this.handleValueChange}
handleTnputKey={this.handleTnputKey}
handleBtnClick={this.handleBtnClick}
handleItemDelete={this.handleItemDelete}
/>;
}
// 在 componentDidMount() 中进行 axios 接口调用
componentDidMount(){
const action=getInitList();
store.dispatch(action);
}
handleValueChange(e){
const action =getInputChangeAcion(e.target.value)
store.dispatch(action) ;
}
handleStateChange(){
this.setState(store.getState())
}
handleBtnClick(){
const action=getAddItemAcion()
// 通过 dispatch(action),将数据传递给 store
store.dispatch(action) ;
}
handleTnputKey(e){
if(e.keyCode===13){
this.handleBtnClick()
}
}
handleItemDelete(index){
const action=getDeleteItemAcion(index)
store.dispatch(action);
}
}
export default TodoList;
sagas.js组件
import {put, takeEvery } from 'redux-saga/effects';
import {initListAction} from "./actionCreators";
import {GET_INIT_LIST} from "./actionTypes";
import axios from "axios"; //引入axios
function* getInitList(){
try{
const res=yield axios.get('/list.json');
const action =initListAction(res.data.todolist);
yield put(action)
}catch(e){
console.log("网络请求失败")
}
}
function* TodoSagas() {
yield takeEvery(GET_INIT_LIST, getInitList);
}
export default TodoSagas;
actionCreators.js组件
import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM,INIT_LIST_ACTION,GET_INIT_LIST} from "./actionTypes";
export const getInputChangeAcion=(value)=>({
type:CHANGE_INPUT_VALUE,
value
})
export const getAddItemAcion=(value)=>({
type:ADD_TODO_ITEM,
})
export const getDeleteItemAcion=(index)=>({
type:DELETE_TODO_ITEM,
index
})
// 创建要导出的 initListAction,所以需要先在 actionTypes 中引入 INIT_LIST_ACTION
export const initListAction=(data)=>({
type:INIT_LIST_ACTION,
data
})
export const getInitList=()=>({
type:GET_INIT_LIST
})
actionTypes.js组件
export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_TODO_ITEM = 'add_todo_item';
export const DELETE_TODO_ITEM = 'delete_todo_item';
export const INIT_LIST_ACTION='init_list_action';
export const GET_INIT_LIST='get_init_list';
通过以上操作,我们就把调用接口的异步请求函数,抽取到了 sagas.js 文件中,从而对接口进行统一管理。
分析总结:
- TodoList.js —— 删除
initListAction
以及axios
,并引入 actionCreators.js 中的 getInitList() - actionTypes.js —— 定义
GET_INIT_LIST
并导出给 actionCreators.js 使用 - actionCreators.js —— 导入 actionTypes.js 中的
GET_INIT_LIST
- TodoList.js —— 调用
getInitList
,并使用dispatch
将action
派发出去。这时候不仅 reducer.js 可以接收到这个action
,我们的 sagas.js 也可以接收到这个action
。 - sagas.js ——引用
redux-saga/effets
中的takeEvery
- sagas.js ——引入
GET_INIT_LIST
类型 - sagas.js ——使用
generator
函数 - sagas.js ——通过
takeEvery
,表示只要我们接收到GET_INIT_LIST
的类型,我们就执行getInitList
方法 - sagas.js ——定义
getInitList
方法 - 将 TodoList.js 的
axios
引入迁移到 sagas.js 中 - 引入 actionCreator.js 中的
initListAction
- 在 sagas.js 中处理异步请求函数
getInitList
() - 由于我们在 sagas.js 中没有引用到
store
,所以不能使用store.dispatch()
,但是redux-saga
给我们提供了put
方法来代替store.dispatch()
方法,所以我们引用put
方法。等action
处理完之后,再执行put
方法:yield put(action)
在 src/store/sagas.js
中,我们还通过 try...catch...
方法,对接口进行处理,当接口不存在或者请求异常的时候,就会抛出错误。
十、React-redux的使用
在之前的章节中,我们学习了 React,也学习了 Redux,接触了解Redux 的中间件:Redux-Thunk 和 Redux-Saga。
其实在学习这章节以前我一直以为redux就是react-redux,redux是react-redux的简写,是不是有些小伙伴也有这样的想法啊,其实这俩个是不一样滴,本章节讲解下 React-Redux
- 什么是 React-Redux
它是第三方模块,将所有组件分成两大类:UI 组件和容器组件
- 核心API
- Provider React-Redux 提供Provider组件在根组件外面包了一层,这样一来,Provider下的所有子组件就默认都可以拿到state了。
- connect 用于从 UI 组件生成容器组件。connect的意思就是将这两种组件连起来。
- 详细讲解connect
connect方法接受两个参数:mapStateToProps和mapDispatchToProps。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。
mapStateToProps是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
mapDispatchToProps是connect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。
接下来我们看具体的代码:
index.js组件
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './components/TodoList';
import * as serviceWorker from './serviceWorker';
import {Provider} from "react-redux";
import store from "./store/index.js"
//React-Redux 提供Provider组件(核心 API),Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。
const App =(
<Provider store={store}>
<TodoList></TodoList>
</Provider>
)
ReactDOM.render(App, document.getElementById('root'));
serviceWorker.unregister();
TodoList.js组件
不用引入store,通过connect连接组件TodoList与store
import React,{Component} from "react";
import { connect } from "react-redux"; //引入核心APIconnect
class TodoList extends Component{
render (){
return (
<div>
<div>
<input value={this.props.inputValue} onChange={this.props.handleChangeValue}/>
<button>提交</button>
</div>
<ul>
<li>{this.props.list}</li>
</ul>
</div>
)
}
}
//建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。 作为函数,mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射
const mapStateToProps=(state)=>{
return {
inputValue:state.inputValue,
list:state.list
}
}
const mapDispatchToProps=(dispatch)=>{
return{
handleChangeValue(e){
const action ={
type:"change_input_value",
value:e.target.value
}
dispatch(action)
}
}
}
export default connect (mapStateToProps,mapDispatchToProps)(TodoList);
//connect方法接受两个参数:mapStateToProps和mapDispatchToProps。
// 它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),
// 后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。
reducer.js组件
const defaultState={
inputValue:"HELLO WORLD",
list:[1,2,3]
}
export default (state=defaultState,action)=>{
if(action.type==='change_input_value'){
const newStore=JSON.parse(JSON.stringify(state));
newStore.inputValue=action.value;
return newStore;
}
return state;
}
分析总结:
- 重新创建TodoList的项目
- index.js——引入react-redux中的Provider,引入store
- 用Provider重新定义App,并连接store,使得所有的子组件都可以获得store中的内容
- TodoList.js组件——不需要import store from store,也不需要在constructor中获取store,而是通过react-redux的connect来获取数据
- TodoList.js——导出connect,连接TodoList和store,并且需要传入俩个参数mapStateToProps,mapDispatchToProps
- 定义mapStateToProps方法,负责输入逻辑,即将state映射到 UI 组件的参数(props)
- 定义mapDispatchToProps方法,负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。该方法即是 TodoList.js 将
store.dispatch
方法映射到props
上,所以我们就可以通过this.props
来定义方法 - 给input绑定handleChangeValue事件,通过this.props绑定方法
- mapDispatchToProps中定义handleChangeValue方法,并使用disptach将action派发到reducer.js,
- reducer.js——判断传来的action的类型,对之前的state进行深拷贝,获取action.value的值并返回
通过以上步骤,我们就简单的使用了react-redux。对于项目的应有的增删改我们就不一一介绍了,大家可以参考之前的内容,进行项目优化。
十一、总结
经过一段时间的学习,我们掌握了很多的知识点,包括
- react,redux,react-redux的基础知识点
- 学会Ant Design 的reactUI库使用,
- 使用axios配合Charles抓包工具实现异步请求数据,而且为了方便接口数据的管理,我们也学习了 Redux 的中间件 Redux-Thunk 和 Redux-Thunk
- 以及了解UI 组件、容器组件、无状态组件等
- 也对项目进行优化
这些基础知识点很多,需要我们多多练习,熟练掌握。下一个阶段我们就要进行实战啦,期待!