redux 中间件入门到编写,到改进,到出门

春江花月夜调 bug,孤独寂寞日著 blog。吾母昨夜托梦,吾儿只身在外,读书撩妹两手要抓,前端知识切莫荒废。如无对象可面向,可以学学函数式。多写代码少睡觉,还有周五的周报。redux 要会,middleware 能写,有空记得写博客,写好发给我看看。

惊醒之余,其敦敦教诲不敢忘,乃正襟危坐,挑灯写下这篇博客,感动~

redux 中间件

redux 提供了类似 Web 开发的中间件机制,Web 中经过中间件的是一个个请求,而 redux 中经过中间件的是一个个 action,使得开发人员能够在中间件中针对特定 action 进行各种统一的处理,比如日志打印,数据请求,错误处理等。

如何使用

redux 提供 applyMiddleware 方法,通过如下方式即可应用中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {
createStore,
applyMiddleware,
compose,
} from 'redux'
import nextAndRequest from './middleware/redux-next-and-request'
import errorCatcher from './middleware/redux-error-catcher'
import reducer from '../reducer'

const createStoreWithMiddleware = compose(
applyMiddleware(
nextAndRequest,
errorCatcher,
),
DevTools.instrument(),
window.devToolsExtension(),
)(createStore)

实现原理

compose 函数

此处有个神奇的函数,即 compose,该函数在 applyMiddleware 中也是核心代码,正是它实现了 redux 中间件的机制,我们来看看 compose 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Composes single-argument functions from right to left. The rightmost
* function can take multiple arguments as it provides the signature for
* the resulting composite function.
*
* @param {...Function} funcs The functions to compose.
* @returns {Function} A function obtained by composing the argument functions
* from right to left. For example, compose(f, g, h) is identical to doing
* (...args) => f(g(h(...args))).
*/

export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}

return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

从注释中可知,它的作用是从右到左将多个函数组合成一个新函数,其中最右边的函数消耗了该新函数的参数,并逐级向左作为参数依次执行;

执行 compose(f1, f2, f3) 可得 (…args) => f1(f2(f3(…args)));核心操作为 reduce,详细使用方式可参看文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
第一次 reduce
previousValue: f1;
currentValue: f2;
returnValue: (...args1) => f1(f2(...args1)) // 记为 R1

第二次 reduce
previousValue: R1
currentValue: f3;
returnValue: (...args2) => R1(f3(...args2)) // 记为 R2

其中 R2:
(...args2) => ((...args1) => f1(f2(...args1)))(f3(...args2))

此时传入 args 执行 R2
第一步: 得到 ((...args1) => f1(f2(...args1)))(f3(args)) // 记为 R3
第二步:f3(args) 即是 R3 的参数 (...args1),继续执行可得 f1(f2(f3(args)))

其实之前这个 compose 方法不是使用 reduce 实现的,而是使用 reduceRight 实现 composeRight,因此对比新版实现,比较好理解,原来的版本为:

新版 Merge Request
新版的方式使用惰性求值,性能有提升

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
/**
* Composes single-argument functions from right to left. The rightmost
* function can take multiple arguments as it provides the signature for
* the resulting composite function.
*
* @param {...Function} funcs The functions to compose.
* @returns {Function} A function obtained by composing the argument functions
* from right to left. For example, compose(f, g, h) is identical to doing
* (...args) => f(g(h(...args))).
*/

export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

funcs = funcs.filter(func => typeof func === 'function')

if (funcs.length === 1) {
return funcs[0]
}

const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

让我们回到:

1
2
3
4
5
6
7
8
const createStoreWithMiddleware = compose(
applyMiddleware(
nextAndRequest,
errorCatcher,
),
DevTools.instrument(),
window.devToolsExtension(),
)(createStore)

createStoreWithMiddleware 的最终值为:

1
applyMiddleware(nextAndRequest,errorCatcher)(DevTools.instrument()(window.devToolsExtension()(createStore)))

applyMiddleware 函数

其源码如下:

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
37
import compose from './compose'

/**
* Creates a store enhancer that applies middleware to the dispatch method
* of the Redux store. This is handy for a variety of tasks, such as expressing
* asynchronous actions in a concise manner, or logging every action payload.
*
* See `redux-thunk` package as an example of the Redux middleware.
*
* Because middleware is potentially asynchronous, this should be the first
* store enhancer in the composition chain.
*
* Note that each middleware will be given the `dispatch` and `getState` functions
* as named arguments.
*
* @param {...Function} middlewares The middleware chain to be applied.
* @returns {Function} A store enhancer applying the middleware.
*/
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []

const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
...store,
dispatch
}
}
}

一个 redux 中间件的结构:

