在express和koa框架中,中间件是指 可以被嵌入在框架接收请求到产生相应过程之中的代码 ,例如,express和koa的中间件可以完成添加cors头,记录日志和内容压缩等工作。

中间件最优秀的就是可以被链式组合。可以在一个项目中多次使用多个独立的第三方中间件。

redux的中间件提供 action出发后,reducer之前 的扩展点。可以用来

  • 记录日志
  • 创建崩溃报告
  • 调用异步接口
  • 调用路由

现在用日志记录来理解中间件

问题:记录日志

最简单的日志

记录日志可以使用 console.log

  1. const action = addTodo('Use Redux')
  2. console.log('dispatching', action)
  3. store.dispatch(action)
  4. console.log('next state', store.getState())

使用 console.log 非常简单,但是非常不方便。每次调用action都需要执行打印语句。因此可以将面的 disaptch 过程封装到一个函数里面。

封装输出日志的过程到函数

将以上日志输入封装如下:

  1. function dispatchAndLog(store, action) {
  2. console.log('dispatching', action)
  3. store.dispatch(action)
  4. console.log('next state', store.getState())
  5. }

然后用这个 dispatch 替换 store 原来的 dispatch :

  1. // 原生redux使用store.dispatch()
  2. dispatchAndLog(store, addTodo('Use Redux'))

封装 store.dispatch 后,每次调用都需要导入函数,依然麻烦

Monkeypatching Dispatch

redux的store只是一个包含方法的普通对象,因此我们可以直接替换 sotre 中的 dispatch 方法。这样我们就不用每次都导入封装后的 dispatch 函数了

  1. const next = store.dispatch
  2. store.dispatch = function dispatchAndLog(action) {
  3. console.log('dispatching', action)
  4. let result = next(action)
  5. console.log('next state', store.getState())
  6. return result
  7. }

image.png

问题:崩溃报告

上面的例子中,我们只想 store.dispatch 中增加了日志功能,如果还要扩展其功能呢?

试想当发起一个 action 的结果是一个异常时,我们将包含调用堆栈,引起错误的 action 以及当前的 state 等错误信息通通发到类似于 Sentry 这样的报告服务中,不是很好吗?这样我们可以更容易地在开发环境中重现这个错误。

然而,将日志记录和崩溃报告分离是很重要的。理想情况下,我们希望他们是两个不同的模块,也可能在不同的包中。否则我们无法构建一个由这些工具组成的生态系统。(提示:我们正在慢慢了解 middleware 的本质到底是什么!)

如果将崩溃报告和日志分离,扩展结果会像下面这样:

  1. function patchStoreToAddLogging(store) {
  2. const next = store.dispatch
  3. store.dispatch = function dispatchAndLog(action) {
  4. console.log('dispatching', action)
  5. let result = next(action)
  6. console.log('next state', store.getState())
  7. return result
  8. }
  9. }
  10. function patchStoreToAddCrashReporting(store) {
  11. const next = store.dispatch
  12. store.dispatch = function dispatchAndReportErrors(action) {
  13. try {
  14. return next(action)
  15. } catch (err) {
  16. console.error('捕获一个异常!', err)
  17. Raven.captureException(err, {
  18. extra: {
  19. action,
  20. state: store.getState()
  21. }
  22. })
  23. throw err
  24. }
  25. }
  26. }

如果以上功能通过不同模块发布,使用将会是下面这样:

  1. patchStoreToAddLogging(store)
  2. patchStoreToAddCrashReporting(store)

然而在实际开发中,我们需要同时打印日志和崩溃报告,同时还要让两者分别属于两个模块。

隐藏monkyPatch

monkey本质上是一种hack,将任意的方法替换成目标方法,此时的api是什么样的呢?

在之前,我们用我们自己的函数替换掉了 store.dispatch ,如果不做替换,而是返回一个新的 dispatch 呢?(注意异步action创建函数)

  1. function logger(store) {
  2. const next = store.dispatch
  3. // 我们之前的做法:
  4. // store.dispatch = function dispatchAndLog(action) {
  5. return function dispatchAndLog(action) {
  6. console.log('dispatching', action)
  7. let result = next(action)
  8. console.log('next state', store.getState())
  9. return result
  10. }
  11. }

