在前面的几小节中已经完成了一个todolist的添加,删除的操做,经过把组件的数据放到了Redux中的公共存储区域store中去存储,在Redux中进行状态数据的更新修改css
改变store的数据惟一办法就是派发action,调用store.dispatch方法,也知道经过getState方法获取store中的全部状态数据,而实现组件页面的更新与store保持同步,必须得触发注册subscribe方法,通时还得监听一个事件处理函数html
用于从新在一次获取store的数据使页面同步更新react
在上几回编写Redux的代码中,建立store,reducer,acton,以及actionTypes(动做类型)都是放在一个文件当中进行编写的,然而更改store可能有多个action动做,全部代码杂糅在一块儿,后期维护起来显然是很是痛苦的git
因此有必要进行将Redux代码进行按照特定的职责,功能结构进行拆分的,其实也就是把以前各个逻辑代码拆分到各个文件当中去单独管理的 编程
完整的TodoList代码
这是上一节完整的一todolist的代码,建立store,reducer,以及action,UI组件等都是混写在一个文件当中的,这样虽然没有什么问题,可是维护起来,很是痛苦,若是一个文件里代码行数超过了130行,就应该考虑拆分代码了的,固然这并非硬性的规定,适当的拆分有利于代码的维护,可是过分的拆分,也会增长项目的复杂程度json
import React from 'react';
import ReactDOM from 'react-dom';
import { Input, Button, List, message, Modal } from 'antd'; // 引入antd组件库
import 'antd/dist/antd.css'; // 引入antd样式
// 1. 建立一个store管理仓库,从redux库中引入一个createStore函数
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
// 2. 引入createStore后,store并无建立,须要调用createStore()后才有store
//const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); // 建立好reducer后,须要将reducer做为参数传到createStore当中去,这样store才能拿到reducer的state数据
const store = createStore(reducer, composeWithDevTools(applyMiddleware())); // 建立好reducer后,须要将reducer做为参数传到createStore当中去,这样store才能拿到reducer的state数据
// 3. 建立reducer函数,管理组件共享的数据状态以及一些动做
// reducer是一个纯函数,返回一个新的state给store
// 4. 初始化state值,将原先组件内部的状态的数据,移除到reducer里面去管理
function reducer(state = {
inputValue: '',
list: []
}, action) {
console.log(state, action);
if (action.type === 'handle_Input_Change') {
// 对原有的上一次的state作一次深拷贝,在Redux中,reducer不容许直接修改state
// const newState = Object.assign({}, state);
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue = action.value; // 将新的value值赋值给newState
return newState;
}
if (action.type === 'addInputcontent') {
const newState = JSON.parse(JSON.stringify(state));
if (Trim(newState.inputValue) === '') {
message.error('输入表单内不能为空,请输入内容');
} else {
newState.list.push(newState.inputValue); // 往list数组中添加input的内容
newState.inputValue = '';
return newState; // 返回newState
}
}
if (action.type === 'deletelist') {
// 下面这个也是拷贝原对象的一种方式与上面等价
const newState = Object.assign({}, state);
newState.list.splice(action.index, 1);
return newState;
}
return state;
}
// 去除先后空格
function Trim(str) {
return str.replace(/(^\s*)|(\s*$)/g, "");
}
const { confirm } = Modal
// TodoList组件
class TodoList extends React.Component {
constructor(props) {
super(props);
// 5. 在组件内部经过getState()方法就能够拿到store里面的数据
this.state = store.getState();
// this环境的绑定
this.handleInputChange = this.handleInputChange.bind(this);
this.handleStoreChange = this.handleStoreChange.bind(this);
this.handleAddClick = this.handleAddClick.bind(this);
// 触发订阅,让store感知到state的变化
store.subscribe(this.handleStoreChange); // 接收一个函数,从新获取store最新的数据,subscribe里面必须接收一个函数,不然是会报错的,这个订阅函数放在componentWillMount生命周期函数内调用操做也是能够的
}
// componentWillMount(){
// store.subscribe(this.handleStoreChange);
// }
// 组件卸载,移除时调用该函数,通常取消,清理已注册的订阅,定时器的清理,取消网络请求,在这里面操做
componentWillUnmount() {
store.unsubscribe(this.handleStoreChange);
}
render() {
return (
<div style={{width:'600px',margin: "100px auto"}}>
<div>
<Input onChange={this.handleInputChange} value={this.state.inputValue} style={{ width:"300px",marginRight:"10px"}} placeholder="请输入内容..." />
<Button type="primary" onClick={this.handleAddClick}>提交</Button>
</div>
<List
style={{ width: '300px',marginTop:'10px'}}
bordered
dataSource={this.state.list}
renderItem={(item,index) => <List.Item onClick={this.handleDelList.bind(this, index,item)}>{item}</List.Item>}/>
</div>
)
}
handleInputChange(e) {
console.log(e.target.value);
// 定义action,肯定一个操做,动做,注意action必须遵循必定的规范,是一个对象,type字段是肯定要作的动做,类型,监听表单输入框的变化,value是输入框的值
const action = {
type: 'handle_Input_Change',
value: e.target.value
}
store.dispatch(action); // 经过store派发dispatch一个action,只有这里接收一个action,Reducer里面才能对新旧数据进行计算等操做
}
handleStoreChange() {
console.log("handleStorechange,触发了");
this.setState(store.getState()); // 触发setState从新获取store的数据,让input的数据与store保持同步了的
}
// 添加列表的操做
handleAddClick() {
console.log("添加按钮执行了");
// 定义action动做
const action = {
type: 'addInputcontent'
}
store.dispatch(action); // 还要将action传递给dispatch,这样store才会接收到
}
// 删除列表操做
handleDelList(index,item) {
this.showDeleteConfirm(index, item);
}
showDeleteConfirm(index,item) {
const action = {
type: 'deletelist',
index: index
}
confirm({
title: '肯定要删除该列表?',
content: item,
okText: '确认',
okType: 'danger',
cancelText: '取消',
onOk() {
console.log('OK');
store.dispatch(action);
},
onCancel() {
console.log('Cancel');
},
});
}
}
const container = document.getElementById('root');
ReactDOM.render(<TodoList />, container);
复制代码
此时,项目的src根目下只有一个index.js文件,项目的目录树结构是这样的redux
D:\公开课\2019\React进阶\lesson2
├─split-redux
| ├─.gitignore
| ├─package-lock.json
| ├─package.json
| ├─README.md
| ├─yarn-error.log
| ├─yarn.lock
| ├─src
| | ├─index.js
| ├─public
| | ├─favicon.ico
| | ├─index.html
| | └manifest.json
复制代码
下面来一步一步拆分的,先从简单的入手,不断的简化代码的数组
拆分ActionTypes定义成一个常量,独立管理
改变store里面state数据,惟一的办法就是派发action,调用store.dispatch(action)方法bash
而定义action,它得是一个对象,该对象下type类型必须是一个字符串类型值,这个类型值必须和reducer里面action.type后面的值相同,若是不相等,控制台虽然不报错,可是却会影响实际的功能网络
代码以下所示
// 定义action,也就是具体要作的什么事情
const action = {
type: 'handle_Input_Change', // 这个type后面的字符串值与在reducer里面的action.type相同
value: e.target.value
}
// 字符串类型值要与reducer相同
function reducer(state, action){
if (action.type === 'handle_Input_Change') { // 这个必需要与上面定义相同
// 对原有的上一次的state作一次深拷贝,在Redux中,reducer不容许直接修改state
// const newState = Object.assign({}, state);
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue = action.value; // 将新的value值赋值给newState
return newState;
}
}
复制代码
在根目录src下建立一个store文件夹,而后在新建一个actionsTypes.js
把上面action对象下的type的类型值定义成一个常量,而后对外暴露出去,由于这个动做type类型每每是固定的,通常不怎么去改变,类型值与常量名都定义成同名,这里的类型值与常量名设置成同名不必定非要一致,可是这已是你们约定俗成的一种规定,是个良好的开发习惯
定义actionType类型以下所示,将action的type类型值定义成常量
const CHANGE_INPUT_VALUE = 'CHANGE_INPUT_VALUE';
export {
CHANGE_INPUT_VALUE
}
复制代码
而后在须要使用actionType类型处,引入该暴露的变量对象便可
import { CHANGE_INPUT_VALUE } from './store/actionTypes'; // 引入actionTypes类型
handleInputChange(e) {
const action = {
type: CHANGE_INPUT_VALUE, // 这里引入上面定义的变量对象
value: e.target.value
}
store.dispatch(action); // 经过store派发dispatch一个action,只有这里接收一个action,Reducer里面才能对新旧数据进行计算等操做
}
复制代码
以此类推,按照以上模式把action里面的type类型值都更改为常量,放到一个文件(actionTypes.js)去管理的,这个文件只用于定义动做action类型的常量
由于上面的代码中的action有三个:因此完整的以下所示:
const CHANGE_INPUT_VALUE = 'CHANGE_INPUT_VALUE'; // 监听input框输入值的常量
const ADD_INPUT_CONTENT = 'ADD_INPUT_CONTENT'; // 添加列表
const DELETE_LIST = 'DELETE_LIST'; // 删除列表
export {
CHANGE_INPUT_VALUE,
ADD_INPUT_CONTENT,
DELETE_LIST
}
复制代码
而后在须要使用action Type的地方引用便可
import { CHANGE_INPUT_VALUE, ADD_INPUT_CONTENT, DELETE_LIST } from './store/actionTypes'; // 引入actionTypes
// 监听input变化动做
handleInputChange(e) {
const action = {
type: CHANGE_INPUT_VALUE,
value: e.target.value
}
store.dispatch(action);
}
// 添加列表的操做
handleAddClick() {
// 定义action动做
const action = {
type: ADD_INPUT_CONTENT
}
store.dispatch(action); // 还要将action传递给dispatch,这样store才会接收到
}
// 删除列表操做
handleDelList(index,item) {
this.showDeleteConfirm(index, item);
}
showDeleteConfirm(index,item) {
const action = { // action在这里
type: DELETE_LIST,
index: index
}
confirm({
title: '肯定要删除该列表?',
content: item,
okText: '确认',
okType: 'danger',
cancelText: '取消',
onOk() {
console.log('OK');
store.dispatch(action);
},
onCancel() {
console.log('Cancel');
},
});
}
复制代码
通过上面的处理,关于action的type类型值就已经拆分出去了的,至于拆分action中type类型值的好处就是,当你由于不当心把actionType拼写错误时,它会有很好的错误异常提示,这就是定义成一个常量的好处
拆分action,将它封装到一个函数里面去管理
在上面的代码中,只是把action中的type类型值定义成一个常量而后拆分了出去的,可是仍然发现,代码并无简化多少,其实在派发action以前,改变store的数据,对于action的动做(具体要作的事情),是不该该直接定义在咱们的组件里,在事件处理函数里面定义action对象不是不能够
可是这样代码的内聚性不高,对于简易的项目,一些action定义在各个组件内,也没有什么,可是一多的话,找起来就是灾难了的,不利于后续代码的维护,若是你可以把相应的action代码拆分出去,后来的同窗必定会感谢你的,固然随之而然就是增长了点阅读代码的复杂度
若是是高手,那绝对从心里上是要感谢那种把action拆分到一个文件里去管理的,至于初学者,那确定以为特么复杂的,很是绕以及难以理解的,其实只要把Redux的工做流程图理清楚了,也就天然为何要这么拆分了的
一般来讲,咱们把上面的action都放在一个action Creators.js的文件中去管理的,管理这个action文件代码的名字并非固定的,你想要怎么定义成管理action的任何一个名字均可以,可是最好是见名知意
具体actionCreators.js代码以下所示:
import { CHANGE_INPUT_VALUE, ADD_INPUT_CONTENT, DELETE_LIST } from './actionTypes'; // 引入actionTypes
// 将action封装成一个函数,用于返回type类型和须要的参数
function getInputChangeAction(value){
return {
type: CHANGE_INPUT_VALUE,
value:value
}
}
// 获取input框内容,添加列表action函数
function getAddInputContentAction(){
return {
type: ADD_INPUT_CONTENT
}
}
// 获取删除列表acton函数
function getDeleteListAction(index){
return {
type: DELETE_LIST,
index:index
}
}
// 上面的也等价于,在Es6中有简写函数的形式,与下面是等价的,在React代码中这种写法很常见
/*
const getInputChangeAction = (value) => ({
type: CHANGE_INPUT_VALUE,
value
});
const getAddInputContentAction = () => ({
type: ADD_INPUT_CONTENT
})
const getDeleteListAction = index => ({ // 当只有一个参数时,圆括号能够省略,当返回值有多个时,外面须要用一个大括号包裹起来的
type: DELETE_LIST,
index
})
*/
// 将变量对象的函数给暴露出去
export {
getInputChangeAction,
getAddInputContentAction,
getDeleteListAction
}
复制代码
在组件所须要引入actionCreactors的地方,引入actions,以下所示:
import { getInputChangeAction, getAddInputContentAction, getDeleteListAction} from './store/actionCreators';
// 监听input操做
handleInputChange(e) {
const action = getInputChangeAction(e.target.value);
store.dispatch(action);
}
// 添加操做
handleAddClick() {
const action = getAddInputContentAction();
store.dispatch(action);
}
// 删除列表操做
handleDelList(index,item) {
this.showDeleteConfirm(index, item);
}
showDeleteConfirm(index,item) {
const action = getDeleteListAction(index);
confirm({
title: '肯定要删除该列表?',
content: item,
okText: '确认',
okType: 'danger',
cancelText: '取消',
onOk() {
console.log('OK');
store.dispatch(action);
},
onCancel() {
console.log('Cancel');
},
});
}
复制代码
通过上面的action的拆分,如今看来咱们的代码清晰多了,经过actionCreators来建立action,这是一个很是好的编程习惯,固然若是过分的拆分,就不免会让人以为项目复杂,在各个文件之间来回切来切去的,若是不清晰他们之间的关系,那么的确是比较绕,可是不能由于这样,就不作拆分的
从长远来看,拆分action是颇有必要的,一是将事件动做的类型定义成常量给分离出去,二是把总体action单独封装成一个函数放在一个单独的文件中进行管理的,它返回对应的类型和必要的参数的
拆分的目的主要是提升代码的可维护性
建立store单独管理
在上面的代码中,已经解决了Redux工做流程中的右半边部分,也就是作了action的拆分管理,那么接下来是整理store和reducer以及React Component了
在store文件夹中建立一个index.js的文件
这个index.js主要用于建立store
import { createStore } from "redux";
// 建立store,调用createStore函数
const store = createStore();
复制代码
建立reducer,更新state数据操做
在store文件夹下建立reducer.js文件,主要用于更新state数据操做,以下代码所示
import { message } from 'antd';
import { CHANGE_INPUT_VALUE, ADD_INPUT_CONTENT, DELETE_LIST } from './actionTypes';
const defaultStatus = { // 默认初始值
inputValue: 'itclanCoder',
list: ['川川','111', '222']
}
function reducer(state=defaultStatus, action){
if(action.type === CHANGE_INPUT_VALUE){
const newState = JSON.parse(JSON.stringify(state));
newState.inputValue = action.value;
return newState;
}
if(action.type === ADD_INPUT_CONTENT){
const newState = JSON.parse(JSON.stringify(state));
if (Trim(newState.inputValue) === '') {
message.error('输入表单内不能为空,请输入内容');
} else {
newState.list.push(newState.inputValue); // 往list数组中添加input的内容
newState.inputValue = '';
return newState; // 返回newState
}
}
if(action.type === DELETE_LIST){
const newState = JSON.parse(JSON.stringify(state));
newState.list.splice(action.index, 1);
return newState;
}
return state;
}
// 去除先后空格
function Trim(str) {
return str.replace(/(^\s*)|(\s*$)/g, "");
}
export default reducer;
复制代码
在建立好reducer后,必定把reducer放到createStore()函数当作参数给传进去,这样store才会真正存储reducer的数据,同时把store给暴露出去,以下store文件夹中index.js的代码
import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from 'redux-devtools-extension'; // 这个是redux-devtools调试工具
import reducer from './reducer'; // 引入reducer
// 建立store
const store = createStore(reducer, composeWithDevTools(applyMiddleware()));
export default store; // 导出store
复制代码
最后在主入口文件index.js中引入store,全局进行使用的,以下代码所示
import React from 'react';
import ReactDOM from 'react-dom';
import { Input, Button, List, Modal } from 'antd'; // 引入antd组件库
import 'antd/dist/antd.css'; // 引入antd样式
import { getInputChangeAction, getAddInputContentAction, getDeleteListAction} from './store/actionCreators';
import store from './store/'; // 引入store
const { confirm } = Modal
// TodoList组件
class TodoList extends React.Component {
constructor(props) {
super(props);
// 5. 在组件内部经过getState()方法就能够拿到store里面的数据
this.state = store.getState();
// this环境的绑定
this.handleInputChange = this.handleInputChange.bind(this);
this.handleStoreChange = this.handleStoreChange.bind(this);
this.handleAddClick = this.handleAddClick.bind(this);
// 触发订阅,让store感知到state的变化
store.subscribe(this.handleStoreChange); // 接收一个函数,从新获取store最新的数据,subscribe里面必须接收一个函数,不然是会报错的,这个订阅函数放在componentWillMount生命周期函数内调用操做也是能够的
}
// componentWillMount(){
// store.subscribe(this.handleStoreChange);
// }
// 组件卸载,移除时调用该函数,通常取消,清理已注册的订阅,定时器的清理,取消网络请求,在这里面操做
componentWillUnmount() {
store.unsubscribe(this.handleStoreChange);
}
render() {
return (
<div style={{width:'600px',margin: "100px auto"}}>
<div>
<Input onChange={this.handleInputChange} value={this.state.inputValue} style={{ width:"300px",marginRight:"10px"}} placeholder="请输入内容..." />
<Button type="primary" onClick={this.handleAddClick}>提交</Button>
</div>
<List
style={{ width: '300px',marginTop:'10px'}}
bordered
dataSource={this.state.list}
renderItem={(item,index) => <List.Item onClick={this.handleDelList.bind(this, index,item)}>{item}</List.Item>}/>
</div>
)
}
handleInputChange(e) {
const action = getInputChangeAction(e.target.value);
store.dispatch(action);
}
handleStoreChange() {
console.log("handleStorechange,触发了");
this.setState(store.getState()); // 触发setState从新获取store的数据,让input的数据与store保持同步了的
}
// 添加列表的操做
handleAddClick() {
const action = getAddInputContentAction();
store.dispatch(action);
}
// 删除列表操做
handleDelList(index,item) {
this.showDeleteConfirm(index, item);
}
showDeleteConfirm(index,item) {
const action = getDeleteListAction(index);
confirm({
title: '肯定要删除该列表?',
content: item,
okText: '确认',
okType: 'danger',
cancelText: '取消',
onOk() {
console.log('OK');
store.dispatch(action);
},
onCancel() {
console.log('Cancel');
},
});
}
}
const container = document.getElementById('root');
ReactDOM.render(<TodoList />, container);
复制代码
上面的代码是渲染一个todolist组件的功能,显然对于主入口文件,咱们仍但愿它是比较干净的
咱们继续将todolist组件单独的抽离出去的
抽离容器组件
对于todolist就是一个简单的组件,那么咱们能够把它抽离出去单独定义的,在根目录src下建立一个views文件夹,这个文件夹能够放咱们的视图组件,在里面建一个TodoList.js的文件的 具体代码以下所示:
对于下面用类class定义声明的TodoList组件,称做为一个容器组件,之因此这么叫,是由于在这个组件里面包含不少业务逻辑,例如:this坏境的绑定,生命周期函数,以及一些事件处理函数等,负责整个业务功能组件的逻辑实现,也有人叫它聪明组件的,这个只是个称呼而已,没有褒贬之义 以下代码所示
import React from 'react';
import { Input, Button, List, Modal } from 'antd'; // 引入antd组件库
import 'antd/dist/antd.css'; // 引入antd样式
import { getInputChangeAction, getAddInputContentAction, getDeleteListAction} from '../store/actionCreators';
import store from '../store/index'; // 引入store
const { confirm } = Modal
// TodoList组件
class TodoList extends React.Component {
constructor(props) {
super(props);
// 5. 在组件内部经过getState()方法就能够拿到store里面的数据
this.state = store.getState();
// this环境的绑定
this.handleInputChange = this.handleInputChange.bind(this);
this.handleStoreChange = this.handleStoreChange.bind(this);
this.handleAddClick = this.handleAddClick.bind(this);
// 触发订阅,让store感知到state的变化
store.subscribe(this.handleStoreChange); // 接收一个函数,从新获取store最新的数据,subscribe里面必须接收一个函数,不然是会报错的,这个订阅函数放在componentWillMount生命周期函数内调用操做也是能够的
}
// componentWillMount(){
// store.subscribe(this.handleStoreChange);
// }
// 组件卸载,移除时调用该函数,通常取消,清理已注册的订阅,定时器的清理,取消网络请求,在这里面操做
componentWillUnmount() {
store.unsubscribe(this.handleStoreChange);
}
render() {
return (
<div style={{width:'600px',margin: "100px auto"}}>
<div>
<Input onChange={this.handleInputChange} value={this.state.inputValue} style={{ width:"300px",marginRight:"10px"}} placeholder="请输入内容..." />
<Button type="primary" onClick={this.handleAddClick}>提交</Button>
</div>
<List
style={{ width: '300px',marginTop:'10px'}}
bordered
dataSource={this.state.list}
renderItem={(item,index) => <List.Item onClick={this.handleDelList.bind(this, index,item)}>{item}</List.Item>}/>
</div>
)
}
handleInputChange(e) {
const action = getInputChangeAction(e.target.value);
store.dispatch(action);
}
handleStoreChange() {
console.log("handleStorechange,触发了");
this.setState(store.getState()); // 触发setState从新获取store的数据,让input的数据与store保持同步了的
}
// 添加列表的操做
handleAddClick() {
const action = getAddInputContentAction();
store.dispatch(action);
}
// 删除列表操做
handleDelList(index,item) {
this.showDeleteConfirm(index, item);
}
showDeleteConfirm(index,item) {
const action = getDeleteListAction(index);
confirm({
title: '肯定要删除该列表?',
content: item,
okText: '确认',
okType: 'danger',
cancelText: '取消',
onOk() {
console.log('OK');
store.dispatch(action);
},
onCancel() {
console.log('Cancel');
},
});
}
}
export default TodoList;
复制代码
其实没有作多大的代码改变,只是把原先的代码挪到另外一个文件管理了的,那么如今的项目目录结构是这样的
D:\公开课\2019\React进阶\lesson2
├─split-redux
| ├─.gitignore
| ├─package-lock.json
| ├─package.json
| ├─README.md
| ├─yarn-error.log
| ├─yarn.lock
| ├─src
| | ├─index.js // 主入口文件
| | ├─views
| | | └TodoList.js // 容器组件
| | ├─store // 组件的数据
| | | ├─actionCreators.js // action建立者
| | | ├─actionTypes.js // actionType的类型,定义成的常量
| | | ├─index.js // 建立的store主文件
| | | └reducer.js // 建立的reducer
| ├─public
| | ├─favicon.ico
| | ├─index.html
| | └manifest.json
复制代码
从这个目录树中,很是清楚了的,由起初在index.js的代码,把redux中的store,reducer,action逐渐剥离出去单独管理了的
总结
本小节主要是对上一节代码的拆分,将Redux中的store,action,以及reducer分离开来,各自独立的管理,职责分明,若是项目比较简单,一开始是能够写在一块的,而后一点一旦的拆分出去的
若是不是老司机,一开始一上来就拆分,若是对Redux的工做流程不是很清晰,不免会有所懵逼的,发现,写着,写着,找不到头绪,不知道本身在写些什么的
在实际开发当中,至于拆分的顺序,不必定按照我这种方式的,合适的流程应当时,先建立store,而后在建立reducer,肯定要作什么事情,编写action,拆分action代码,其中获取store就用getState方法,而更改store就要经过dispatch派发action,这个流程是固定的
固然这个代码仍然优化的地方,咱们在后续当中,仍会进一步的拆分的