Redux 入门教程(二):中间件与异步操作

作者: 阮一峰

日期: 2016年9月20日

上一篇文章,我介绍了 Redux 的基本做法:用户发出 Action,Reducer 函数算出新的 State,View 重新渲染。

但是,一个关键问题没有解决:异步操作怎么办?Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。

怎么才能 Reducer 在异步操作结束后自动执行呢?这就要用到新的工具:中间件(middleware)。

一、中间件的概念

为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?

(1)Reducer:纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。

(2)View:与 State 一一对应,可以看作 State 的视觉层,也不合适承担其他功能。

(3)Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。

想来想去,只有发送 Action 的这个步骤,即store.dispatch()方法,可以添加功能。举例来说,要添加日志功能,把 Action 和 State 打印出来,可以对store.dispatch进行如下改造。


let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  next(action);
  console.log('next state', store.getState());
}

上面代码中,对store.dispatch进行了重定义,在发送 Action 前后添加了打印功能。这就是中间件的雏形。

中间件就是一个函数,对store.dispatch方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。

二、中间件的用法

本教程不涉及如何编写中间件,因为常用的中间件都有现成的,只要引用别人写好的模块即可。比如,上一节的日志中间件,就有现成的redux-logger模块。这里只介绍怎么使用中间件。


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()的功能增强。

这里有两点需要注意:

(1)createStore方法可以接受整个应用的初始状态作为参数,那样的话,applyMiddleware就是第三个参数了。


const store = createStore(
  reducer,
  initial_state,
  applyMiddleware(logger)
);

(2)中间件的次序有讲究。


const store = createStore(
  reducer,
  applyMiddleware(thunk, promise, logger)
);

上面代码中,applyMiddleware方法的三个参数,就是三个中间件。有的中间件有次序要求,使用前要查一下文档。比如,logger就一定要放在最后,否则输出结果会不正确。

三、applyMiddlewares()

看到这里,你可能会问,applyMiddlewares这个方法到底是干什么的?

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


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
  • 操作失败时的 Action

以向服务器取出数据为例,三种 Action 可以有两种不同的写法。


// 写法一:名称相同,参数不同
{ 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 也要进行改造,反映不同的操作状态。下面是 State 的一个例子。


let state = {
  // ... 
  isFetching: true,
  didInvalidate: true,
  lastUpdated: 'xxxxxxx'
};

上面代码中,State 的属性isFetching表示是否在抓取数据。didInvalidate表示数据是否过时,lastUpdated表示上一次更新时间。

现在,整个异步操作的思路就很清楚了。

  • 操作开始时,送出一个 Action,触发 State 更新为"正在操作"状态,View 重新渲染
  • 操作结束后,再送出一个 Action,触发 State 更新为"操作结束"状态,View 再一次重新渲染

五、redux-thunk 中间件

异步操作至少要送出两个 Action:用户触发第一个 Action,这个跟同步操作一样,没有问题;如何才能在操作结束时,系统自动送出第二个 Action 呢?

奥妙就在 Action Creator 之中。


class AsyncApp extends Component {
  componentDidMount() {
    const { dispatch, selectedPost } = this.props
    dispatch(fetchPosts(selectedPost))
  }

// ...

上面代码是一个异步组件的例子。加载成功后(componentDidMount方法),它送出了(dispatch方法)一个 Action,向服务器要求数据 fetchPosts(selectedSubreddit)。这里的fetchPosts就是 Action Creator。

下面就是fetchPosts的代码,关键之处就在里面。


const fetchPosts = postTitle => (dispatch, getState) => {
  dispatch(requestPosts(postTitle));
  return fetch(`/some/API/${postTitle}.json`)
    .then(response => response.json())
    .then(json => dispatch(receivePosts(postTitle, json)));
  };
};

// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
  console.log(store.getState())
);

上面代码中,fetchPosts是一个Action Creator(动作生成器),返回一个函数。这个函数执行后,先发出一个Action(requestPosts(postTitle)),然后进行异步操作。拿到结果后,先将结果转成 JSON 格式,然后再发出一个 Action( receivePosts(postTitle, json))。

上面代码中,有几个地方需要注意。

(1)fetchPosts返回了一个函数,而普通的 Action Creator 默认返回一个对象。

(2)返回的函数的参数是dispatchgetState这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。

(3)在返回的函数之中,先发出一个 Action(requestPosts(postTitle)),表示操作开始。

(4)异步操作结束之后,再发出一个 Action(receivePosts(postTitle, json)),表示操作结束。

这样的处理,就解决了自动发送第二个 Action 的问题。但是,又带来了一个新的问题,Action 是由store.dispatch方法发送的。而store.dispatch方法正常情况下,参数只能是对象,不能是函数。

这时,就要使用中间件redux-thunk


import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';

