前言
在前面的几小节中已经完成了一个todolist的添加,删除的操作,通过把组件的数据放到了Redux中的公共存储区域store中去存储,在Redux中进行状态数据的更新修改
改变store的数据唯一办法就是派发action,调用store.dispatch方法,也知道通过getState方法获取store中的所有状态数据,而实现组件页面的更新与store保持同步,必须得触发注册subscribe方法,通时还得监听一个事件处理函数
用于重新在一次获取store的数据使页面同步更新
在上几次编写Redux的代码中,创建store,reducer,acton,以及actionTypes(动作类型)都是放在一个文件当中进行编写的,然而更改store可能有多个action动作,所有代码杂糅在一起,后期维护起来显然是非常痛苦的
所以有必要进行将Redux代码进行按照特定的职责,功能结构进行拆分的,其实也就是把之前各个逻辑代码拆分到各个文件当中去单独管理的
Redux(4)-拆分Redux.jpg
完整的TodoList代码
这是上一节完整的一todolist的代码,创建store,reducer,以及action,UI组件等都是混写在一个文件当中的,这样虽然没有什么问题,但是维护起来,非常痛苦,如果一个文件里代码行数超过了130行,就应该考虑拆分代码了的,当然这并不是硬性的规定,适当的拆分有利于代码的维护,但是过度的拆分,也会增加项目的复杂程度
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文件,项目的目录树结构是这样的
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)方法
而定义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,这个流程是固定的
当然这个代码仍然优化的地方,我们在后续当中,仍会进一步的拆分的
网友评论