Redux在我看来,是和Vuex一样的存在,是为了跳出继承传值的条框,方便组件之间通信的一个方法。同时,它也有自己触发的体系。
首先还是看看Redux的流程图吧:
我用文字,描述一下需要用到的API以及经过的点:
1、ReactComponents:发起action 和 接收store的组件,组件发起action用store.dispath(),接收store返回的信息可以用store.getState()。如果想要实时订阅store变化返回的值,可以用store.subscribe(fn),其中,store是通过Redux中的createStore创建的。
2、store:中转站,转发组件传过来的action给Reducer,接收Reducer返回的state,并在返回的state有更新的时候实时更新并发给有实时订阅的组件
3、Reducers:处理state和action的地方根据不同的action,返回不同的store。
再直白点说,就是组件将改变,(通过actionCreators)封装成action通过dispath传给store,再由store传给Reducer,但是Redecer怎么知道如何处理数据呢?通过传入的action,判断该如何处理数据。处理好后返回给store,再由store派发给订阅的组件,组件再实时更新。
使用Redux,首先需要安装Redux插件:
npm install redux
//or
yarn add redux
建议将流程在心里熟悉之后再看demo,这里我准备的demo 是一个todoList,为了看起来美观点用了antd简单布局了一下,这里要实现的效果有,input框键入数值的时候实时更新,点击提交按钮的时候,list会添加键入数值,如果点击该list的项,会删除对应的项。
代码部分,组件:
import React, { Component } from 'react';
import { Input, Button, List } from 'antd';
import { ArrowRightOutlined } from '@ant-design/icons'
import store from './store'
import './app.css'
class App extends Component {
constructor(props) {
super(props);
this.state = store.getState()
this.handleStoreChange = this.handleStoreChange.bind(this)
this.handleInputChange = this.handleInputChange.bind(this)
this.handleButtonClick = this.handleButtonClick.bind(this)
store.subscribe(this.handleStoreChange)
}
handleStoreChange() {
this.setState(store.getState())
}
handleInputChange(e) {
const action = {
type: 'handle_input_change',
value: e.target.value
}
store.dispatch(action)
}
handleButtonClick() {
const action = {
type: 'handle_button_click'
}
store.dispatch(action)
}
handleItemClick(index) {
const action = {
type: 'handle_item_click',
index
}
store.dispatch(action)
}
render() {
return (
<div>
<div className={'total'}>
<div className={'title'} >
<div>
<Input style={{ width: 525 }} value={this.state.inputValue} onChange={(e) => { this.handleInputChange(e) }} />
</div>
<div style={{ paddingLeft: 10 }}>
<Button type="primary" onClick={() => { this.handleButtonClick() }}><ArrowRightOutlined style={{ color: 'white' }} /></Button>
</div>
</div>
<div className={'list-area'}>
<List
className={'list ant-list'}
bordered
size='large'
split='true'
dataSource={this.state.list}
renderItem={(item, index) => (
<List.Item >
<div onClick={() => { this.handleItemClick(index) }}> {item}</div>
</List.Item>
)} />
</div>
</div>
</div>
)
}
}
export default App
在这里,action是一个object,包括type和对应的value,type为必须项,value可选。该组件的state全靠reducer返回。
store我单独放的一个文件夹,在同级目录里,通过import引入功能。
store的代码:
import reducer from './reducer';
import { createStore } from 'redux';
const store = createStore(reducer);
export default store
通过Redux提供的createStoreAPI,建立一个store,为了可读性,我将store和reducer都单独建了一个文件夹,放在一起也是可以相互引用的。
reducer的代码:
const defaultState = {
list: ['hello', 'world', 'React'],
inputValue: '2222'
}
export default (state = defaultState, action) => {
console.log('state:', state, 'action', action)
let newstate = JSON.parse(JSON.stringify(state))
switch (action.type) {
case 'handle_input_change':
newstate.inputValue = action.value
return newstate
case 'handle_button_click':
newstate.list.push(newstate.inputValue);
newstate.inputValue = ''
return newstate
case 'handle_item_click':
newstate.list.splice(action.index, 1)
return newstate
default:
return state
}
}
其实reducer是一个纯函数,接收state和action,且这个函数不能直接修改state,修改state只能通过深拷贝,修改并返回备份。这里可以看到,我是通过判断action.type来做出相应的增删改的处理。
在reducer中,我将defaultState定义好了并传值,如果按照日常开发,这里应该是空,后续请求数据填充进去,这里需要注意一点,虽然是空,但是需要将需要用到的state的类型写入defaultState中,例如上面这个defaultState,应该写成:
const defaultState = {
list: [],
inputValue: ''
}
这一点和React组件不同,在组件里面,state可以不先定义在this.state中,只要不涉及到页面渲染的,可以用setState定义并赋值,如果涉及到页面渲染又没有定义,后续用setState赋值的,在赋值前会是undefind,不会报错,且无论网速多块,这个undefind都会闪一下。
至此,一个小的demo就做好了,但是仔细看流程图,action是被actionCreators封装发出的,但是我的代码中是组件自己封装好发出的,这个时候就存在问题:
1、如果代码复杂,那么散落在各处的action增加后期维护时间和精神成本;
2、actionType是通过创建action的时候手动打的字段,可能出现手打错误但是错误不明显,有了bug浏览器不会报错(不信可以自己实验一下),找的话也耗费时间,更没有头绪。这个时候推荐自己创建一个actionTypes统一规划actionType,如果引用出错,浏览器也会精准的报错。
根据这两个问题,我们优化一下代码结构,这个时候整体结构如下:
首先,我们集合一下actionTypes:
export const HANDLE_INPUT_CHANGE = 'handle_input_change'
export const HANDLE_BUTTON_CLICK = 'handle_button_click'
export const HANDLE_ITEM_CLICK = 'handle_item_click'
reducer那里也涉及到了actionTypes,相应做出更换,为了避免冗余,代码仅贴部分改动的部分:
case HANDLE_INPUT_CHANGE:
newstate.inputValue = action.value
return newstate
然后,再将action全部提到actionCreator里面:
import { HANDLE_INPUT_CHANGE, HANDLE_BUTTON_CLICK, HANDLE_ITEM_CLICK } from './actionTypes'
export const inputChangeAction = (value) => ({
type: HANDLE_INPUT_CHANGE,
value
})
export const buttonClickAction = () => ({
type: HANDLE_BUTTON_CLICK
})
export const itemClickAction = (value) => ({
type: HANDLE_ITEM_CLICK,
index: value
})
最后,我们修改组件,首先,头部引用actionCreator:
import { inputChangeAction, buttonClickAction, itemClickAction } from './actionCreator'
action处:
handleInputChange(e) {
const action = inputChangeAction(e.target.value);
store.dispatch(action)
}
然后我们验证一下,我们新写一个组件b.js,在里面展示从Redux得到的数据:
import React from 'react'
import store from './store'
let data = store.getState()
class B extends React.Component {
constructor(props) {
super(props);
this.getStateData = this.getStateData.bind(this);
store.subscribe(this.getStateData)
}
getStateData = () => {
data = store.getState()
}
render() {
return (
<div>接收到的数据是:{data.list}</div>
)
}
}
export default B;
页面的变化:
这里可以看到,只要是注册了store,就可以从里面得到当前state里的值(前提是用subscribe订阅了)。在实际使用中,可以跳出继承的限制,如果组件有数据更新,发出diapath后store更新,那么引用store里相应的值的组件也会发现相应的更新,这个功能其实非常的使用,比如在地区页选择了地区,相应的主页上会显示出相应的选择,等等。
Redux-thunk:
这是一个Redux的中间件,它的主要作用是将action从对象变成函数,可以处理异步函数,而Redux只能处理同步的数据。现在的工程中,异步函数的使用必不可少,所以Redux-thunk是个不得不提的中间件。且在action里可以引入dispath,更加方便管理。
首先,得安装这个中间件:
npm install --save redux-thunk
//or
yarn add redux-thunk
为了演示异步操作,我用express在本地的后端起了个服务,占用7001接口(另一个工程):
const express = require('express')
const app = express();
app.get('/api', (req, res) => {
res.json(['learn', 'redux', 'thunk'])
})
app.listen(7001, () => {
console.log('serverisrunning')
})
功能只有简单的返回数组,然后改一下前端工程的代理:
npm install http-proxy-middleware -D
因为我是react16,通过package.json改代理会出现报错,所以装一个中间件消除报错。
在src目录下新建一个setupProxy,内容:
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:7001/api',
pathRewrite: {
"^/api": ""
},
changeOrigin: true,
})
);
};
如果觉得自己起服务然后引用过于复杂,可以用charles等抓包工具做代理,引入本地的json,但是最新版本的charles不支持。
背景介绍完毕,接下来我们引入redux-thunk,在store中引入并注册:
import thunk from 'redux-thunk';
import { createStore, applyMiddleware } from 'redux';
const store = createStore(reducer, applyMiddleware(thunk));
到组件中,在componentDidMount生命周期中,加入:
const action = initData();
store.dispatch(action)
再到actionCreator中加入:
export const initDataAction = (value) => ({
type: INIT_DATA_ACTIONS,
value
})
export const initData = () => {
return dispatch => {
//这里做异步的操作
axios.get('/api').then((res) => {
const action = initDataAction(res.data)
dispatch(action)
})
}
}
相应的,reducer:
case INIT_DATA_ACTIONS:
newstate.list = action.value
return newstate
actionTypes:
export const INIT_DATA_ACTIONS = 'init_data_actions'
得到的效果如下:
如果有不正确的,欢迎指正,我及时修改。
详细的代码请见:https://github.com/PYLDora/React-Redux