// Note: this API requires redux@>=3.1.0
const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

上面代码使用redux-thunk中间件,改造store.dispatch,使得后者可以接受函数作为参数。

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

六、redux-promise 中间件

既然 Action Creator 可以返回函数,当然也可以返回其他值。另一种异步操作的解决方案,就是让 Action Creator 返回一个 Promise 对象。

这就需要使用redux-promise中间件。


import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';

const store = createStore(
  reducer,
  applyMiddleware(promiseMiddleware)
); 

这个中间件使得store.dispatch方法可以接受 Promise 对象作为参数。这时,Action Creator 有两种写法。写法一,返回值是一个 Promise 对象。


const fetchPosts = 
  (dispatch, postTitle) => new Promise(function (resolve, reject) {
     dispatch(requestPosts(postTitle));
     return fetch(`/some/API/${postTitle}.json`)
       .then(response => {
         type: 'FETCH_POSTS',
         payload: response.json()
       });
});

写法二,Action 对象的payload属性是一个 Promise 对象。这需要从redux-actions模块引入createAction方法,并且写法也要变成下面这样。


import { createAction } from 'redux-actions';

class AsyncApp extends Component {
  componentDidMount() {
    const { dispatch, selectedPost } = this.props
    // 发出同步 Action
    dispatch(requestPosts(selectedPost));
    // 发出异步 Action
    dispatch(createAction(
      'FETCH_POSTS', 
      fetch(`/some/API/${postTitle}.json`)
        .then(response => response.json())
    ));
  }

上面代码中,第二个dispatch方法发出的是异步 Action,只有等到操作结束,这个 Action 才会实际发出。注意,createAction的第二个参数必须是一个 Promise 对象。

看一下redux-promise源码,就会明白它内部是怎么操作的。


export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

    return isPromise(action.payload)
      ? action.payload.then(
          result => dispatch({ ...action, payload: result }),
          error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          }
        )
      : next(action);
  };
}

从上面代码可以看出,如果 Action 本身是一个 Promise,它 resolve 以后的值应该是一个 Action 对象,会被dispatch方法送出(action.then(dispatch)),但 reject 以后不会有任何动作;如果 Action 对象的payload属性是一个 Promise 对象,那么无论 resolve 和 reject,dispatch方法都会发出 Action。

中间件和异步操作,就介绍到这里。下一篇文章将是最后一部分,介绍如何使用react-redux这个库。

(完)

珠峰培训

码云

留言(39条)

刚学习完阮老师的Redux 入门教程(一),写的非常通俗易懂,学习了,谢谢。

Redux在处理异步轮询的业务场景时十分尴尬:

1. 应用响应用户操作,开始轮询,每隔5秒dispatch一个异步action。
2. 应用根据上一次action的reducer结果,自动改变两次状态(doing, done)。
3. 轮询期间,应用需要根据返回结果确定是否结束轮询。

麻烦就出在步骤3中,判断逻辑应该写在component中呢还是reducer中呢?

1. 写在component的render方法中显然不合适,componentWillUpdate或者componentDidUpdate也不太好,因为组件很可能受其他action的影响而改变状态,这个时候组件的判断逻辑就会变得繁琐。
2. 写在reducer中也很尴尬,这个时候reducer要控制业务逻辑,结束轮询需要调用clearInterval,这就意味着每次发action时要带上setInterval的返回值。

Redux处理一次性异步action很方便,但对于多异步action或者链式异步action,没有合适的办法进行过程控制。对于这种情形,抛开Redux使用常规方法也许是更好的选择。

不知阮老师有何建议?

不错不错,下面该实践了

“奥秒就在 Action Creator 之中”,老师字打错了,应该是“奥妙”吧!

上周直接看的官方教程没太看懂,老师写的真是简明易懂,期待下一篇

@York: 谢谢指出,已经改正。

@Micooz:轮询的开始和结束判断,都写在 Action Creator 里面。

@Micooz:

你可以在dispatch之后再返回一个promise结果,例如:

this.props.dispatch(login(data)).then((response) => {
    if (response.code == 200) {
        // do something
    }
});

谢谢老师!!!

很认真的拜读了您的文章,写的很赞。
对redux-promise中间件中的一段话不太理解。
如果 Action 对象的payload属性是一个 Promise 对象,那么无论 resolve 和 reject,dispatch方法都会发出 Action。

我个人初步觉得 写法一,返回值是一个 Promise 对象,也可以在reject时发出action吧?
敬请赐教哈

compose真的是非常巧妙,最后生成一个洋葱剖面图的函数,dispatch(action)在最中心,中间件在两边。两句研究了半天惨研究出来,真实够厉害的。自己也真是渣T_T

到了第二章就看不懂了, 我到底缺少了什么技能。。。。。

