中间件
¥Middleware
你已经在 "Redux 基础知识" 教程 中看到了中间件的实际应用。如果你使用过 表达 和 相思木 这样的服务器端库,你可能也已经熟悉中间件的概念。在这些框架中,中间件是你可以在接收请求的框架和生成响应的框架之间放置的一些代码。例如,Express 或 Koa 中间件可能会添加 CORS 标头、日志记录、压缩等。中间件的最大特点是它可以在链中组合。你可以在单个项目中使用多个独立的第三方中间件。
¥You've seen middleware in action in the "Redux Fundamentals" tutorial. If you've used server-side libraries like Express and Koa, you were also probably already familiar with the concept of middleware. In these frameworks, middleware is some code you can put between the framework receiving a request, and the framework generating a response. For example, Express or Koa middleware may add CORS headers, logging, compression, and more. The best feature of middleware is that it's composable in a chain. You can use multiple independent third-party middleware in a single project.
Redux 中间件解决的问题与 Express 或 Koa 中间件不同,但概念上类似。它在调度操作和到达 reducer 之间提供了一个第三方扩展点。人们使用 Redux 中间件进行日志记录、崩溃报告、与异步 API 交互、路由等。
¥Redux middleware solves different problems than Express or Koa middleware, but in a conceptually similar way. It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more.
本文分为深入介绍和 一些实际例子,以帮助你理解这个概念,并在最后展示中间件的强大功能。当你在感到无聊和受到启发之间切换时,你可能会发现在它们之间来回切换很有帮助。
¥This article is divided into an in-depth intro to help you grok the concept, and a few practical examples to show the power of middleware at the very end. You may find it helpful to switch back and forth between them, as you flip between feeling bored and inspired.
了解中间件
¥Understanding Middleware
虽然中间件可用于多种用途,包括异步 API 调用,但了解它的来源非常重要。我们将使用日志记录和崩溃报告作为示例,引导你完成中间件的思考过程。
¥While middleware can be used for a variety of things, including asynchronous API calls, it's really important that you understand where it comes from. We'll guide you through the thought process leading to middleware, by using logging and crash reporting as examples.
问题:记录
¥Problem: Logging
Redux 的好处之一是它使状态更改可预测且透明。每次调度操作时,都会计算并保存新状态。状态本身不能改变,它只能作为特定行动的结果而改变。
¥One of the benefits of Redux is that it makes state changes predictable and transparent. Every time an action is dispatched, the new state is computed and saved. The state cannot change by itself, it can only change as a consequence of a specific action.
如果我们记录应用中发生的每个操作以及之后计算的状态,不是很好吗?当出现问题时,我们可以回顾日志,找出哪个操作破坏了状态。
¥Wouldn't it be nice if we logged every action that happens in the app, together with the state computed after it? When something goes wrong, we can look back at our log, and figure out which action corrupted the state.
我们如何使用 Redux 来解决这个问题?
¥How do we approach this with Redux?
尝试 #1:手动记录
¥Attempt #1: Logging Manually
最简单的解决方案是每次调用 store.dispatch(action)
时自己记录操作和下一个状态。这并不是真正的解决方案,而只是理解问题的第一步。
¥The most naïve solution is just to log the action and the next state yourself every time you call store.dispatch(action)
. It's not really a solution, but just a first step towards understanding the problem.
注意
¥Note
如果你使用 react-redux 或类似的绑定,你可能无法直接访问组件中的存储实例。对于接下来的几段,假设你显式地传递了存储。
¥If you're using react-redux or similar bindings, you likely won't have direct access to the store instance in your components. For the next few paragraphs, just assume you pass the store down explicitly.
假设你在创建待办事项时调用此方法:
¥Say, you call this when creating a todo:
store.dispatch(addTodo('Use Redux'))
要记录操作和状态,你可以将其更改为如下所示:
¥To log the action and state, you can change it to something like this:
const action = addTodo('Use Redux')
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
这会产生所需的效果,但你不想每次都这样做。
¥This produces the desired effect, but you wouldn't want to do it every time.
尝试 #2:封装分发
¥Attempt #2: Wrapping Dispatch
你可以将日志记录提取到函数中:
¥You can extract logging into a function:
function dispatchAndLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}
然后你可以在任何地方使用它而不是 store.dispatch()
:
¥You can then use it everywhere instead of store.dispatch()
:
dispatchAndLog(store, addTodo('Use Redux'))
我们可以到这里结束,但是每次都导入一个特殊的函数不是很方便。
¥We could end this here, but it's not very convenient to import a special function every time.
尝试 #3:猴子补丁调度
¥Attempt #3: Monkeypatching Dispatch
如果我们只替换存储实例上的 dispatch
函数会怎样?Redux 存储是一个带有 几个方法 的普通对象,我们正在编写 JavaScript,因此我们可以对 dispatch
实现进行猴子补丁:
¥What if we just replace the dispatch
function on the store instance? The Redux store is a plain object with a few methods, and we're writing JavaScript, so we can just monkeypatch the dispatch
implementation:
const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
这已经比较接近我们想要的了!无论我们在何处调度操作,都保证会记录该操作。猴子补丁从来都感觉不对劲,但我们现在可以忍受这一点。
¥This is already closer to what we want! No matter where we dispatch an action, it is guaranteed to be logged. Monkeypatching never feels right, but we can live with this for now.
问题:崩溃报告
¥Problem: Crash Reporting
如果我们想对 dispatch
应用多个这样的转换怎么办?
¥What if we want to apply more than one such transformation to dispatch
?
我想到的另一个有用的转换是报告生产中的 JavaScript 错误。全局 window.onerror
事件不可靠,因为它在某些较旧的浏览器中不提供堆栈信息,这对于理解错误发生的原因至关重要。
¥A different useful transformation that comes to my mind is reporting JavaScript errors in production. The global window.onerror
event is not reliable because it doesn't provide stack information in some older browsers, which is crucial to understand why an error is happening.
如果每当由于分派操作而引发错误时,我们都会将其发送到像 Sentry 这样的崩溃报告服务,并包含堆栈跟踪、导致错误的操作和当前状态,这不是很有用吗?这样在开发过程中重现错误就容易多了。
¥Wouldn't it be useful if, any time an error is thrown as a result of dispatching an action, we would send it to a crash reporting service like Sentry with the stack trace, the action that caused the error, and the current state? This way it's much easier to reproduce the error in development.
然而,重要的是我们将日志记录和崩溃报告分开。理想情况下,我们希望它们是不同的模块,可能位于不同的包中。否则我们就无法拥有此类实用程序的生态系统。(提示:我们正在慢慢了解中间件是什么!)
¥However, it is important that we keep logging and crash reporting separate. Ideally we want them to be different modules, potentially in different packages. Otherwise we can't have an ecosystem of such utilities. (Hint: we're slowly getting to what middleware is!)
如果日志记录和崩溃报告是单独的实用程序,它们可能如下所示:
¥If logging and crash reporting are separate utilities, they might look like this:
function patchStoreToAddLogging(store) {
const 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) {
const 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
}
}
}
如果这些函数作为单独的模块发布,我们稍后可以使用它们来修补我们的存储:
¥If these functions are published as separate modules, we can later use them to patch our store:
patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
不过,这并不好。
¥Still, this isn't nice.
尝试 #4:隐藏猴子补丁
¥Attempt #4: Hiding Monkeypatching
Monkeypatching 是一种 hack。“替换你喜欢的任何方法”,那是什么样的 API?让我们来弄清楚它的本质。之前,我们的函数替换了 store.dispatch
。如果他们返回新的 dispatch
函数怎么办?
¥Monkeypatching is a hack. “Replace any method you like”, what kind of API is that? Let's figure out the essence of it instead. Previously, our functions replaced store.dispatch
. What if they returned the new dispatch
function instead?
function logger(store) {
const 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 中提供一个辅助程序,它将应用实际的猴子补丁作为实现细节:
¥We could provide a helper inside Redux that would apply the actual monkeypatching as an implementation detail:
function applyMiddlewareByMonkeypatching(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
// Transform dispatch function with each middleware.
middlewares.forEach(middleware => (store.dispatch = middleware(store)))
}
我们可以使用它来应用多个中间件,如下所示:
¥We could use it to apply multiple middleware like this:
applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
然而,它仍然是猴子补丁。我们将其隐藏在库内这一事实并没有改变这一事实。
¥However, it is still monkeypatching. The fact that we hide it inside the library doesn't alter this fact.
尝试 #5:删除猴子补丁
¥Attempt #5: Removing Monkeypatching
为什么我们要覆盖 dispatch
?当然是为了以后能够调用,但还有一个原因:这样每个中间件都可以访问(并调用)之前封装的 store.dispatch
:
¥Why do we even overwrite dispatch
? Of course, to be able to call it later, but there's also another reason: so that every middleware can access (and call) the previously wrapped store.dispatch
:
function logger(store) {
// Must point to the function returned by the previous middleware:
const next = store.dispatch
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
它对于链接中间件至关重要!
¥It is essential to chaining middleware!
如果 applyMiddlewareByMonkeypatching
在处理完第一个中间件后没有立即分配 store.dispatch
,store.dispatch
将继续指向原来的 dispatch
函数。那么第二个中间件也会绑定原来的 dispatch
函数。
¥If applyMiddlewareByMonkeypatching
doesn't assign store.dispatch
immediately after processing the first middleware, store.dispatch
will keep pointing to the original dispatch
function. Then the second middleware will also be bound to the original dispatch
function.
但还有一种不同的方式来启用链接。中间件可以接受 next()
调度函数作为参数,而不是从 store
实例读取它。
¥But there's also a different way to enable chaining. The middleware could accept the next()
dispatch function as a parameter instead of reading it from the store
instance.
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
}
}
}
这是一个 “我们需要更深入” 时刻,所以可能需要一段时间才能明白这一点。函数级联感觉很吓人。箭头函数使 currying 更美观:
¥It's a “we need to go deeper” kind of moment, so it might take a while for this to make sense. The function cascade feels intimidating. Arrow functions make this currying easier on eyes:
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 中间件的样子。
¥This is exactly what Redux middleware looks like.
现在中间件接受 next()
调度函数,并返回一个调度函数,该函数又充当左侧中间件的 next()
,依此类推。访问某些存储方法(例如 getState()
)仍然很有用,因此 store
仍然可用作顶层参数。
¥Now middleware takes the next()
dispatch function, and returns a dispatch function, which in turn serves as next()
to the middleware to the left, and so on. It's still useful to have access to some store methods like getState()
, so store
stays available as the top-level argument.
尝试 #6:原生地应用中间件
¥Attempt #6: Naïvely Applying the Middleware
我们可以编写 applyMiddleware()
来代替 applyMiddlewareByMonkeypatching()
,它首先获取最终的、完全封装的 dispatch()
函数,并使用它返回存储的副本:
¥Instead of applyMiddlewareByMonkeypatching()
, we could write applyMiddleware()
that first obtains the final, fully wrapped dispatch()
function, and returns a copy of the store using it:
// 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()
的实现类似,但在三个重要方面有所不同:
¥The implementation of applyMiddleware()
that ships with Redux is similar, but different in three important aspects:
它仅向中间件公开 存储 API 的子集:
dispatch(action)
和getState()
。¥It only exposes a subset of the store API to the middleware:
dispatch(action)
andgetState()
.它做了一些技巧来确保如果你从中间件调用
store.dispatch(action)
而不是next(action)
,则该操作实际上将再次遍历整个中间件链,包括当前的中间件。这对于异步中间件很有用。在设置过程中调用dispatch
时有一个警告,如下所述。¥It does a bit of trickery to make sure that if you call
store.dispatch(action)
from your middleware instead ofnext(action)
, the action will actually travel the whole middleware chain again, including the current middleware. This is useful for asynchronous middleware. There is one caveat when callingdispatch
during setup, described below.为了确保你只能应用中间件一次,它在
createStore()
上运行,而不是在store
本身上运行。它的签名不是(store, middlewares) => store
,而是(...middlewares) => (createStore) => createStore
。¥To ensure that you may only apply middleware once, it operates on
createStore()
rather than onstore
itself. Instead of(store, middlewares) => store
, its signature is(...middlewares) => (createStore) => createStore
.
由于在使用之前将函数应用于 createStore()
很麻烦,因此 createStore()
接受可选的最后一个参数来指定此类函数。
¥Because it is cumbersome to apply functions to createStore()
before using it, createStore()
accepts an optional last argument to specify such functions.
警告:设置期间调度
¥Caveat: Dispatching During Setup
当 applyMiddleware
执行并设置中间件时,store.dispatch
函数将指向 createStore
提供的普通版本。调度将导致不应用其他中间件。如果你期望在安装过程中与另一个中间件进行交互,你可能会感到失望。由于这种意外行为,如果你尝试在设置完成之前分派操作,applyMiddleware
将引发错误。相反,你应该通过公共对象(对于 API 调用中间件,这可能是你的 API 客户端对象)直接与其他中间件通信,或者等到使用回调构造中间件之后。
¥While applyMiddleware
executes and sets up your middleware, the store.dispatch
function will point to the vanilla version provided by createStore
. Dispatching would result in no other middleware being applied. If you are expecting an interaction with another middleware during setup, you will probably be disappointed. Because of this unexpected behavior, applyMiddleware
will throw an error if you try to dispatch an action before the set up completes. Instead, you should either communicate directly with that other middleware via a common object (for an API-calling middleware, this may be your API client object) or waiting until after the middleware is constructed with a callback.
最终方法
¥The Final Approach
鉴于这个中间件,我们刚刚编写了:
¥Given this middleware we just wrote:
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 存储的方法:
¥Here's how to apply it to a Redux store:
import { createStore, combineReducers, applyMiddleware } from 'redux'
const todoApp = combineReducers(reducers)
const store = createStore(
todoApp,
// applyMiddleware() tells createStore() how to handle middleware
applyMiddleware(logger, crashReporter)
)
就是这样!现在分派到存储实例的任何操作都将流经 logger
和 crashReporter
:
¥That's it! Now any actions dispatched to the store instance will flow through logger
and crashReporter
:
// Will flow through both logger and crashReporter middleware!
store.dispatch(addTodo('Use Redux'))
七个例子
¥Seven Examples
如果你读完上面的部分后头脑发热,想象一下写下它是什么感觉。本节旨在让你和我放松一下,并帮助你加快步伐。
¥If your head boiled from reading the above section, imagine what it was like to write it. This section is meant to be a relaxation for you and me, and will help get your gears turning.
下面的每个函数都是一个有效的 Redux 中间件。它们并不同样有用,但至少同样有趣。
¥Each function below is a valid Redux middleware. They are not equally useful, but at least they are equally fun.
/**
* 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()
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)
}
const 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 => {
const 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) {
const 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.)
const todoApp = combineReducers(reducers)
const store = createStore(
todoApp,
applyMiddleware(
rafScheduler,
timeoutScheduler,
thunk,
vanillaPromise,
readyStatePromise,
logger,
crashReporter
)
)