Skip to main content

编写自定义中间件

¥Writing Custom Middleware

你将学到什么
  • 何时使用自定义中间件

    ¥When to use custom middleware

  • 中间件的标准模式

    ¥Standard patterns for middleware

  • 如何确保你的中间件与其他 Redux 项目兼容

    ¥How to make sure that your middleware is compatible with other Redux projects

Redux 中的中间件主要用于

¥Middleware in Redux can be mainly used to either

  • 为动作创建副作用,

    ¥create side effects for actions,

  • 修改或取消操作,或者

    ¥modify or cancel actions, or to

  • 修改调度接受的输入。

    ¥modify the input accepted by dispatch.

大多数用例属于第一类:例如,Redux-Sagaredux-observableRTK 监听中间件 都会产生对操作做出反应的副作用。这些例子也表明这是一个非常普遍的需求:能够对状态变化之外的动作做出反应。

¥Most use cases fall into the first category: For example Redux-Saga, redux-observable, and RTK listener middleware all create side effects that react to actions. These examples also show that this is a very common need: To be able to react to an action other than with a state change.

修改操作可用于例如 利用来自状态或外部输入的信息来增强动作,或者对它们进行节流、反跳或门控。

¥Modifying actions can be used to e.g. enhance an action with information from the state or from an external input, or to throttle, debounce or gate them.

修改 dispatch 的输入最明显的例子是 Redux Thunk,它通过调用将返回动作的函数转换为动作。

¥The most obvious example for modifying the input of dispatch is Redux Thunk, which transforms a function returning an action into an action by calling it.

何时使用自定义中间件

¥When to use custom middleware

大多数时候,你实际上并不需要自定义中间件。中间件最可能的用例是副作用,并且有很多包很好地封装了 Redux 的副作用,并且已经使用了足够长的时间来消除你自己构建时可能遇到的微妙问题。一个好的起点是用于管理服务器端状态的 RTK 查询 和用于其他副作用的 RTK 监听中间件

¥Most of the time, you won't actually need custom middleware. The most likely use case for middleware is side effects, and there is plenty of packages who nicely package side effects for Redux and have been in use long enough to get rid of the subtle problems you would run into when building this yourself. A good starting point is RTK Query for managing server-side state and RTK listener middleware for other side effects.

在以下两种情况之一中,你可能仍想使用自定义中间件:

¥You might still want to use custom middleware in one of two cases:

  1. 如果你只有一个非常简单的副作用,那么添加完整的附加框架可能不值得。只需确保在应用增长后切换到现有框架,而不是开发自己的自定义解决方案。

    ¥If you only have a single, very simple side effect, it might not be worth it to add a full additional framework. Just make sure that you switch to an existing framework once your application grows instead of growing your own custom solution.

  2. 如果你需要修改或取消操作。

    ¥If you need to modify or cancel actions.

中间件的标准模式

¥Standard patterns for middleware

为操作创建副作用

¥Create side effects for actions

这是最常见的中间件。RTK 监听中间件 的外观如下:

¥This is the most common middleware. Here's what it looks like for rtk listener middleware:

const middleware: ListenerMiddleware<S, D, ExtraArgument> =
api => next => action => {
if (addListener.match(action)) {
return startListening(action.payload)
}

if (clearAllListeners.match(action)) {
clearListenerMiddleware()
return
}

if (removeListener.match(action)) {
return stopListening(action.payload)
}

// Need to get this state _before_ the reducer processes the action
let originalState: S | typeof INTERNAL_NIL_TOKEN = api.getState()

// `getOriginalState` can only be called synchronously.
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
const getOriginalState = (): S => {
if (originalState === INTERNAL_NIL_TOKEN) {
throw new Error(
`${alm}: getOriginalState can only be called synchronously`
)
}

return originalState as S
}

let result: unknown

try {
// Actually forward the action to the reducer before we handle listeners
result = next(action)

if (listenerMap.size > 0) {
let currentState = api.getState()
// Work around ESBuild+TS transpilation issue
const listenerEntries = Array.from(listenerMap.values())
for (let entry of listenerEntries) {
let runListener = false

try {
runListener = entry.predicate(action, currentState, originalState)
} catch (predicateError) {
runListener = false

safelyNotifyError(onError, predicateError, {
raisedBy: 'predicate'
})
}

if (!runListener) {
continue
}

notifyListener(entry, action, api, getOriginalState)
}
}
} finally {
// Remove `originalState` store from this scope.
originalState = INTERNAL_NIL_TOKEN
}

return result
}

在第一部分中,它监听 addListenerclearAllListenersremoveListener 操作,以更改稍后应调用的监听器。

¥In the first part, it listens to addListener, clearAllListeners and removeListener actions to change which listeners should be invoked later on.

在第二部分中,代码主要计算通过其他中间件和 reducer 传递 action 后的状态,然后将原始状态以及来自 reducer 的新状态传递给监听器。

¥In the second part, the code mainly calculates the state after passing the action through the other middlewares and the reducer, and then passes both the original state as well as the new state coming from the reducer to the listeners.

