跳至主要内容

编写自定义中间件

您将学到什么
  • 何时使用自定义中间件
  • 中间件的标准模式
  • 如何确保您的中间件与其他 Redux 项目兼容

Redux 中的中间件主要用于

  • 为操作创建副作用,
  • 修改或取消操作,或
  • 修改调度接受的输入。

大多数用例都属于第一类:例如 Redux-Sagaredux-observableRTK 监听器中间件 都会创建对操作做出反应的副作用。这些示例还表明,这是一个非常普遍的需求:能够对操作做出反应,而不是仅进行状态更改。

修改操作可用于例如使用来自状态或外部输入的信息来增强操作,或对操作进行节流、去抖动或门控。

修改调度输入的最明显示例是 Redux Thunk,它通过调用返回操作的函数来将该函数转换为操作。

何时使用自定义中间件

大多数情况下,您实际上不需要自定义中间件。中间件最有可能的用例是副作用,并且有很多包很好地为 Redux 打包了副作用,并且已经使用足够长的时间来消除您在自行构建时会遇到的细微问题。一个好的起点是 RTK Query 用于管理服务器端状态,以及 RTK 监听器中间件 用于其他副作用。

您可能仍然希望在以下两种情况下使用自定义中间件

  1. 如果您只有一个非常简单的副作用,那么添加一个完整的额外框架可能不值得。只要确保在应用程序增长时切换到现有框架,而不是扩展自己的自定义解决方案。
  2. 如果您需要修改或取消操作。

中间件的标准模式

为操作创建副作用

这是最常见的中间件。以下是 rtk 监听器中间件 的示例。

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 操作,以更改稍后应该调用的监听器。

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

在分发操作后通常会有副作用,因为这允许考虑原始状态和新状态,并且因为来自副作用的交互不应该影响当前操作的执行(否则,它就不是副作用)。

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

虽然这些模式不太常见,但大多数(除了取消操作)都被 redux thunk 中间件 使用。

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 操作。此中间件增加了处理函数形式操作的能力。它还通过将函数操作的返回值传递给 dispatch 函数的返回值来更改 dispatch 函数本身的返回类型。

创建兼容中间件的规则

原则上,中间件是一种非常强大的模式,可以对操作做任何事情。但是,现有的中间件可能对周围中间件中发生的事情有一些假设,了解这些假设将使您更容易确保您的中间件与现有的常用中间件良好协作。

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

调用下一个中间件

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

更微妙的是,一些中间件期望中间件在与dispatch相同的时钟周期内被调用,因此next应该由您的中间件同步调用。

返回 dispatch 返回值

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

这有一个棘手的后果

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 Query 中的中间件。

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

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,而不会对代码产生影响。

下一步

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