Redux 中的 Middleware

Redux 中的 Middleware

之前在学习 Koa.js 当中的 中间件 部分时,曾经接触到过中间件这个概念,也了解洋葱模型这个概念,所以在今天我们就来深入的了解一下 Redux 当中与其十分类似的 middleware

什么是 Middleware

所谓中间件,就是处在服务业务与用户应用中间的软件(架构),主要用来将具体业务和底层逻辑解耦的组件,在 Node.js 当中,middlewarereqres 之间的中间层,可以用来处理很多事情,但是在 Redux 里面,middleware 又是什么呢?在 Reduxmiddleware 的文档里面有这样一句话

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

不难理解,在 Redux 里的 middleware 是发送 actionaction 到达 reducer 之间的第三方扩展,也就是中间层,middleware 提供了一个分类处理 action 的机会,在 middleware 中你可以检阅每一个流过的 action,挑选出特定类型的 action 进行相应操作,简单的来说就是,middleware 是架在 actionstore 之间的一座桥梁

为什么要引入 Middleware

我们先来看一个简单的同步数据流的例子,如下图

当我们点击了 button 以后,在回调中 dispatch 一个 action,然后 reducer 收到 action 后,更新 state 并通知 view 重新渲染,一个很常规的同步数据流动的场景,但是如果我们现在的需求有所变动,比如需要打印每一个 action 信息用来调试,又或者需要在点击了 button 以后进行一些别的操作等等

面对多种多样的业务需求,单纯的修改 dispatchreducer 的代码显然不是很理想的选择,我们需要的是可以组合的,自由插拔的插件机制,所以,在这种情况下,我们就会用上 middleware,如下图

每一个 middleware 处理一个相对独立的业务需求,通过串联不同的 middleware,实现变化多样的的功能,但是在本章当中我们不会过多关注它的内部实现原理,这一部分内容我们在之前的 Redux 源码初探 章节当中已经梳理过了,所以在本章当中,我们重点关注以下几个问题

  • middlewares 是如何工作的?
  • Redux 是如何让 middlewares 串联起来的?
  • middleware 中调用 dispatch 会发生什么?
  • 我们自己的 middlewares 该怎么写?

下面我们就一个一个来看

Middlewares 是如何工作的?

在此之前,我们先简单的介绍一下箭头函数的用法,因为后面我们会经常遇到类似的写法,如下示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 一个标准的闭包函数
function A() {
var i = 0
return function() {
return ++i
}
}

// 我们使用箭头函数来进行改写
var A = (i = 0) => { return _ => ++i }

// 又因为仅有一个返回,所以我们可以省略 return 和 大括号
var A = (i = 0) => _ => ++i

下面我们就正式开始,我们首先先来简单的定义一个日志中间件,如下

1
2
3
4
5
6
// logger
export default store => next => action => {
console.log('dispatch', action)
next(action)
console.log('finish', action)
}

Redux 当中提供了 applyMiddleware 这个 API 用来加载 middleware,我们将其与我们上面定义的 logger 中间件放到一起来进行对比介绍,如下图所示

函数式编程思想设计 Middleware

middleware 的设计有点特殊,是一个层层包裹的匿名函数,这其实是函数式编程中的 柯里化applyMiddleware 会对 logger 这个 middleware 进行层层调用,动态地对 storenext 参数赋值,这样设计的好处在于

  • 易串联,柯里化函数具有延迟执行的特性,通过不断柯里化形成的 middleware 可以累积参数,配合组合 compose 的方式,很容易形成 pipeline 来处理数据流
  • 共享 store,在 applyMiddleware 执行过程中,store 还是旧的,但是因为闭包的存在,在 applyMiddleware 完成后,所有的 middlewares 内部拿到的 store 是最新且相同的

另外,我们可以发现 applyMiddleware 的结构也是一个多层柯里化的函数,借助 composeapplyMiddleware 可以用来和其他插件一起加强 createStore 函数

1
2
3
4
5
6
7
8
9
10
import { createStore, applyMiddleware, compose } from 'redux'
import rootReducer from '../reducers'
import DevTools from '../containers/DevTools'

const finalCreateStore = compose(
// Middleware you want to use in development:
applyMiddleware(d1, d2, d3),
// Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument()
)(createStore)

给 Middleware 分发 Store

创建一个普通的 store 通过如下方式

1
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null)

上面代码执行完后,applyMiddleware 函数陆续获得了三个参数,第一个是我们的 middlewares 数组,[mid1, mid2, mid3, ...],第二个 nextRedux 原生的 createStore,最后一个是 reducer

我们从对比图中可以看到,applyMiddleware 利用 createStorereducer 创建了一个 store,然后 storegetState 方法和 dispatch 方法又分别被直接和间接地赋值给 middlewareAPI 变量,middlewareAPI 就是对比图中红色箭头所指向的函数的入参 store

1
2
3
4
5
6
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}

chain = middlewares.map(middleware => middleware(middlewareAPI))

map 方法让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍,即执行红色箭头指向的函数(简单来说就是使用 middlewareAPI 作为参数将 middleware 包装一层),在执行完后,获得 chain 数组 [f1, f2, ... , fx, ...,fn],它保存的对象是图中绿色箭头指向的匿名函数(中间件序列),因为闭包,所以每个匿名函数都可以访问相同的 store,即 middlewareAPI

但是这里存在一个问题,middlewareAPI 中的 dispatch 为什么要用匿名函数包裹呢?