在分派操作后出现副作用是很常见的,因为这允许同时考虑原始状态和新状态,并且来自副作用的交互无论如何都不应该影响当前操作的执行(否则,它就不会影响当前操作的执行)。 不是副作用)。

¥It is common to have side effects after dispatching the action, because this allows taking into account both the original and the new state, and because the interaction coming from the side effects shouldn't influence the current action execution anyways (otherwise, it wouldn't be a side effect).

修改或取消操作,或修改调度接受的输入

¥Modify or cancel actions, or modify the input accepted by dispatch

虽然这些模式不太常见,但其中大多数(取消操作除外)均由 redux thunk 中间件 使用:

¥While these patterns are less common, most of them (except for cancelling actions) are used by redux thunk middleware:

const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
({ dispatch, getState }) =>
next =>
action => {
// The thunk middleware looks for any functions that were passed to `store.dispatch`.
// If this "action" is really a function, call it and return the result.
if (typeof action === 'function') {
// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
return action(dispatch, getState, extraArgument)
}

// Otherwise, pass the action down the middleware chain as usual
return next(action)
}

通常,dispatch 只能处理 JSON 操作。该中间件还增加了处理函数形式的操作的能力。它还通过将函数操作的返回值传递为调度函数的返回值来更改调度函数本身的返回类型。

¥Usually, dispatch can only handle JSON actions. This middleware adds the ability to also handle actions in the form of functions. It also changes the return type of the dispatch function itself by passing the return value of the function-action to be the return value of the dispatch function.

制作兼容中间件的规则

¥Rules to make compatible middleware

原则上,中间件是一种非常强大的模式,可以通过一个动作做任何它想做的事情。不过,现有的中间件可能对其周围的中间件中发生的情况有一些假设,并且了解这些假设将更容易确保你的中间件与现有的常用中间件良好配合。

¥In principle, middleware is a very powerful pattern and can do whatever it wants with an action. Existing middleware might have assumptions about what happens in the middleware around it, though, and being aware of these assumptions will make it easier to ensure that your middleware works well with existing commonly used middleware.

我们的中间件和其他中间件之间有两个接触点:

¥There are two contact points between our middleware and the other middlewares:

调用下一个中间件

¥Calling the next middleware

当你调用 next 时,中间件将期望某种形式的操作。除非你想显式修改它,否则只需传递你收到的操作即可。

¥When you call next, the middleware will expect some form of action. Unless you want to explicitly modify it, just pass through the action that you received.

更巧妙的是,一些中间件期望中间件在调用 dispatch 的同一时间点被调用,因此 next 应该由中间件同步调用。

¥More subtly, some middlewares expect that the middleware is called on the same tick as dispatch is called, so next should be called synchronously by your middleware.

返回调度返回值

¥Returning the dispatch return value

除非中间件需要显式修改 dispatch 的返回值,否则直接返回从 next 得到的值即可。如果你确实需要修改返回值,那么你的中间件将需要位于中间件链中的一个非常特定的位置,以便能够执行其应该执行的操作 - 你将需要手动检查与所有其他中间件的兼容性并决定它们如何协同工作。

¥Unless the middleware needs to explicitly modify the return value of dispatch, just return what you get from next. If you do need to modify the return value, then your middleware will need to sit in a very specific spot in the middleware chain to be able to do what it is supposed to - you will need to check compatibility with all other middlewares manually and decide how they could work together.

这会产生一个棘手的后果:

¥This has a tricky consequence:

const middleware: Middleware = api => next => async action => {
const response = next(action)

// Do something after the action hits the reducer
const afterState = api.getState()
if (action.type === 'some/action') {
const data = await fetchData()
api.dispatch(dataFetchedAction(data))
}

return response
}

尽管看起来我们没有修改响应,但实际上我们做了:由于 async-await,它现在是一个 promise。这会破坏一些中间件,例如 RTK 查询中的中间件。

¥Even though it looks like we didn't modify the response, we actually did: Due to async-await, it is now a promise. This will break some middlewares like the one from RTK Query.

那么,我们如何编写这个中间件呢?

¥So, how can we write this middleware instead?

const middleware: Middleware = api => next => action => {
const response = next(action)

// Do something after the action hits the reducer
const afterState = api.getState()
if (action.type === 'some/action') {
void loadData(api)
}

return response
}

async function loadData(api) {
const data = await fetchData()
api.dispatch(dataFetchedAction(data))
}

只需将异步逻辑移至单独的函数中,以便你仍然可以使用 async-await,但实际上不必等待中间件中的 Promise 解析。void 向阅读代码的其他人表明你决定不显式等待 promise 而不会对代码产生影响。

¥Just move out the async logic into a separate function so that you can still use async-await, but don't actually wait for the promise to resolve in the middleware. void indicates to others reading the code that you decided to not await the promise explicitly without having an effect on the code.

下一步

¥Next Steps

如果你还没有,请查看 了解 Redux 中的中间件部分 以了解中间件在幕后的工作原理。

¥If you haven't yet, take a look at the Middleware section in Understanding Redux to understand how middleware works under the hood.