中间件(Middleware)

Middleware

您已经在异步操作示例中看到了中间件的操作。如果您已经使用了像 Express 和 Koa 等服务器端库,那么您也可能已经熟悉了中间件的概念。在这些框架中,中间件是可以在接收请求的框架和生成响应的框架之间放置的一些代码。例如,Express 或 Koa 中间件可能会添加 CORS 标头,日志记录,压缩等等。中间件的最大特点是它可以组合在一个链中。您可以在一个项目中使用多个独立的第三方中间件

Redux 中间件解决了与 Express 或 Koa 中间件不同的问题,但概念上类似。它提供了一个第三方扩展点,用于分派一个动作,以及它到达reducer的那一刻。人们使用 Redux 中间件进行日志记录,崩溃报告,与异步 API 对话,路由等等。

本文分为深入介绍以帮助您理解概念,以及一些实际示例,以最终展示中间件的强大功能。您可能会发现在它们之间来回切换是有帮助的,因为您在无聊和灵感之间翻转。

了解中间件

尽管中间件可用于各种各样的事情,包括异步 API 调用,但了解其来源非常重要。我们将通过使用日志记录和崩溃报告作为示例,引导您完成导致中间件的思考过程。

问题:记录

Redux 的好处之一是它使状态变化可预测和透明。每次发送操作时,都会计算并保存新状态。不会随意改变,它只能触发不同的action改变。

如果我们记录了应用程序中发生的每个操作以及之后计算的状态,这不是很好吗?当出现问题时,我们可以回顾我们的日志,并找出哪一个行为影响了状态。

如何用 Redux 处理这个问题?

Attempt #1: Logging Manually

最基本的解决方案就是每次调用store.dispatch(action)时只记录一次动作和下一个状态。这不是一个真正的解决方案,而只是理解问题的第一步。

注意

假设你在创建待办事项时调用了这个:

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

要记录动作和状态,可以将其更改为如下所示:

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

这会产生所需的效果,但您不希望每次都这样做。

Attempt #2: Wrapping Dispatch

你可以提取日志到一个函数中:

function dispatchAndLog(store, action) { console.log('dispatching', action) store.dispatch(action) console.log('next state', store.getState()) }

您可以随处使用它,而不是store.dispatch()

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

我们可以在这里结束,但每次都导入一个特殊的函数并不是很方便。

Attempt #3: Monkeypatching Dispatch

如果我们只是替换dispatch存储实例上的功能呢?Redux 存储只是一个简单的对象,只有几个方法,而我们正在编写JavaScript,所以我们可以简单地实现这个dispatch实现:

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 }

这已经接近我们想要的了!无论我们在哪里发送动作,它都会保证被记录。Monkeypatching 从来没有感觉正确,但我们现在可以忍受这一点。

问题:崩溃报告

如果我们想要应用多个这样的转换到dispatch呢?

我想到的另一个有用的转换是报告生产中的 JavaScript 错误。全局window.onerror事件不可靠,因为它不提供某些较旧浏览器的堆栈信息,这对了解错误发生的原因至关重要。

如果由于调度某个动作而导致错误发生,我们会将它发送给崩溃报告服务,如 Sentry 的堆栈跟踪,导致错误的操作以及当前状态?这样在开发过程中重现错误就容易多了。

无论怎样,我们必须将日志记录和崩溃报告分开。理想情况下,我们希望他们成为不同的模块,可能在不同的包中。否则,我们不能拥有这样的实用程序的生态系统。(提示:我们正在慢慢接触什么是中间件!)

如果日志记录和崩溃报告是单独的实用程序,则它们可能如下所示:

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('Caught an exception!', err) Raven.captureException(err, { extra: { action, state: store.getState() } }) throw err } } }

如果这些函数作为单独的模块发布,我们可以稍后使用它们来修补我们的存储:

patchStoreToAddLogging(store) patchStoreToAddCrashReporting(store)

尽管如此,这并不好。

Attempt #4: Hiding Monkeypatching

Monkeypatching 是一个破解。“替换你喜欢的任何方法”,这是什么样的 API?让我们来弄清楚它的本质。以前,我们的职能被取代store.dispatch。如果他们返回新的dispatch功能呢?

