学习Redux中间件的概念,以及使用Redux中间件完成异步操作的方法。
同步和异步流程
先来复习一下Redux的基本流程:
1234567 | 1. 用户发出Action2. Store自动调用Reducer,计算返回一个新的State3. Store就会调用监听函数4. 监听函数listener中重新渲染View |
在第1、2步之间有一个问题,之前考虑的情况都是在Action发出之后,Reducer立刻计算出State,这是一个同步的过程。如果在Action发出之后,过一段时间再执行Reducer,这是异步过程:
123456789 | 1. 用户发出Action1.5 异步操作(等待一段时间)2. Store自动调用Reducer,计算返回一个新的State3. Store就会调用监听函数4. 监听函数listener中重新渲染View |
现在我们希望的是在异步操作结束后,自动执行Reducer,这就要用到中间件(middleware)
中间件的概念
什么是中间件?中间件(middleware)是一种很常见、也很强大的模式,被广泛应用在Express、Koa、Redux等类库和框架当中。
简单来说,中间件就是在调用目标函数之前,可以随意插入其他函数预先对数据进行处理、过滤,在这个过程里面你可以打印数据、或者停止往下执行中间件等。数据就像水流一样经过中间件的层层的处理、过滤,最终到达目标函数。
1234 | // 中间件可以把 A 发送数据到 B 的形式从// A -----> B// 变成:// A ---> middleware 1 ---> middleware 2 ---> middleware 3 --> ... ---> B |
具体到Redux来看,如果要实现中间件,最合适环节就是在发送Action的环节,即使用中间件包裹store.dispatch
来添加功能,比如要增加打印功能,将Action和State打印出来,我们就可以编写这样一个中间件:
1234567 | const next = store.dispatch;store.dispatch = function (action) { console.log('action: ', action); next(action); console.log('next state: ', store.getState())}; |
中间件对store.dispatch
进行了改造,在发出Action和执行Reducer之间添加了其他功能。但是实际上中间件的写法不是这样的。
在Redux中,中间件是纯函数,有明确的使用方法,并且要严格的遵循以下格式:
1234567 | var anyMiddleware = function ({ dispatch, getState }) { return function(next) { return function (action) { } }} |
中间件由三个嵌套的函数构成(会依次调用):
(1)第一层向其余两层提供分发函数dispatch
和getState
函数
(2)第二层提供next
函数,它允许你显示的将处理过的输入传递给下一个中间件或Redux(这样Redux才能调用所有reducer)。实际上next
作为参数,就是通过componse
传入的下一个要执行的函数,通过next(action)
就将action
传递给了下一中间件
(3)第三层提供从上一个中间件或者从dispatch
传递过来的Action,这个Action可以调用下一个中间件(让Action继续流动)或者以想要的方式处理action
所以一个Log的中间件应该这样写:
12345678 | function ({ dispatch, getState }) { return function(next) { return function (action) { console.log('logMiddleware action received:', action) return next(action) } }} |
next(action)
就是继续传递Action,如果不进行这一步,所有的Action都会被丢弃。
中间件的用法
常用的中间件都有现成的,不用我们自行编写,只需要直接引用别人写好的模块即可,比如上面的打印日志的中间件,就可以使用现成的redux-logger模块:
12345678 | import { applyMiddleware, createStore } from 'redux';import createLogger from 'redux-logger';const logger = createLogger();const store = createStore( reducer, applyMiddleware(logger)); |
使用的时候首先通过redux-logger提供的生成方法createLogger
创建一个中间件实例logger
,然后将它放在Redux提供的applyMiddleware
方法中,放到createStore
方法中(由于createStore
方法可以接受应用的初始状态作为第二个参数,这个时候applyMiddleware
方法就是第三个参数了)
有的中间件有次序要求,必须放在何时的位置才能正确输出,使用之前要查看文档。
applyMiddleware()
applyMiddleware()
是Redux的原生方法,会将所有中间件组成一个数组,依次执行,下面是它的源码:
12345678910111213141516 | export default function (...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} }} |
applyMiddleware
可以接受多个中间件作为参数,全部放进了数组chain
中,每个中间件接受Store的dispatch
和getState
函数作为命名参数,返回一个函数。该函数会被传入称为next
的下一个中间件的dispatch
方法,并返回一个接受Action的新函数,这个函数可以直接调用next(action)
。这个过程是通过compose
方法完成的。
多个中间件形成了一个调用链,调用链中的最后一个中间件会接受真实Store的dispatch
作为next
参数,并借此结束调用链。
12 | ({ getState, dispatch }) => next => action |
compose()
compose(...functions)
的功能是从右到左来组合多个函数,这是函数式编程的方法,其中每个函数的返回值作为参数提供给左边的函数:
123 | compose(funcA, funcB, funcC); funcA(funcB(funcC())) |
关于compose
方法,以前做过一道练习题《前端练习17 函数式编程的compose函数》,手写简易的compose
方法。
1234 | const store = createStore( reducer, applyMiddleware(thunk, promise, logger)); |
异步操作的基本思路
处理异步操作需要使用中间件。
同步操作只要发出一种Action即可,异步操作的差别是要发出三种Action
123 | - 操作发起时的Action- 操作成功时的Action- 操作失败时的Action |
以向服务器取出数据为例,三种Action有两种不同的写法:
123456789 | { type: 'FETCH_POSTS' }{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }{ type: 'FETCH_POSTS', status: 'success', respose: {} }// 写法二, 名称不同{ type: 'FETCH_POSTS' }{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }{ type: 'FETCH_POSTS_SUCCESS', respose: {} } |
除了Action种类不同,异步操作的State也要进行改造,反映不同的操作状态,例如:
123456 | const state = { // ... isFetching: true, didInvalidate: true, lastUpdated: 'xxxxxxx'} |
State中的属性isFetching
表示是否正在抓取数据,didInvalidate
表示是否正过期,lastUpdated
表示上一次更新事件。
现在整个异步操作的思路就很清晰了:
12 | 1. 操作开始,发出一个Action,触发State更新为“正在操作”状态,View重新渲染2. 操作结束,再次发出一个Action,触发State更新为“操作结束”状态,View再次重新渲染 |
redux-thunk中间件
异步操作至少要发出两个Action,用户操作触发第一个Action,这个和同步操作一样,标识着异步操作的开始,现在要做的是在异步操作结束时,自动发送第二个Action
奥妙就在Action Creator中,需要对其进行改造。我们有一个组件,点击按钮后会发出一个Ajax请求,将返回的结果填充在视图中,按钮的点击事件如下:
123456789101112131415161718192021 | sendQuestion() { const question = this.state.questionInput; // Action Creator1 const requestPost = (question) => ({ type: 'SEND_QUESTION', status: 'sending...', question }); // Action Creator2 const receivePost = (answer) => ({ type: 'RECEIVE_ANSWER', status: '', answer }); // Action Creator3 const actionCreator = () => (dispatch, getState) => { dispatch(requestPost(question)); // 重置输入框 this.setState({ questionInput: '' }); return Request.demo2.getAnswer({ question }) .then(res => dispatch(receivePost(res))) }; store.dispatch(actionCreator())} |
其中最关键的就是actionCreator
,它的返回值是一个函数,这个函数执行时,会先发出一个ActionrequestPost
(由Action Creator生成)并进行其他同步操作,然后进行异步操作Request.demo2.getAnswer({ question })
,在异步操作的回调函数中发出第二个ActionactionCreator
(由Action Creator2生成)。
上面的代码中,有几点要注意:
(1)完成异步操作的Action CreatoractionCreator
返回的是一个函数,普通的Action Creator返回的是Action对象
(2)返回的这个函数参数是dispatch
和getState
这两个Redux方法,普通的Action Creator参数是Action的内容。
(3)在返回的函数中,先发出的Actiondispatch(requestPost(question))
表示操作开始
(4)异步操作结束后,在发出的Actiondispatch(receivePost(res))
表示操作结束
第二点中,返回函数的两个Redux方法是执行时由函数的执行者传进去的,函数的执行者是谁呢?就是中间件redux-thunk
为什么要使用redux-thunk?因为Action是由store.dispatch
发出的,这个方法接受的参数是一个对象,而我们的Action Creator返回的是一个函数,使用redux-thunk对store.dispatch
进行改造,改造后在执行Action Creator返回的函数时就传入了dispatch
和getState
两个参数
12345678910 | import { createStore, applyMiddleware } from "redux";import reducer from "./reducers/index";// 使用thunk中间件,使dispatch可以接受函数作为参数(默认只能接受Action对象作为参数)import thunk from 'redux-thunk';// 创建Storeconst store = createStore(reducer, applyMiddleware(thunk));export default store; |
因此,异步操作的第一种解决方案就是,==编写一个返回函数的Action Creator,然后使用redux-thunk中间件改造store.dispatch
==
redux-promise中间件
在上面的Action Creator返回了一个函数,也可以返回其他值,另一种异步操作的解决方案,就是让Action Creator返回一个Promise对象
这需要使用redux-promise中间件
12345678910 | import { createStore, applyMiddleware } from "redux";import reducer from "./reducers/index";// 使用redux-promise中间件,使dispatch可以接受Promise作为参数import promiseMiddleware from 'redux-promise'// 创建Storeconst store = createStore(reducer, applyMiddleware(promiseMiddleware));export default store; |
来看一下它的源码:
12345678910111213141516171819202122 | import isPromise from 'is-promise';import { isFSA } from 'flux-standard-action';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
方法提交,reject
后不会有任何动作,如果Action本身不是一个Promise对象,而Action对象的payload
属性是一个Promise对象,那么无论其resolve或reject,dispatch
都会发出Action
所以有两种写法,一种是让Action本身返回一个Promise对象:
12345678910111213141516171819202122 | sendQuestion() { const question = this.state.questionInput; // Action Creator1 const requestPost = (question) => ({ type: 'SEND_QUESTION', status: 'sending...', question }); // Action Creator2 const receivePost = async () => ({ type: 'RECEIVE_ANSWER', status: '', answer: await Request.demo2.getAnswer({ question }) }); store.dispatch(requestPost(question)); // 重置输入框 this.setState({ questionInput: '' }); store.dispatch(receivePost());} |
更常见的是第二种写法,一般会配合redux-action中间件使用。
redux-action中createAction
的用法:
12345678 | const a = createAction('test1', () => 10);a(); // {type: "test1", payload: 10}const b = createAction('test2');b(100); // {type: "test2", payload: 100} |
使用redux-action将上面的写法改为:
1234567891011121314151617181920212223 | // 使用redux-promise中间件解决异步操作第二种写法sendQuestion() { const question = this.state.questionInput; // Action Creator1 const requestPost = (question) => ({type: 'SEND_QUESTION', status: 'sending...', question}); // 发出同步Action store.dispatch(requestPost(question)); // 重置输入框 this.setState({ questionInput: '' }); // 发出异步Action store.dispatch( createAction('RECEIVE_ANSWER')( // Promise的then函数返回值才是createAction的第二个参数 Request.demo2.getAnswer({question}).then(v => ({ status: '', answer: v }) ) ) );} |
注意,createAction的第二个参数实际上就是向要发送的Action的payload
属性值,这里必须是一个Promise对象。(在reducer里面也必须从action.payload
属性中获取对应的值)
明显,使用redux-promise的代码量更小一些,但是也因此失去了一定的灵活度,它的同步Action是脱离在异步操作之外单独存在的(即无法在一个Action Creator完成多个dispatch
动作)
其他的比较热门的解决方案还有redux-promise-middleware(感觉像是前两者的一个集合)、redux-action-tools、redux-saga,可以学习这篇文章的讲解。
总结
学习Redux的异步操作和中间件之后,最大的体会就是太繁琐了,各种解决方案太多了。如果是复杂的项目中,有着复杂的业务逻辑,使用Redux会是一个很麻烦的事情。
以前在做一个React项目时,项目组选型使用的Mobx,当时没觉得有好用(当然也有用的比较浅的原因),但是仅仅是学习Redux,就发现Mobx或者是Vuex真的比Redux好上手太多了,Redux的函数式编程的思想带来的难度不仅是阅读、学习的难度,更是过多的范式代码带来的苦恼。
我认为会经久流传的解决方案一定会在可阅读性、可维护性以及入手难度上取得一个比较好的平衡,除非它是为了解决一些别人无法解决的问题而提出的,是一个时间段内近乎唯一的解决方案,但我感觉Redux好像并不是这样。