1
2
3
store => next => action => {
// 中间件逻辑代码
}

假设有三个中间件 M1, M2, M3,应用 applyMiddleware(M1, M2, M3) 将返回一个闭包函数,该函数接受 createStore 函数作为参数,使得创建状态树 store 的步骤在这个闭包函数内执行;
接着将 store 重新组装成 middlewareAPI 作为新的 store,也就是我们编写的中间件最外层函数的参数 store,这样中间件就可以根据状态树进行各种操作了。

可以发现重新组装之后的 store 只有两个方法,一个是用户获取 state 的 getState 方法,另一个是用于分发 action 的 dispatch,而 setState、subscribe、replaceReducer 等方法则不提供,setState 在设置状态时重新 render 可能会触发新的 action 而导致死循环;setState 本身就是用于订阅每个 dispatch 操作,此时 dispatch 就在你手上(next),根本不需要订阅;replaceReducer 用于动态加载新的 reducer,我猜你用不到。

将中间件数组中的函数逐一传入参数 middlewareAPI 并执行,从而得到 chain 数组,此时 chain 数组中的每个函数长这样:

1
2
3
next => action => {
// 中间件逻辑代码
}

核心代码解读

dispatch = compose(…chain)(store.dispatch)

假设 chain 是包含 C1, C2, C3 三个函数的数组,那么 compose(…chain)(store.dispatch) 即是 C1(C2(C3(store.dispatch))), 因此易知:

  1. applyMiddleware 的最后一个中间件 M3 中的 next 就是原始的 store.dispatch;
  2. M2 中的 next 为 C3(store.dispatch);
  3. M1 中的 next 为 C2(C3(store.dispatch));

最终将 C1(C2(C3(store.dispatch))) 作为新的 dispatch 挂在 store 上返回给用户,因此这就是用户切实调到的 dispatch 方法,既然层层执行了 C3,C2, C1,那么一个中间件已经被拆解为:

1
2
3
action => {

}

触发 action 的完整流程

有了这个 dispatch 方法和被扒光的中间件,我们来梳理一遍当用户触发一个 action 的完整流程:

  1. 手动触发一个 action:store.dispatch(action);
  2. 即调用 C1(C2(C3(store.dispatch)))(action);
  3. 执行 C1 中的代码,直到遇到 next(action),此时 next 为 M1 中的 next,即:C2(c3(store.dispatch));
  4. 执行 C2(c3(store.dispatch))(action),直到遇到 next(action),此时 next 为 M2 中的 next,即:C3(store.dispatch);
  5. 执行 C3(store.dispatch)(action),直到遇到 next(action),此时 next 为 M3 中的 next,即:store.dispatch;
  6. 执行 store.dispatch(action),store.dispatch 内部调用 root reducer 更新当前 state;
  7. 执行 C3 中 next(action) 之后的代码
  8. 执行 C2 中 next(action) 之后的代码
  9. 执行 C1 中 next(action) 之后的代码

即:C1 -> C2 -> C3 -> store.dispatch -> C3 -> C2 -> C1

洋葱模型有没有!!!

如何编写

讲了这么多,终于切入正题,开始写中间件了,目标是实现中间件,使得异步请求,错误处理都能经由中间件处理;而不需要每次手动繁琐的发起异步请求,同时每个异步请求语句之后都手动处理错误代码。

先从简单的错误处理中间件开始~

错误处理中间件

通过检测 action 上是否存在 error 字段,来决定是否抛出错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { notification } from 'antd'

export default store => next => async action => {
try {
if(action.error) {
throw new Error(action.error)
} else {
next(action)
}
} catch (err) {
notification.error({
message: '错误信息',
description: err.message
});
throw err
}
}

当发现 action 中有 error 字段,则抛出错误,这个字段可由上游中间件出错后,将对应的错误信息挂在 action.error 上,使得本中间件能够处理这个错误,由于项目基于 antd,此处将所有错误都通过 notification 组件在右上角弹窗显示;

如果做成通用的错误处理的话,可以再包一层函数,传入错误处理函数,便能够自定义错误处理函数了:

1
2
3
4
5
6
7
8
9
10
11
12
export default handler => store => next => action => {
try {
if(action.error) {
throw new Error(action.error)
} else {
next(action)
}
} catch (err) {
handler && handler(err)
throw err
}
}

则使用方式变为:

1
2
3
4
5
6
7
8
9
10
11
12
const createStoreWithMiddleware = compose(
applyMiddleware(
nextAndRequest,
errorHandler(err => {
notification.error({
message: '错误信息',
description: err.message
})
}),
),
window.devToolsExtension
)(createStore)

异步请求处理中间件