我们用 applyMiddleware 是为了改造 dispatch 的,所以 applyMiddleware 执行完后,dispatch 是变化了的,而 middlewareAPIapplyMiddleware 执行中分发到各个 middleware,所以必须用匿名函数包裹 dispatch(闭包机制),这样只要 dispatch 更新了,middlewareAPI 中的 dispatch 应用也会发生变化

组合串联 Middlewares

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

composechain 中的所有匿名函数 [f1, f2, ... fx, ... fn] 组装成一个新的函数,即新的 dispatch,当新 dispatch 执行时,[f1, f2, ... fx, ... fn] 从右到左依次执行(注意这里使用的是 reduceRight,所以顺序是从右到左,与数组的 reduce 区分开来)

Reduxcompose 的实现是下面这样的

1
2
3
function compose(...funcs) {
return arg => funcs.reduceRight((composed, f) => f(composed), arg)
}

compose(...chain) 返回的是一个匿名函数,函数里的 funcs 就是 chain 数组,当调用 reduceRight 时,依次从数组的右端取一个函数 fx 拿来执行,fx 函数的参数 composed 就是前一次 fx + 1 执行的结果,而第一次执行的 fnn 代表 chain 的长度)的参数 arg 就是 store.dispatch,所以当 compose 执行完后,我们得到的 dispatch 是这样的

1
dispatch = f1(f2(f3(store.dispatch)))

这个时候再调用新的 dispatch,每个 middleware 的代码就会依次执行

在 Middleware 中调用 Dispatch 会发生什么?

经过 compose 之后,所有的 middleware 算是串联起来了,可是还有一个问题,在之前我们提到过每个 middleware 都可以访问 store,即 middlewareAPI 这个变量,所以就可以拿到 storedispatch 方法,那么在 middleware 中调用 store.dispatch() 会发生什么,和调用 next() 有区别吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
const logger = store => next => action => {
console.log('dispatch', action)
next(action) // <==
console.log('finish', action)
}

// 对比

const logger = store => next => action => {
console.log('dispatch', action)
store.dispatch(action) // <==
console.log('finish', action)
}

在之前我们已经介绍过,通过匿名函数的方式 middleware 中拿到的 dispatch 和最终 compose 结束后的新 dispatch 是保持一致的,所以在 middleware 中调用 store.dispatch() 和在其他任何地方调用效果是一样的,而在 middleware 中调用 next() 的效果则是进入下一个 middleware,如下图所示

在正常情况下(图左),当我们 dispatch 一个 action 时,middleware 通过 next(action) 一层一层处理和传递 action 直到 Redux 原生的 dispatch,如果某个 middleware 使用 store.dispatch(action) 来分发 action,就发生了右图的情况,相当于从外层重新再来一遍

在 Middleware 中调用 Dispatch 的应用场景

我们知道,如果在中间件当中不调用 next 的话,中间件就不会串起来执行的,不过有些特殊的 action,比如异步请求的 action,它们的目的地并不是原生的 dispatch,而是对异步请求的 action 进行拦截,在请求完数据后利用新的 dispatch 发送更新 UIaction,这个 action 就可以把所有的中间价走一遍了

我们可以来模拟一个异步请求到服务器获取数据,成功后弹出一个自定义的 Message 的中间件,我们使用 redux-thunk 这个中间件

1
2
3
4
const thunk = store => next => action =>
typeof action === 'function'
? action(store.dispatch, store.getState)
: next(action)

没有看错,整个源码的核心只有这一点点,redux-thunk 做的事情就是判断 action 类型是否是函数,若是则执行 action,若不是则继续传递 action 到下个 middleware,我们首先来设计一个请求的 action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const getThenShow = (dispatch, getState) => {
const url = 'http://xxx.json'
fetch(url)
.then(response => {
dispatch({
type: 'SHOW_MESSAGE_FOR_ME',
message: response.json(),
})
}, e => {
dispatch({
type: 'FETCH_DATA_FAIL',
message: e,
})
})
}

这个时候只要在业务代码里面调用 store.dispatch(getThenShow)redux-thunk 就会拦截并执行 getThenShow 这个 actiongetThenShow 会先请求数据,如果成功则会 dispatch 一个显示 Messageaction,否则 dispatch 一个请求失败的 action,这里的 dispatch 就是通过 redux-thunk 这个 middleware 传递进来的

我们可以简单的总结一下,在 middleware 中使用 dispatch 的场景一般是,接受到一个定向 action,这个 action 并不希望到达原生的 dsipatch,存在的目的是为了触发其他新的 action,往往用在异步请求的需求当中

自定义的 Middlewares 该怎么写?

实现很简单,我们先来定义一个 callTraceMiddleware 的中间件用来追踪函数的调用过程

1
2
3
4
5
6
7
// callTraceMiddleware.js
export default function callTraceMiddleware({ dispatch, getState }) {
return next => action => {
console.trace()
return next(action)
}
}

然后在调用中间件部分添加中间件

1
2
3
4
5
const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware,
loggerMiddleware,
callTraceMiddleware
)(createStore)

这样我们运行在浏览器窗口就可以看到打印的函数调用轨迹

总结

applyMiddleware 机制的核心在于组合 compose,将不同的 middlewares 一层一层包裹到原生的 dispatch 之上,而为了方便进行 compose,需对 middleware 的设计采用柯里化的方式,达到动态产生 next 方法以及保持 store 的一致性

由于在 middleware 中可以像在外部一样轻松访问到 store,因此可以利用当前 storestate 来进行条件判断,用 dispatch 方法拦截老的 action 或发送新的 action

参考

# React

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×