• Middleware
    • 理解 Middleware
      • 问题: 记录日志
      • 尝试 #1: 手动记录
        • 注意
    • 尝试 #2: 封装 Dispatch
    • 尝试 #3: Monkeypatching Dispatch
    • 问题: 崩溃报告
    • 尝试 #4: 隐藏 Monkeypatching
    • 尝试 #5: 移除 Monkeypatching
    • 尝试 #6: “单纯”地使用 Middleware
    • 最终的方法
  • 7个示例

    Middleware

    我们已经在异步 Action 一节的示例中看到了一些 middleware 的使用。如果你使用过 Express 或者 Koa 等服务端框架, 那么应该对 middleware 的概念不会陌生。 在这类框架中,middleware 是指可以被嵌入在框架接收请求到产生响应过程之中的代码。例如,Express 或者 Koa 的 middleware 可以完成添加 CORS headers、记录日志、内容压缩等工作。middleware 最优秀的特性就是可以被链式组合。你可以在一个项目中使用多个独立的第三方 middleware。

    相对于 Express 或者 Koa 的 middleware,Redux middleware 被用于解决不同的问题,但其中的概念是类似的。它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

    这个章节分为两个部分,前面是帮助你理解相关概念的深度介绍,而后半部分则通过一些实例来体现 middleware 的强大能力。对文章前后内容进行结合通读,会帮助你更好的理解枯燥的概念,并从中获得启发。

    理解 Middleware

    正因为 middleware 可以完成包括异步 API 调用在内的各种事情,了解它的演化过程是一件相当重要的事。我们将以记录日志和创建崩溃报告为例,引导你体会从分析问题到通过构建 middleware 解决问题的思维过程。

    问题: 记录日志

    使用 Redux 的一个益处就是它让 state 的变化过程变的可预知和透明。每当一个 action 发起完成后,新的 state 就会被计算并保存下来。State 不能被自身修改,只能由特定的 action 引起变化。

    试想一下,当我们的应用中每一个 action 被发起以及每次新的 state 被计算完成时都将它们记录下来,岂不是很好?当程序出现问题时,我们可以通过查阅日志找出是哪个 action 导致了 state 不正确。

    Middleware - 图1

    我们如何通过 Redux 实现它呢?

    尝试 #1: 手动记录

    最直接的解决方案就是在每次调用 store.dispatch(action) 前后手动记录被发起的 action 和新的 state。这称不上一个真正的解决方案,仅仅是我们理解这个问题的第一步。

    注意

    如果你使用 react-redux 或者类似的绑定库,最好不要直接在你的组件中操作 store 的实例。在接下来的内容中,仅仅是假设你会通过 store 显式地向下传递。

    假设,你在创建一个 Todo 时这样调用:

    1. store.dispatch(addTodo('Use Redux'))

    为了记录这个 action 以及产生的新的 state,你可以通过这种方式记录日志:

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

    虽然这样做达到了想要的效果,但是你并不想每次都这么干。

    尝试 #2: 封装 Dispatch

    你可以将上面的操作抽取成一个函数:

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

    然后用它替换 store.dispatch():

    1. dispatchAndLog(store, addTodo('Use Redux'))

    你可以选择到此为止,但是每次都要导入一个外部方法总归还是不太方便。

    尝试 #3: Monkeypatching Dispatch

    如果我们直接替换 store 实例中的 dispatch 函数会怎么样呢?Redux store 只是一个包含一些方法的普通对象,同时我们使用的是 JavaScript,因此我们可以这样实现 dispatch 的 monkeypatch:

    1. let 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. }

    这离我们想要的已经非常接近了!无论我们在哪里发起 action,保证都会被记录。Monkeypatching 令人感觉还是不太舒服,不过利用它我们做到了我们想要的。

    问题: 崩溃报告

    如果我们想对 dispatch 附加超过一个的变换,又会怎么样呢?

    我脑海中出现的另一个常用的变换就是在生产过程中报告 JavaScript 的错误。全局的 window.onerror 并不可靠,因为它在一些旧的浏览器中无法提供错误堆栈,而这是排查错误所需的至关重要信息。

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

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

    如果按照我们的想法,日志记录和崩溃报告属于不同的模块,他们看起来应该像这样:

    1. function patchStoreToAddLogging(store) {
    2. let 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. let 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. }

    如果这些功能以不同的模块发布,我们可以在 store 中像这样使用它们:

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

    尽管如此,这种方式看起来还是不是够令人满意。

    尝试 #4: 隐藏 Monkeypatching

    Monkeypatching 本质上是一种 hack。“将任意的方法替换成你想要的”,此时的 API 会是什么样的呢?现在,让我们来看看这种替换的本质。 在之前,我们用自己的函数替换掉了 store.dispatch。如果我们不这样做,而是在函数中返回新的 dispatch 呢?

    1. function logger(store) {
    2. let 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 =>
    6. store.dispatch = middleware(store)
    7. )
    8. }

    然后像这样应用多个 middleware:

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

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

    尝试 #5: 移除 Monkeypatching

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

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

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

    如果 applyMiddlewareByMonkeypatching 方法中没有在第一个 middleware 执行时立即替换掉 store.dispatch,那么 store.dispatch 将会一直指向原始的 dispatch 方法。也就是说,第二个 middleware 依旧会作用在原始的 dispatch 方法。

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

    1. function logger(store) {
    2. return function wrapDispatchToAddLogging(next) {
    3. return 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. }

    现在是“我们该更进一步”的时刻了,所以可能会多花一点时间来让它变的更为合理一些。这些串联函数很吓人。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 中被使用。

    尝试 #6: “单纯”地使用 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 =>
    8. dispatch = middleware(store)(dispatch)
    9. )
    10. return Object.assign({}, store, { dispatch })
    11. }

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

    • 它只暴露一个 store API 的子集给 middleware:dispatch(action)getState()

    • 它用了一个非常巧妙的方式,以确保如果你在 middleware 中调用的是 store.dispatch(action) 而不是 next(action),那么这个操作会再次遍历包含当前 middleware 在内的整个 middleware 链。这对异步的 middleware 非常有用,正如我们在之前的章节中提到的。

    • 为了保证你只能应用 middleware 一次,它作用在 createStore() 上而不是 store 本身。因此它的签名不是 (store, middlewares) => store, 而是 (...middlewares) => (createStore) => createStore

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

    最终的方法

    这是我们刚刚所写的 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. let todoApp = combineReducers(reducers)
    3. let 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'))

    7个示例

    如果读完上面的章节你已经觉得头都要爆了,那就想象一下把它写出来之后的样子。下面的内容会让我们放松一下,并让你的思路延续。

    下面的每个函数都是一个有效的 Redux middleware。它们不是同样有用,但是至少他们一样有趣。

    1. /**
    2. * 记录所有被发起的 action 以及产生的新的 state。
    3. */
    4. const logger = store => next => action => {
    5. console.group(action.type)
    6. console.info('dispatching', action)
    7. let result = next(action)
    8. console.log('next state', store.getState())
    9. console.groupEnd(action.type)
    10. return result
    11. }
    12. /**
    13. * 在 state 更新完成和 listener 被通知之后发送崩溃报告。
    14. */
    15. const crashReporter = store => next => action => {
    16. try {
    17. return next(action)
    18. } catch (err) {
    19. console.error('Caught an exception!', err)
    20. Raven.captureException(err, {
    21. extra: {
    22. action,
    23. state: store.getState()
    24. }
    25. })
    26. throw err
    27. }
    28. }
    29. /**
    30. * 用 { meta: { delay: N } } 来让 action 延迟 N 毫秒。
    31. * 在这个案例中,让 `dispatch` 返回一个取消 timeout 的函数。
    32. */
    33. const timeoutScheduler = store => next => action => {
    34. if (!action.meta || !action.meta.delay) {
    35. return next(action)
    36. }
    37. let timeoutId = setTimeout(
    38. () => next(action),
    39. action.meta.delay
    40. )
    41. return function cancel() {
    42. clearTimeout(timeoutId)
    43. }
    44. }
    45. /**
    46. * 通过 { meta: { raf: true } } 让 action 在一个 rAF 循环帧中被发起。
    47. * 在这个案例中,让 `dispatch` 返回一个从队列中移除该 action 的函数。
    48. */
    49. const rafScheduler = store => next => {
    50. let queuedActions = []
    51. let frame = null
    52. function loop() {
    53. frame = null
    54. try {
    55. if (queuedActions.length) {
    56. next(queuedActions.shift())
    57. }
    58. } finally {
    59. maybeRaf()
    60. }
    61. }
    62. function maybeRaf() {
    63. if (queuedActions.length && !frame) {
    64. frame = requestAnimationFrame(loop)
    65. }
    66. }
    67. return action => {
    68. if (!action.meta || !action.meta.raf) {
    69. return next(action)
    70. }
    71. queuedActions.push(action)
    72. maybeRaf()
    73. return function cancel() {
    74. queuedActions = queuedActions.filter(a => a !== action)
    75. }
    76. }
    77. }
    78. /**
    79. * 使你除了 action 之外还可以发起 promise。
    80. * 如果这个 promise 被 resolved,他的结果将被作为 action 发起。
    81. * 这个 promise 会被 `dispatch` 返回,因此调用者可以处理 rejection。
    82. */
    83. const vanillaPromise = store => next => action => {
    84. if (typeof action.then !== 'function') {
    85. return next(action)
    86. }
    87. return Promise.resolve(action).then(store.dispatch)
    88. }
    89. /**
    90. * 让你可以发起带有一个 { promise } 属性的特殊 action。
    91. *
    92. * 这个 middleware 会在开始时发起一个 action,并在这个 `promise` resolve 时发起另一个成功(或失败)的 action。
    93. *
    94. * 为了方便起见,`dispatch` 会返回这个 promise 让调用者可以等待。
    95. */
    96. const readyStatePromise = store => next => action => {
    97. if (!action.promise) {
    98. return next(action)
    99. }
    100. function makeAction(ready, data) {
    101. let newAction = Object.assign({}, action, { ready }, data)
    102. delete newAction.promise
    103. return newAction
    104. }
    105. next(makeAction(false))
    106. return action.promise.then(
    107. result => next(makeAction(true, { result })),
    108. error => next(makeAction(true, { error }))
    109. )
    110. }
    111. /**
    112. * 让你可以发起一个函数来替代 action。
    113. * 这个函数接收 `dispatch` 和 `getState` 作为参数。
    114. *
    115. * 对于(根据 `getState()` 的情况)提前退出,或者异步控制流( `dispatch()` 一些其他东西)来说,这非常有用。
    116. *
    117. * `dispatch` 会返回被发起函数的返回值。
    118. */
    119. const thunk = store => next => action =>
    120. typeof action === 'function' ?
    121. action(store.dispatch, store.getState) :
    122. next(action)
    123. // 你可以使用以上全部的 middleware!(当然,这不意味着你必须全都使用。)
    124. let todoApp = combineReducers(reducers)
    125. let store = createStore(
    126. todoApp,
    127. applyMiddleware(
    128. rafScheduler,
    129. timeoutScheduler,
    130. thunk,
    131. vanillaPromise,
    132. readyStatePromise,
    133. logger,
    134. crashReporter
    135. )
    136. )