版本一

通过判断 action 字段上是否用 url 字段来判断是否需要发起异步请求,同时将请求结果挂在 action 的 result 字段上,供下一个中间件或 reducer 使用。

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
37
38
39
40
41
42
import request from './request'

export default store => next => async action => {
if (action.url) {
try {
const execAction = async act => {

if(act.url) {
const {
code,
data,
error,
} = await request({
url: act.url,
method: act.method || 'get',
data: act.data || {},
})

if (code !== 0) {
throw new Error(error || '未知错误!')
} else {
return data
}
}
}

const result = await execAction(action)

next({
result,
...action
})

} catch (error) {
next({
error: error.message,
})
}
} else {
next(action)
}
}

版本二

由于本项目大部分情况需要在执行一个异步 action 之后,再重新执行一个异步 action,达到更新当前列表的目的。

例如删除或添加一条记录后,希望更新当前列表信息

因此做如下更改,在 action 上增加一个 nextAction 字段,使得能够在执行当前 action 之后,接着执行一个 action:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import request from './request'

export default store => next => async action => {
if (action.url || action.nextAction) {
try {
const execAction = async act => {

if(act.url) {
const {
code,
data,
error,
} = await request({
url: act.url,
method: act.method || 'get',
data: act.data || {},
})

if (code !== 0) {
throw new Error(error || '未知错误!')
} else {
return data
}
}
}

const result = await execAction(action)

next({
result,
...action
})

if (action.nextAction) {
const act = action.nextAction
const nextAction = typeof act === 'function' ? await act(result, action) : act
const nextResult = await execAction(nextAction)

next({
result: nextResult,
lastResult: result,
...nextAction
})
}

} catch (error) {
next({
error: error.message,
})
}
} else {
next(action)
}
}

为了方便执行一些额外的操作,此处 nextAction 也可以是一个函数,该函数必须返回一个 action,同时将当前 action 的返回值作为回调传入这个函数,nextAction 执行之后,除了将请求结果作为 result 字段挂在 action 之外,还加入了一个 lastResult 字段保存首次 action 的值。

版本三

目前只能支持一级 nextAction,如果要支持多级的话,可以传入数组,数组中可以是一个普通的 action,也可以是返回一个 action 的函数,完整代码如下:

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
37
38
39
40
41
42
43
44
45
46
// index.js

import execAction from './exec-action'
import execNextAction from './exec-next-action'
import isFunction from './is-function'
import isArray from './is-array'

export default () => next => async action => {
if (action.url || action.nextAction) {
try {
const result = await execAction(action)

next({
result,
...action
})

if (action.nextAction) {
let nextAction = action.nextAction
let lastResult = result
let lastAction = action

if(isFunction(nextAction)) {
nextAction = await nextAction(lastResult, lastAction)
await execNextAction(nextAction, lastResult, next)
} else if(isArray(nextAction)) {
let currentAction
for( let i = 0; i < nextAction.length; i++ ) {
lastAction = nextAction[i - 1] ? nextAction[i - 1] : lastAction
currentAction = isFunction(nextAction[i]) ? await nextAction[i](lastResult, lastAction) : nextAction[i]
await execNextAction(currentAction, lastResult, next)
}
} else {
await execNextAction(nextAction, lastResult, next)
}
}

} catch (error) {
next({
error: error.message,
})
}
} else {
next(action)
}
}
1
2
3
// is-array.js

export default param => Array.isArray(param)
1
2
3
// is-function.js

export default param => typeof param === 'function'
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
// request.js

import reqwest from 'reqwest'

export default async opts => {
const defaultOpts = {
type: 'json',
url: `/routers${opts.url}`,
}

const finalOpts = {
...opts,
...defaultOpts,
}

let ret
try {
ret = await reqwest(finalOpts)
return ret
} catch (e) {
try {
ret = JSON.parse(e.response)
} catch (e) {
ret = e.message
}

return ret
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// exec-action.js

import request from './request'

export default async act => {

if(act.url) {
const {
code,
data,
error,
} = await request({
url: act.url,
method: act.method || 'get',
data: act.data || {},
})

if (code !== 0) {
throw new Error(error || '未知错误!')
} else {
return data
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// exec-next-action.js

import execAction from './exec-action'

export default async (nextAct, lastResult, next) => {
const result = await execAction(nextAct)

next({
result,
lastResult,
...nextAct
})
}

如此这般,便能开心的写页面了~
好了,我要发给我妈看看。

redux 中间件入门到编写,到改进,到出门

http://quanru.github.io/2017/03/18/编写 redux 中间件/

作者

林宜丙

发布于

2017-03-18

更新于

2023-07-13

许可协议