React-Redux 中间件和异步操作

前面所接触到的 state 状态更新,都是同步更新,但实际上 state 状态更新并不是在用户发起操作的一刻就马上更新,往往还伴随着【请求服务器 -> 等待服务器响应 -> 处理服务器响应 -> 最后更新 state 】这样的一个流程。

一开始我有这样一个疑问,在异步操作完成之后,手动调用 dispatch 分发一个 action 似乎也可以完成对 state 的状态更新管理,但如果 dispatch() 过程中,还需要加一些额外的处理,而整个应用中有几十个异步操作,那不得对每个操作结果都手动进行处理,势必是不行的,不易维护且容易出错,但使用 Redux 的中间件 (middleware) 可以巧妙的解决这两个问题:数据处理和操作完成后自动更新 state

使用中间件

redux-logger

redux-logger 用于打印日志,可以在开发过程中通过 log 清楚的跟踪 state 的改变。

1
2
3
4
5
6
7
8
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
applyMiddleware(logger)
);

redux-logger 提供一个生成器 createLogger,可以生成日志中间件 logger。然后,将它放在 applyMiddleware 方法之中,传入 createStore 方法,就完成了store.dispatch() 的功能增强,在使用 store.dispatch() 分发一个 action 的时候,会自动打印出 action,以及 state 在改变前后的状态;

createStore 方法可以接受整个应用的初始状态作为参数,此时 applyMiddleware 就是第三个参数。
中间件是有次序的,比如 logger 中间件得放到最后。

1
2
3
4
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);

applyMiddleware

applyMiddlewareRedux 的原生方法,作用是将所有的中间件组成一个数组,依次处理执行 action。下面是它的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer);
var dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {...store, dispatch}
}
}

所有中间件被放进了一个数组 chain,然后嵌套执行,最后执行 store.dispatch。可以看到,中间件内部(middlewareAPI)可以拿到 getStatedispatch 这两个方法。

异步操作流程

异步操作需要三种 action:

  • 操作发起时的 Action
  • 操作成功时的 Action
  • 操作失败时的 Action

比如一个 http 请求

1
2
3
4
5
6
7
8
9
// 写法一:名称相同,参数不同
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
// 写法二:名称不同
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
  • 操作开始时,送出一个 Action,触发 state 更新为”正在操作”状态,View 重新渲染
  • 操作结束后,再送出一个 Action,触发 state 更新为”操作结束”状态,View 再一次重新渲染

异步操作之 redux-thunk

异步操作至少要两个 action,第一个是开始操作时,这是一个同步的 action,当操作完成后,是另外一个 action,第二个 action 我们不想手动的在操作完成的函数里面使用 store.dispatch() 去发,而是由中间件自动替我们发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const getPromiseObj = function () {
return new Promise(function (resolve, reject) {
var r = Math.random();
var timeout = 0;
if (r < 0.5) {
timeout = 500;
} else {
timeout = 1000;
}
setTimeout(function () {
resolve(timeout);
}, timeout);
});
};
const begin = ()=>({
type: "BEGIN"
});
const end = coast => ({
type: "END",
coast
});
const httpFetch = function () {
return function (dispatch, getState) {
dispatch(begin());
return getPromiseObj().then(num => {
dispatch(end(num));
});
};
};

上面是一个异步操作,使用 timeout 来模仿网络请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from "react";
import {connect} from 'react-redux';
import {httpFetch} from './actions.js';
const App = React.createClass({
componentDidMount: function () {
this.props.httpFetch();
},
render() {
return <div>
<h2>Begin:</h2>
<div>{this.props.begin}</div>
<h2>Coast:</h2>
<div>{this.props.coast}</div>
<br />
</div>
}
});
const mapStateToProps = (state, ownProps) => {
return {
begin: state.begin,
coast: state.coast
}
};
const mapDispatchToProps = (dispatch, ownProps) => ({
httpFetch: ()=> {
dispatch(httpFetch());
}
});
const ContainerApp = connect(mapStateToProps, mapDispatchToProps)(App);
module.exports = ContainerApp;

App 被加载之后,使用 dispatch(httpFetch()) 发出一个 actionhttpFetch() 的返回值就是一个 action

  • httpFetch() 是一个 Action Creator(动作生成器),它返回了一个函数,而普通的 Action Creator 默认返回一个对象。
  • 返回的函数的参数是 dispatch()getState() 这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。
  • 在返回的函数被执行后,首先发出一个 Action,表示操作开始。
  • 异步操作结束之后,再发出一个 Action,表示操作结束。

上面的处理方法,就解决了自动发送第二个 Action 的问题。但是,Action 是由 store.dispatch() 方法发送的。而 store.dispatch 方法正常情况下,参数只能是对象,不能是函数。此时,就需要使用中间件 redux-thunk,它会改造 store.dispatch,使它能够接受函数作为参数。

  • reducer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const timer = function (state = {begin: 0, coast: 0}, action) {
switch (action.type) {
case "BEGIN":
return Object.assign({}, state, {
begin: new Date().getTime()
});
case "END":
return Object.assign({}, state, {
coast: action.coast
});
default:
return state;
}
};
module.exports = timer;
  • index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {createStore} from "redux";
import React from "react";
import ReactDOM from "react-dom";
import {Provider} from 'react-redux';
import {applyMiddleware} from 'redux';
import thunkMiddleware from "redux-thunk";
import createLogger from 'redux-logger';
import reducer from './reducer.js';
import ContainerApp from './ContainerApp.js';
const logger = createLogger();
const store = createStore(reducer, applyMiddleware(thunkMiddleware, logger));
ReactDOM.render(<Provider store={store}>
<ContainerApp />
</Provider>, document.getElementById('root'));

异步操作的第一种解决方案就是,写出一个返回函数的 Action Creator,然后使用 redux-thunk 中间件改造 store.dispatch