十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

编写 Redux 自定义中间件:从标准模式到源码级兼容性实践

编写 Redux 自定义中间件:从标准模式到源码级兼容性实践 编写 Redux 自定义中间件从标准模式到源码级兼容性实践【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux导读中间件Middleware是 Redux 在派发 action与action 到达 reducer之间提供的第三方扩展点用于为 action 创建副作用、修改或取消 action以及扩展dispatch所能接受的输入类型。本文以 Redux 官方文档《Writing Custom Middleware》为主线结合当前仓库 applyMiddleware 源码 与 类型定义 的底层实现系统讲解自定义中间件的三大用途、标准编写模式以及与 redux-thunk、RTK Query 等生态中间件协同时必须遵守的两条兼容性规则。读完本文你将能写出可投入生产、与现有中间件链无缝协作的自定义中间件。中间件能做什么三种典型用途Redux 中的中间件主要用来做三类事情为 action 创建副作用最常见例如 [Redux-Saga]、[redux-observable] 以及 [RTK listener middleware] 都属于这一类——它们对 action 做出反应执行状态变更之外的额外逻辑。修改或取消 action例如用状态或外部输入为 action 附加信息或者对 action 做节流throttle、防抖debounce、门控gate。修改dispatch接受的输入最典型的例子是 [Redux Thunk]它把返回 action 的函数通过调用该函数转换为真正的 action。注上文提及的第三方库链接仅作为概念参照本文的代码与实现细节均以当前仓库源码为准。什么时候才需要自己写中间件大多数情况下你并不需要自定义中间件。副作用是中间件最常见的用途而生态中已有大量经过长期验证的成熟方案自己从零实现容易踩到各种隐蔽问题。建议优先考虑现成方案管理服务端状态数据获取/缓存使用 [RTK Query]其他副作用使用 [RTK listener middleware]。只有在以下两种场景才建议手写中间件只有一个非常简单的副作用为一个副作用引入整套框架可能不划算。此时可以写一个轻量中间件但务必记住当应用规模增长后要切换到成熟框架而不是继续养大自己的自定义方案。需要修改或取消 action这类需求没有通用的现成方案通常必须自定义。中间件的标准模式Standard Patterns为 action 创建副作用以 RTK listener middleware 为例这是最常见的中间件形态。其标准骨架是三层嵌套函数外层接收{ dispatch, getState }MiddlewareAPI中层接收next内层接收action。以 RTK listener middleware 的核心逻辑为例const middleware: ListenerMiddlewareS, D, ExtraArgument api next action { // 第一部分处理 addListener / clearAllListeners / removeListener 这类管理型action // 借此增删后续要触发的监听器 if (addListener.match(action)) { return startListening(action.payload) } if (clearAllListeners.match(action)) { clearListenerMiddleware() return } if (removeListener.match(action)) { return stopListening(action.payload) } // 第二部分在 reducer 处理 action 之前先取一次原始状态 // 注意必须在 next(action) 之前同步调用 let originalState: S | typeof INTERNAL_NIL_TOKEN api.getState() // getOriginalState 只能在同步代码中调用 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 { // 先把 action 沿中间件链继续向下传、交给 reducer 处理 result next(action) // reducer 执行完之后再取新状态然后通知各监听器 if (listenerMap.size 0) { let currentState api.getState() 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 { // 清除作用域内的 originalState 引用 originalState INTERNAL_NIL_TOKEN } return result }这段代码体现了两个要点第一部分next(action)之前监听addListener、clearAllListeners、removeListener等命令型action用来动态增删后续要运行的监听器。第二部分next(action)之后先通过api.getState()取到 action 经过其他中间件和 reducer 之后的新状态再把原始状态和新状态一并交给监听器使用。为什么副作用通常放在next(action)之后执行因为这样能同时拿到原始状态与新状态做对比而且副作用产生的交互本就不该影响当前 action 的执行——否则它就不叫副作用了。修改/取消 action或扩展dispatch输入以 redux-thunk 为例这一类模式相对少见但除取消 action外绝大部分能力都能在 redux-thunk 中间件中看到。redux-thunk 的完整实现只有几行const middleware: ThunkMiddlewareState, BasicAction, ExtraThunkArg ({ dispatch, getState }) next action { // thunk 中间件检查传给 store.dispatch 的是否是函数。 // 如果是函数即 thunk就调用它并返回其结果。 if (typeof action function) { // 注入 store 的 dispatch、getState 方法以及可选的 extra arg return action(dispatch, getState, extraArgument) } // 否则把 action 沿中间件链正常向下传递 return next(action) }原理说明原生dispatch只能处理普通对象JSON 可序列化actionthunk 中间件让dispatch额外接受函数形态的 action。它还改变了dispatch的返回类型把函数 action 的返回值作为dispatch的返回值返回因此调用方可以await一个返回 Promise 的 thunk。注意这里注入的是dispatch而非nextthunk 内部的dispatch会重新从中间件链的起点开始派发从而支持在异步流程中继续派发其他 action。与仓库源码互相印证applyMiddleware如何组装链条要理解上面两种模式为什么成立需要看 Redux 内置的 applyMiddleware 源码export default function applyMiddleware( ...middlewares: Middleware[] ): StoreEnhancerany { return createStore (reducer, preloadedState) { const store createStore(reducer, preloadedState) let dispatch: Dispatch () { throw new Error( Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch. ) } const middlewareAPI: MiddlewareAPI { getState: store.getState, dispatch: (action, ...args) dispatch(action, ...args) } const chain middlewares.map(middleware middleware(middlewareAPI)) dispatch composetypeof dispatch(...chain)(store.dispatch) return { ...store, dispatch } } }关键机制如下每个中间件只被调用一次middleware(middlewareAPI)得到包装函数即中层函数真正的包装链通过 compose 源码 从右到左组合而成。中间件拿到的是{ dispatch, getState }子集而不是整个 store参见 types/middleware.ts 中MiddlewareAPI的定义这与仓库 types/store.ts 中dispatch/getState的真实签名一致。通过闭包dispatch: (action, ...args) dispatch(action, ...args)实现了从中间件内部调用dispatch会重新走完整条链含当前中间件的行为——这正是 thunk 等异步中间件所依赖的特性也是 test/applyMiddleware.spec.ts 中passes recursive dispatches through the middleware chain测试验证的内容。在中间件构造期间派发 action 是被禁止的此时dispatch指向一个会抛错的占位函数错误信息见上文源码第 58-63 行因为此时组装尚未完成其他中间件尚未生效。对应测试见 test/applyMiddleware.spec.tswarns when dispatching during middleware setup。仓库中 test/helpers/middleware.ts 也提供了一个极简 thunk 中间件实现可作为自定义中间件的最小参考模板export const thunk: Middleware{ R(thunk: (dispatch: Dispatch, getState: () any) R): R } ({ dispatch, getState }) next action typeof action function ? action(dispatch, getState) : next(action)编写兼容中间件的两条规则中间件本质上是能力极强的模式可以对 action 做任何事。但链中的其他中间件可能对周围发生了什么存在隐含假设。你的中间件与外界只有两个接触点把握住这两点就能保证与常用生态中间件良好协作。规则一正确调用next调用next时它期望收到某种形式的 action。除非你要显式修改它否则原样透传收到的 action。更微妙的一点某些中间件假设中间件与dispatch的调用发生在同一 tick同步。因此你的中间件应当同步调用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后即便return response写的是同步值函数实际返回的也会是一个 Promise——返回类型被悄悄改变了。这会破坏像 RTK Query 这类对dispatch返回值有严格预期的中间件。正确写法把异步逻辑挪出中间件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逻辑提取到独立函数loadData中中间件本体保持同步response的返回类型不被改变。void是显式声明我故意不 await 这个 Promise对代码执行无影响但向阅读者传达了意图也避免了 eslint 的 no-floating-promises 告警。附仓库内的相关资源中间件设计史与原理从手动记录日志 → 包装 dispatch → monkeypatch → 柯里化三层函数的完整推导过程并给出 logger、crashReporter、timeoutScheduler、rafScheduler、vanillaPromise、readyStatePromise、thunk 等七个可直接运行的中间件示例。applyMiddleware API 文档参数说明、返回的 store enhancer 签名、自定义 Logger 中间件与 thunk 异步 action 的完整示例以及多 enhancer 组合时applyMiddleware应放在最前等注意事项。Store 的 dispatch 契约基础dispatch只接受普通对象 action若需派发 Promise、Observable、thunk 等必须通过中间件扩展。仓库测试 test/applyMiddleware.spec.ts覆盖中间件构造期禁止 dispatch中间件只包装一次递归 dispatch 穿过整条链与 thunk 协同透传 dispatch 额外参数等行为是验证自定义中间件行为的直接参考。【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表