Redux Middleware
我们已经在异步 Action 一节的示例中看到了一些 middleware 的使用。如果你使用过 或者 等服务端框架, 那么应该对 middleware 的概念不会陌生。 类框架中,middleware 是指可以被嵌入在框架接收请求到产生响应过程之中的。例如,Express 或者 Koa 的 middleware 可以完成 CORS headers、记录日志、压缩等工作。middleware 最优秀的特性就是可以被链式组合。你可以在项目中使用多个独立的第三方 middleware。
相对于 Express 或者 Koa 的 middleware,R middleware 被用于不同的问题,但其中的概念是类似的。它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 你可以利用 R middleware 来进行日志记录、创建崩溃报告、异步接口或者路由等等。
这个章节分为两个部分,前面是帮助你理解相关概念的深度介绍,而后半部分则通过一些实例来体现 middleware 的强大能力。对前后进行结合通读,会帮助你更好的理解枯燥的概念,并从中获得启发。
理解 Middleware
正因为 middleware 可以完成异步 API 在内的各种事情,了解它的演化过程是一件相当重要的事。我们将以记录日志和创建崩溃报告为例,引导你体会从分析问题到通过构建 middleware 问题的思维过程。
使用 R 的益处就是它让 state 的变化过程变的可预知和透明。每当 action 发起完成后,新的 state 就会被计算并保存下来。State 不能被自身,只能由特定的 action 引起变化。
试想一下,当我们的应用中每 action 被发起以及每次新的 state 被计算完成时都将它们记录下来,岂不是很好?当程序出现问题时,我们可以通过查阅日志找出是哪个 action 导致了 state 不正确。
我们如何通过 R 实现它呢?
最直接的案就是在每次 store.dispatch(action) 前后手动记录被发起的 action 和新的 state。这称不上真正的案,仅仅是我们理解这个问题的第一步。
如果你使用 或者类似的绑定库,最好不要直接在你的组件中操作 store 的实例。在接下来的中,仅仅是假设你会通过 store 显式地向下传递。
假设,你在创建 Todo 时这样:
store.dispatch(addTodo('Use R'))
为了记录这个 action 以及产生的新的 state,你可以通过这种方式记录日志:
let action = addTodo('Use R') console.log('dispatching', action) store.dispatch(action) console.log('next state', store.getState())
虽然这样做达到了想要的,但是你并不想每次都这么干。
你可以将上面的操作抽取成:
function dispatchAndLog(store, action) { console.log('dispatching', action) store.dispatch(action) console.log('next state', store.getState()) }
然后用它替换 store.dispatch():
dispatchAndLog(store, addTodo('Use R'))
你可以选择到此为止,但是每次都要导入外部总归还是不太方便。
如果我们直接替换 store 实例中的 dispatch 会怎么样呢?R store 只是包含一些的普通对象,同时我们使用的是 JavaScript,因此我们可以这样实现 dispatch 的 monkeypatch:
let next = store.dispatch store.dispatch = function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result }
这离我们想要的已经非常接近了!无论我们在哪里发起 action,保证都会被记录。Monkeypatching 令人感觉还是不太舒服,不过利用它我们做到了我们想要的。
如果我们想对 dispatch 附加超过的变换,又会怎么样呢?
我脑海中出现的另常用的变换就是在生产过程中报告 JavaScript 的。全局的 window.onerror 并不可靠,因为它在一些旧的浏览器中无法提供堆栈,而这是排查所需的至关重要信息。
试想当发起 action 的结果是异常时,我们将包含堆栈,引起的 action 以及当前的 state 等信息通通发到类似于 这样的报告服务中,不是很好吗?这样我们可以更容易地在开发环境中重现这个。
然而,将日志记录和崩溃报告分离是很重要的。理想情况下,我们希望他们是两个不同的模块,也可能在不同的包中。否则我们无法构建由这些工具组成的生态系统。(:我们正在慢慢了解 middleware 的本质到底是什么!)
如果按照我们的想法,日志记录和崩溃报告属于不同的模块,他们看起来应该像这样:
function patchStoreToAddLogging(store) { let next = store.dispatch store.dispatch = function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } } function patchStoreToAddCrashReporting(store) { let next = store.dispatch store.dispatch = function dispatchAndReportErrors(action) { try { return next(action) } catch (err) { console.error('捕获异常!', err) Raven.captureException(err, { extra: { action, state: store.getState() } }) throw err } } }
如果这些以不同的模块发布,我们可以在 store 中像这样使用它们:
patchStoreToAddLogging(store) patchStoreToAddCrashReporting(store)
尽管如此,这种方式看起来还是不是够令人满意。
Monkeypatching 本质上是一种 hack。“将任意的替换成你想要的”,此时的 API 会是什么样的呢?现在,让我们来看看这种替换的本质。 在之前,我们用自己的掉了 store.dispatch。如果我们不这样做,而是在中返回新的 dispatch 呢?
function logger(store) { let next = store.dispatch // 我们之前的做法: // store.dispatch = function dispatchAndLog(action) { return function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } }
我们可以在 R 内部提供可以将实际的 monkeypatching 应用到 store.dispatch 中的辅助:
function applyMiddlewareByMonkeypatching(store, middlewares) { middlewares = middlewares.slice() middlewares.reverse() // 在每 middleware 中变换 dispatch 。 middlewares.forEach(middleware => store.dispatch = middleware(store) ) }
然后像这样应用多个 middleware:
applyMiddlewareByMonkeypatching(store, [ logger, crashReporter ])
尽管我们做了很多,实现方式依旧是 monkeypatching。
因为我们仅仅是将它隐藏在我们的框架内部,并没有改变这个事实。
为什么我们要替换原来的 dispatch 呢?当然,这样我们就可以在后面直接它,但是还有另原因:就是每 middleware 都可以操作(或者直接)前 middleware 包装过的 store.dispatch:
function logger(store) { // 这里的 next 必须指向前 middleware 返回的: let next = store.dispatch return function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } }
将 middleware 串连起来的必要性是显而易见的。
如果 applyMiddlewareByMonkeypatching 中没有在第 middleware 执行时立即替换掉 store.dispatch,那么 store.dispatch 将会一直指向原始的 dispatch 。也就是说,第二个 middleware 依旧会作用在原始的 dispatch 。
但是,还有另一种方式来实现这种链式的。可以让 middleware 以参数的形式接收 next() ,而不是通过 store 的实例去。
function logger(store) { return function wrapDispatchToAddLogging(next) { return function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } } }
现在是的时刻了,所以可能会多花一点时间来让它变的更为合理一些。这些串联很吓人。ES6 的箭头可以使其 ,从而看起来更舒服一些:
const logger = store => next => action => { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } const crashReporter = store => next => action => { try { return next(action) } catch (err) { console.error('Caught an exception!', err) Raven.captureException(err, { extra: { action, state: store.getState() } }) throw err } }
这正是 R middleware 的样子。
Middleware 接收了 next() 的 dispatch ,并返回 dispatch ,返回的会被作为下 middleware 的 next(),以此类推。由于 store 中类似 getState() 的依旧非常有用,我们将 store 作为顶层的参数,使得它可以在所有 middleware 中被使用。
我们可以写 applyMiddleware() 替换掉原来的 applyMiddlewareByMonkeypatching()。在新的 applyMiddleware() 中,我们取得最终完整的被包装过的 dispatch() ,并返回 store 的副本:
// 警告:这只是一种“单纯”的实现方式! // 这 *并不是* R 的 API. function applyMiddleware(store, middlewares) { middlewares = middlewares.slice() middlewares.reverse() let dispatch = store.dispatch middlewares.forEach(middleware => dispatch = middleware(store)(dispatch) ) return Object.assign({}, store, { dispatch }) }
这与 R 中 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:
const logger = store => next => action => { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } const crashReporter = store => next => action => { try { return next(action) } catch (err) { console.error('Caught an exception!', err) Raven.captureException(err, { extra: { action, state: store.getState() } }) throw err } }
然后是将它们引用到 R store 中:
import { createStore, combineReducers, applyMiddleware } from 'r' let todoApp = combineReducers(reducers) let store = createStore( todoApp, // applyMiddleware() 告诉 createStore() 如何处理中间件 applyMiddleware(logger, crashReporter) )
就是这样!现在任何被发送到 store 的 action 都会经过 logger 和 crashReporter:
// 将经过 logger 和 crashReporter 两个 middleware! store.dispatch(addTodo('Use R'))
7个示例
如果读完上面的章节你已经觉得头都要爆了,那就想象一下把它写出来之后的样子。下面的会让我们放松一下,并让你的思路延续。
下面的每个都是有效的 R middleware。它们不是同样有用,但是至少他们一样有趣。
/** * 记录所有被发起的 action 以及产生的新的 state。 */ const logger = store => next => action => { console.group(action.type) console.info('dispatching', action) let result = next(action) console.log('next state', store.getState()) console.groupEnd(action.type) return result } /** * 在 state 更新完成和 listener 被之后发送崩溃报告。 */ const crashReporter = store => next => action => { try { return next(action) } catch (err) { console.error('Caught an exception!', err) Raven.captureException(err, { extra: { action, state: store.getState() } }) throw err } } /** * 用 { : { delay: N } } 来让 action 延迟 N 毫秒。 * 个案例中,让 `dispatch` 返回取消 timeout 的。 */ const timeoutScheduler = store => next => action => { if (!action. || !action..delay) { return next(action) } let timeoutId = setTimeout( () => next(action), action..delay ) return function cancel() { clearTimeout(timeoutId) } } /** * 通过 { : { raf: true } } 让 action 在 rAF 循环帧中被发起。 * 个案例中,让 `dispatch` 返回从队列中移除该 action 的。 */ const rafScheduler = store => next => { let queuedActions = [] let frame = null function loop() { frame = null try { if (queuedActions.length) { next(queuedActions.shift()) } } finally { maybeRaf() } } function maybeRaf() { if (queuedActions.length && !frame) { frame = requestAnimationFrame(loop) } } return action => { if (!action. || !action..raf) { return next(action) } queuedActions.push(action) maybeRaf() return function cancel() { queuedActions = queuedActions.filter(a => a !== action) } } } /** * 使你除了 action 之外还可以发起 promise。 * 如果这个 promise 被 resolved,他的结果将被作为 action 发起。 * 这个 promise 会被 `dispatch` 返回,因此者可以处理 rejection。 */ const vanillaPromise = store => next => action => { if (typeof action.then !== 'function') { return next(action) } return Promise.resolve(action).then(store.dispatch) } /** * 让你可以发起带有 { promise } 的特殊 action。 * * 这个 middleware 会在开始时发起 action,并个 `promise` resolve 时发起另成功(或失败)的 action。 * * 为了方便起见,`dispatch` 会返回这个 promise 让者可以等待。 */ const readyStatePromise = store => next => action => { if (!action.promise) { return next(action) } function makeAction(ready, data) { let newAction = Object.assign({}, action, { ready }, data) delete newAction.promise return newAction } next(makeAction(false)) return action.promise.then( result => next(makeAction(true, { result })), error => next(makeAction(true, { error })) ) } /** * 让你可以发起来替代 action。 * 这个接收 `dispatch` 和 `getState` 作为参数。 * * 对于(根据 `getState()` 的情况)提前,或者异步控制流( `dispatch()` 一些其他东西)来说,这非常有用。 * * `dispatch` 会返回被发起的返回值。 */ const thunk = store => next => action => typeof action === 'function' ? action(store.dispatch, store.getState) : next(action) // 你可以使用以上全部的 middleware!(当然,这不意味着你必须全都使用。) let todoApp = combineReducers(reducers) let store = createStore( todoApp, applyMiddleware( rafScheduler, timeoutScheduler, thunk, vanillaPromise, readyStatePromise, logger, crashReporter ) )