我们可以在 Redux 内部提供一个可以将实际的 monkeypatching 应用到 store.dispatch 中的辅助方法:

  1. function applyMiddlewareByMonkeypatching(store, middlewares) {
  2. middlewares = middlewares.slice()
  3. middlewares.reverse()
  4. // 在每一个 middleware 中变换 dispatch 方法。
  5. middlewares.forEach(middleware => (store.dispatch = middleware(store)))
  6. }

在这个变换方法中,每一个 middleWare 都会对store执行操作,可以理解为包装或者封装。每一次包装都会返回新的store。当所有middleWare都执行完成后,所有 middleWare 提供的功能都会集成到store中。

我们可以应用多个middleWare :

  1. applyMiddlewareByMonkeypatching(store, [logger, crashReporter])

尽管我们做了很多,实现方式依旧是 monkeypatching。
因为我们仅仅是将它隐藏在我们的框架内部,并没有改变这个事实。

移除monkeyPatch

为什么我们要替换原来的 dispatch 呢?当然,这样我们就可以在后面直接调用它,但是还有另一个原因:就是每一个 middleware 都可以操作(或者直接调用)前一个 middleware 包装过的 store.dispatch

  1. function logger(store) {
  2. // 这里的 next 必须指向前一个 middleware 返回的函数:
  3. const next = store.dispatch
  4. return function dispatchAndLog(action) {
  5. // 在执行前操作
  6. console.log('dispatching', action)
  7. let result = next(action)
  8. // 执行后操作
  9. console.log('next state', store.getState())
  10. return result
  11. }
  12. }

将 middleware 串连起来的必要性是显而易见的。

如果 applyMiddlewareByMonkeypatching 方法中没有在第一个 middleware 执行时立即替换掉 store.dispatch,那么 store.dispatch 将会一直指向原始的 dispatch 方法。也就是说,第二个 middleware 依旧会作用在原始的 dispatch 方法,而由于调用方式相同,第二个middleWare依然没有替换原始 dispatch 方法。这样下去,所有 middleware 都不会包装原市 dispatch 方法。

但是,还有另一种方式来实现这种链式调用的效果。可以让 middleware 以方法参数的形式接收一个 next() 方法,而不是通过 store 的实例去获取。

  1. // logger的目标是包装dispatch 并返回新的dispatch
  2. function logger(store) {
  3. // 上个例子中我们先获取dispatch,然后再包装
  4. // 这里wrap函数从参数中获取dispatch,从而代替了上面的const next = store.dispatch
  5. return function wrapDispatchToAddLogging(next) {
  6. return function dispatchAndLog(action) {
  7. console.log('dispatching', action)
  8. let result = next(action)
  9. console.log('next state', store.getState())
  10. return result
  11. }
  12. }
  13. }

对与以上过程,可以编写demo函数来理解:调用和输出如下: image.png

  1. // 定义
  2. function a(x) {
  3. console.log("a", x);
  4. return function b(y) {
  5. console.log("b", y);
  6. return function c(z) {
  7. console.log("c", z);
  8. }
  9. }
  10. }

现在是“我们该更进一步”的时刻了,所以可能会多花一点时间来让它变的更为合理一些。这些串联函数很吓人。ES6 的箭头函数可以使其 柯里化 ,从而看起来更舒服一些:

  1. const logger = store => next => action => {
  2. console.log('dispatching', action)
  3. let result = next(action)
  4. console.log('next state', store.getState())
  5. return result
  6. }
  7. const crashReporter = store => next => action => {
  8. try {
  9. return next(action)
  10. } catch (err) {
  11. console.error('Caught an exception!', err)
  12. Raven.captureException(err, {
  13. extra: {
  14. action,
  15. state: store.getState()
  16. }
  17. })
  18. throw err
  19. }
  20. }

这正是 Redux middleware 的样子。
**
Middleware 接收了一个 next() 的 dispatch 函数,并返回一个 dispatch 函数,返回的函数会被作为下一个 middleware 的 next(),以此类推。由于 store 中类似 getState() 的方法依旧非常有用,我们将 store 作为顶层的参数,使得它可以在所有 middleware 中被使用。

