跳至主内容
非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

编写自定义中间件

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

Redux 中的中间件主要可用于以下场景:

  • 为 Action 创建副作用

  • 修改或取消 Action

  • 修改 dispatch 接受的输入

大多数用例属于第一类:例如 Redux-Sagaredux-observableRTK 监听中间件 都会创建响应 Action 的副作用。这些示例也表明这是非常普遍的需求:能够以状态变更之外的方式响应 Action。

修改 Action 可用于增强 Action(例如从状态或外部输入添加信息),或进行节流、防抖和门控操作。

修改 dispatch 输入最典型的例子是 Redux Thunk,它通过调用函数将返回 Action 的函数转换为 Action。

何时使用自定义中间件

大多数情况下你并不需要自定义中间件。中间件最可能的用例是处理副作用,而现有许多成熟的包已经为 Redux 封装了副作用处理逻辑,并经过长期验证能规避自行实现时可能遇到的隐性问题。建议从 RTK Query 开始管理服务端状态,使用 RTK 监听中间件 处理其他副作用。

在以下两种情况下你仍可能需要自定义中间件:

  1. 当仅存在非常简单的单一副作用时,可能不值得引入完整框架。但需确保在应用规模增长时改用现有方案,而非持续扩展自定义实现。

  2. 当需要修改或取消 Action 时。

中间件的标准模式

为 Action 创建副作用

这是最常见的中间件类型。以下是 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 Action,动态调整后续应触发的监听器。

第二部分主要计算 Action 经过其他中间件和 reducer 处理后的状态,并将原始状态与 reducer 产生的新状态同时传递给监听器。

通常副作用应在 dispatch Action 后执行,这样既能兼顾原始状态和新状态,又避免副作用产生的交互影响当前 Action 执行(否则就不称为副作用了)。

修改/取消 Action 或修改 dispatch 输入

虽然这些模式较为少见,但 redux thunk 中间件 实现了除取消 Action 外的大部分功能:

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 Action。此中间件新增了处理函数形式 Action 的能力,同时通过将函数 Action 的返回值作为 dispatch 的返回值,改变了 dispatch 函数本身的返回类型。

编写兼容中间件的准则

中间件本质上是非常强大的模式,可以自由处理 Action。但现有中间件可能对周边中间件的行为存在预期,了解这些预期将有助于确保你的中间件与常用中间件良好协作。

我们的中间件与其他中间件存在两个接触点:

调用后续中间件

调用 next 时,后续中间件期望接收某种形式的 Action。除非需要显式修改,否则直接透传接收到的 Action。

更微妙的是:某些中间件要求 next 必须在调用 dispatch 的同一事件循环中被同步调用。

返回 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 中的中间件章节 掌握中间件的底层原理。