function logger(store) { let next = store.dispatch // Previously: // 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 } }

我们可以在 Redux 中提供一个帮助器,将实际的 monkeypatching 作为实现细节应用:

function applyMiddlewareByMonkeypatching(store, middlewares) { middlewares = middlewares.slice() middlewares.reverse() // Transform dispatch function with each middleware. middlewares.forEach(middleware => store.dispatch = middleware(store) ) }

我们可以用它来应用这样的多个中间件:

applyMiddlewareByMonkeypatching(store, [logger, crashReporter])

然而,它仍然是 monkeypatching。

我们将其隐藏在库内并不会改变这一事实。

Attempt #5: Removing Monkeypatching

为什么我们甚至覆盖dispatch?当然,为了能够稍后调用它,但还有另一个原因:为了使每个中间件都可以访问(并调用)之前包装的store.dispatch

function logger(store) { // Must point to the function returned by the previous 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 } }

链接中间件至关重要!

如果在处理完第一个中间件后applyMiddlewareByMonkeypatching不立即分配store.dispatchstore.dispatch则会一直指向原来的dispatch函数。那么第二个中间件也将被绑定到原始dispatch函数。

但是还有一种不同的方式来启用链接。中间件可以接受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 } }

这正是 Redux 中间件的样子。

现在,中间件采用next()调度函数,并返回一个调度功能,该调度功能反过来用作next()左侧的中间件,依此类推。访问某些像getState()的存储方法仍然很有用,因此可以将store保持作为顶级参数。

Attempt #6: Naïvely Applying the Middleware

为了替换applyMiddlewareByMonkeypatching(),我们可以写出applyMiddleware()第一个获得最终的完全包装dispatch()函数,并使用它返回存储的副本:

// Warning: Naïve implementation! // That's *not* Redux 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 }) }

用 Redux 实施applyMiddleware()的传输是相似的,但在三个重要方面有所不同

  • 它只将存储 API 的一部分公开给中间件:dispatch(action)getState()

  • 请确保如果您调用从中间件中store.dispatch(action)而不是从中间件调用next(action),该操作实际上会遍历整个中间件链,包括当前的中间件,这确实会带来一些诡计。正如我们以前所见,这对于异步中间件非常有用。

  • 为确保您只能应用中间件一次,它可以运行createStore()而不是store自身运行。而不是(store, middlewares) => store,它的签名是(...middlewares) => (createStore) => createStore。

由于在使用createStore()函数之前应用该函数很麻烦,因此createStore()接受一个可选的最后一个参数来指定这些函数。

最后的方法

鉴于我们刚才写的这个中间件:

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 } }

以下是如何将其应用于 Redux 存储的方法:

import { createStore, combineReducers, applyMiddleware } from 'redux' let todoApp = combineReducers(reducers) let store = createStore( todoApp, // applyMiddleware() tells createStore() how to handle middleware applyMiddleware(logger, crashReporter) )

现在,分派给商店实例的任何操作都将流经loggercrashReporter

// Will flow through both logger and crashReporter middleware! store.dispatch(addTodo('Use Redux'))

七个例子

如果你的头脑从阅读上面的章节中解脱出来,想象一下写下它的感觉。本部分旨在为您和我放松身心,并有助于您转换注意力。

下面的每个功能都是有效的 Redux 中间件。它们不是同样有用,但至少它们同样有趣。

/** * Logs all actions and states after they are dispatched. */ 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 } /** * Sends crash reports as state is updated and listeners are notified. */ 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 } } /** * Schedules actions with { meta: { delay: N } } to be delayed by N milliseconds. * Makes `dispatch` return a function to cancel the timeout in this case. */ const timeoutScheduler = store => next => action => { if (!action.meta || !action.meta.delay) { return next(action) } let timeoutId = setTimeout( () => next(action), action.meta.delay ) return function cancel() { clearTimeout(timeoutId) } } /** * Schedules actions with { meta: { raf: true } } to be dispatched inside a rAF loop * frame. Makes `dispatch` return a function to remove the action from the queue in * this case. */ 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.meta || !action.meta.raf) { return next(action) } queuedActions.push(action) maybeRaf() return function cancel() { queuedActions = queuedActions.filter(a => a !== action) } } } /** * Lets you dispatch promises in addition to actions. * If the promise is resolved, its result will be dispatched as an action. * The promise is returned from `dispatch` so the caller may handle rejection. */ const vanillaPromise = store => next => action => { if (typeof action.then !== 'function') { return next(action) } return Promise.resolve(action).then(store.dispatch) } /** * Lets you dispatch special actions with a { promise } field. * * This middleware will turn them into a single action at the beginning, * and a single success (or failure) action when the `promise` resolves. * * For convenience, `dispatch` will return the promise so the caller can wait. */ 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 })) ) } /** * Lets you dispatch a function instead of an action. * This function will receive `dispatch` and `getState` as arguments. * * Useful for early exits (conditions over `getState()`), as well * as for async control flow (it can `dispatch()` something else). * * `dispatch` will return the return value of the dispatched function. */ const thunk = store => next => action => typeof action === 'function' ? action(store.dispatch, store.getState) : next(action) // You can use all of them! (It doesn't mean you should.) let todoApp = combineReducers(reducers) let store = createStore( todoApp, applyMiddleware( rafScheduler, timeoutScheduler, thunk, vanillaPromise, readyStatePromise, logger, crashReporter ) )