老师没看懂
异步的话直接settimeout 然后执行dispathc不就可以了吗?

为什么需要绕一个弯,先dispatch(fn) 传入一个函数然后再在该函数内settimeout 然后再dispatch? 这里实在是想不明白,而且我写了一下确实好像没什么不一样 希望老师指点下

export default function applyMiddleware(...middlewares)

问一个比较入门的问题。

请问这里的三个点是什么意思啊

引用coolpot的发言:

export default function applyMiddleware(...middlewares)

问一个比较入门的问题。

请问这里的三个点是什么意思啊

任意长参数

表示没有明白要用到promise这个中间件来干什么。 获取到promise对象后干嘛用。。。

@Micooz:

你需要sagas

引用好炫的发言:

老师没看懂
异步的话直接settimeout 然后执行dispathc不就可以了吗?

为什么需要绕一个弯,先dispatch(fn) 传入一个函数然后再在该函数内settimeout 然后再dispatch? 这里实在是想不明白,而且我写了一下确实好像没什么不一样 希望老师指点下

两个action是为了将异步的状态信息也写入redux里面。有些项目直接可以把异步状态写到控件自己的state里面了,而有些项目是需要写到redux里面,阮老师就是讲的就是第二种情况。

如果要将异步的状态信息也写入redux里面,最少需要发两个action:第一个是同步的action,是将异步开始这个状态写入了redux;第二个action是异步方法的结果,可能是请求成功的action,也可能是失败的action,因为不可能同时成功和失败,所以阮老师说异步方法有3种action,但一次异步方法发出两种就够。

接下来阮老师说了两种方法做这件事,第一种是createAction配合redux-thunk中间件,另一种是createAction配合redux-promise中间件。这些方法好处就是你只需要调用一次就可以自动地发出这两个action,用起来比较方便。

两个方法中都没有用到settimeout,都是用promise做的,这和settimeout完全不是一回事

阮老师,redux-thunk、redux-promise、redux-saga这三个异步方案孰优孰劣您有什么看法不?

写异步的先把自己整晕了。

redux早晚得死,MLGB,搞的这么麻烦。自古以来,能在历史中一直被延续的框架,一定是操作简易的。

createAction(
'FETCH_POSTS',
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
)

阮老师,您这段代码create Action的调用方式是不是有问题,官网上貌似应该
createAction(
'FETCH_POSTS')(
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
)

感谢老师,说的很好,可是我的理解能力有点差,慢慢体会

这部分真的好难理解啊。表示看不懂

redux中的compose更新了,执行顺序从右向左。

引用曹东旭的发言:

createAction(
'FETCH_POSTS',
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
)

阮老师,您这段代码create Action的调用方式是不是有问题,官网上貌似应该
createAction(
'FETCH_POSTS')(
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
)

感谢,用这样写的方式,终于成功了

大神 您学习redux参考了哪些资料 ,网址可以发出来么

阮老师,您的这块代码少加了一个括号吧,
.then(response => {
type: 'FETCH_POSTS',
payload: response.json()
});

.then(response => ({
type: 'FETCH_POSTS',
payload: response.json()
}));

这里应该有误:下面是官方写法
第一处、
import ReduxThunk from 'redux-thunk' // no changes here

正在跟着老师学

这个做法好丑,还不如把异步放在外层,里层包装dispatch

自从接触了redux,不知道代码写在哪……

感觉现在react这一套有点故弄玄虚,为了秀某些编程思想,想的太远了,《易》曰:“易则易知,简则易从”

在中间件redux-logger这一块有一个小更新:
import createLogger from 'redux-logger';
const logger = createLogger();
现在直接引入就是一个logger,不需要再调用函数,变成如下:
import logger from 'redux-logger';

引用cisen的发言:

compose真的是非常巧妙,最后生成一个洋葱剖面图的函数,dispatch(action)在最中心,中间件在两边。两句研究了半天惨研究出来,真实够厉害的。自己也真是渣T_T

这叫责任链模式,compose是它的一种应用,完整的责任链的定义所有的含义更丰富。

感觉前面写得很好,文章第六部分“redux-promise 中间件”说的好晦涩难懂,自己捋了半天都没有很明白。贴的源码也不容易看懂。
其实我觉得您可以改成,把中间件dedux-promise和promise类型的action合成一个顺序流程来讲,他们是完整的调用流程是如何的。

有人发现官网的例子,每次启动服务时,reducer都会触发三次default吗?。。

您的教程对我很有启发,看来以后的路还长。不过相比于vuex,redux的确太麻烦了。

要不要这么复杂,这么多专业术语,表示一脸懵逼,哎...真心看不懂

我也没看懂,不知道我缺少什么技能!函数感觉太多了,根本分不清哪些是自定义函数,哪些是库函数!仍然要感谢峰哥!

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接