单纯”地使用 Middleware

我们可以写一个 applyMiddleware() 方法替换掉原来的 applyMiddlewareByMonkeypatching()。在新的 applyMiddleware() 中,我们取得最终完整的被包装过的 dispatch() 函数,并返回一个 store 的副本:

  1. // 警告:这只是一种“单纯”的实现方式!
  2. // 这 *并不是* Redux 的 API.
  3. function applyMiddleware(store, middlewares) {
  4. middlewares = middlewares.slice()
  5. middlewares.reverse()
  6. let dispatch = store.dispatch
  7. middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
  8. return Object.assign({}, store, { dispatch })
  9. }

这与 Redux 中 applyMiddleware() 的实现已经很接近了,但是有三个重要的不同之处

  • 它只暴露一个 store API 的子集给 middleware:dispatch(action)getState()
  • 它用了一个非常巧妙的方式,以确保如果你在 middleware 中调用的是 store.dispatch(action) 而不是 next(action),那么这个操作会再次遍历包含当前 middleware 在内的整个 middleware 链。这对异步的 middleware 非常有用,正如我们在之前的章节中提到的。在创建阶段调用 dispatch 时你需要特别注意,详见下方警告。
  • 为了保证你只能应用 middleware 一次,它作用在 createStore() 上而不是 store 本身。因此它的签名不是 (store, middlewares) => store, 而是 (...middlewares) => (createStore) => createStore

由于在使用之前需要先应用方法到 createStore() 之上有些麻烦,createStore() 也接受将希望被应用的函数作为最后一个可选参数传入。

由于在使用之前需要先应用方法到 createStore() 之上有些麻烦,createStore() 也接受将希望被应用的函数作为最后一个可选参数传入。

警告:在创建阶段 dispatch

执行 applyMiddleware 建立你的 middleware 时,store.dispatch 函数会指向 createStore 创建的原生版本。这时进行 dispatch 会导致没有任何 middleware 被应用。如果你准备在创建阶段与另一个 middleware 交互,你恐怕要失望了。由于这个行为出乎意料,如果你尝试在创建阶段结束前 dispatch 一个 action,applyMiddleware 会抛出一个错误。想要达到这个目的,你可以通过一个普通对象直接与其他 middleware 通信(例如对于一个负责 API 调用的 middleware,使用 API 客户端对象与之通信),或者使用回调函数等待 middleware 创建完毕。

最终方法

中间件:

  1. const logger = store => next => action => {
  2. console.log('dispatching', action)
  3. let result = next(action)
  4. console.log('next state', store.getState())
  5. return result
  6. }
  7. const crashReporter = store => next => action => {
  8. try {
  9. return next(action)
  10. } catch (err) {
  11. console.error('Caught an exception!', err)
  12. Raven.captureException(err, {
  13. extra: {
  14. action,
  15. state: store.getState()
  16. }
  17. })
  18. throw err
  19. }
  20. }

引入到 redux store 中:

  1. import { createStore, combineReducers, applyMiddleware } from 'redux'
  2. const todoApp = combineReducers(reducers)
  3. const store = createStore(
  4. todoApp,
  5. // applyMiddleware() 告诉 createStore() 如何处理中间件
  6. applyMiddleware(logger, crashReporter)
  7. )

就是这样!现在任何被发送到 store 的 action 都会经过 loggercrashReporter

  1. // 将经过 logger 和 crashReporter 两个 middleware!
  2. store.dispatch(addTodo('Use Redux'))

注意事项

在大概看了一边官方文档中的指导后,我以为我会写了,结果在写的时候还是犯了一些细节错误。

在写中间件的过程中,我们的最终目标是扩展 dispatch 的功能,而有一些细节问题会导致扩展失败。在以上步骤中可能是替换失败等。

  • 编码过程中,需要注意 return 的使用。如果没有 return,很可能导致失败。
  • 注意最单纯使用 middleware 中 applyMiddleware 方法,此方法返回新的 store, 因此在调用时必须将调用结果赋值给原来的 store 来替换原来的 store, 否则此函数相当于没有执行:
    1. let store = create(reducers);
    2. store = applyMiddleWare(store, [addLog, addCrast]);
    3. export default store;