尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

redux-observable 依赖注入实战:用 createEpicMiddleware 的 dependencies 选项让 Epic 更易测试

发布时间:2026/9/27 21:16:31

资讯中心
01
ARTICLE

redux-observable 依赖注入实战:用 createEpicMiddleware 的 dependencies 选项让 Epic 更易测试

redux-observable 依赖注入实战:用 createEpicMiddleware 的 dependencies 选项让 Epic 更易测试
前端【免费下载链接】redux-observableRxJS middleware for action side effects in Redux using Epics项目地址https://gitcode.com/gh_mirrors/re/redux-observable点击查看免费下载本篇指南围绕 redux-observable 官方 Recipes 中的《Injecting Dependencies Into Epics》展开讲解如何通过createEpicMiddleware({ dependencies })配置项把 AJAX、存储、配置等外部服务以第三个参数的形式注入到所有 Epic 中从而让 Epic 与副作用实现解耦、可直接在测试中传入 mock。读完本文你将掌握依赖注入的标准写法、其底层实现原理对应 src/createEpicMiddleware.ts 源码以及直接调用 Epic 传 mock与TestScheduler 大理石测试两种可落地的测试方案。为什么需要依赖注入直接 import 的痛点在编写 Epic 处理网络请求时最直观的写法是从rxjs/ajax直接引入ajax工具函数并在 Epic 内部使用它import { ajax } from rxjs/ajax; const fetchUserEpic (action$, state$) action$.pipe( ofType(FETCH_USER), mergeMap(({ payload }) ajax.getJSON(/api/users/${payload}).pipe( map(response ({ type: FETCH_USER_FULFILLED, payload: response, })), ), );这段代码本身没有问题但它埋下了一个测试层面的隐患存放该 Epic 的文件直接 import 了它的依赖。由于依赖是模块级的硬编码绑定在测试中想要替换它就非常困难。常见的替代方案是 mock 全局对象window.XMLHttpRequest。这虽然可行但存在两个明显问题工作量更大测试代码变得复杂冗长你不再只是在测试自己的 Epic而是在测试 RxJS 内部是否正确使用了XMLHttpRequest——这显然不该是你的测试目标。提示许多测试框架提供了比本文描述方案更强大的 mock 能力例如 Jest 的 manual mocks官方文档有详细介绍。测试工具的选型请以适合你的项目为准本文介绍的注入方式与框架无关可作为通用基础。依赖注入的思路因此变得顺理成章让 Epic 不直接拥有依赖而是由外部在创建 middleware 时统一提供。标准做法createEpicMiddleware 的 dependencies 配置项redux-observable 在createEpicMiddleware的 options 中提供了专门的dependencies字段。官方 API 文档见 docs/api/createEpicMiddleware.md对其描述为If given, it will be injected as the 3rd argument to all epics.——只要提供了该选项它就会被作为第三个参数注入到所有Epic 中。import { createEpicMiddleware, combineEpics } from redux-observable; import { ajax } from rxjs/ajax; import rootEpic from ./somewhere; const epicMiddleware createEpicMiddleware({ dependencies: { getJSON: ajax.getJSON }, }); epicMiddleware.run(rootEpic);配置好 middleware 后注入的依赖会作为第三个参数传给每一个 Epic前两个参数分别是action$与state$。底层实现在 src/createEpicMiddleware.ts 中一目了然const output$ epic(action$, state$, options.dependencies!);对应的Options接口同样定义在该文件中src/createEpicMiddleware.tsinterface OptionsD any { dependencies?: D; }而从 Epic 的类型定义src/epic.ts可以确认这一约定是类型系统的一部分——Epic 本质上就是一个接收三个参数、返回 Observable 的纯函数export declare interface Epic Input unknown, Output extends Input Input, State void, Dependencies any { ( action$: ObservableInput, state$: StateObservableState, dependencies: Dependencies ): ObservableOutput; }需要说明任何值都可以作为dependencies传入它不限于对象字面量。测试用例 test/createEpicMiddleware-spec.ts 中的should pass literally anything provided as dependencies, even \undefined明确验证了这一点——即使显式传入undefined它也会被原样透传给 Epic。在 Epic 中使用注入的依赖注入之后Epic 便不再自己 importajax而是从第三个参数中解构出依赖// 注意第三个参数就是我们注入的 dependencies const fetchUserEpic (action$, state$, { getJSON }) action$.pipe( ofType(FETCH_USER), mergeMap(({ payload }) getJSON(/api/users/${payload}).pipe( map(response ({ type: FETCH_USER_FULFILLED, payload: response, })), ), );相比直接 import 的版本这里唯一的区别是ajax.getJSON换成了参数解构出来的getJSON。由于createEpicMiddleware({ dependencies: { getJSON: ajax.getJSON } })在创建时已把真实的ajax.getJSON绑定为getJSON生产环境的行为完全一致而在测试环境下你可以自由地把getJSON换成任意 mock。依赖如何穿透 combineEpics 的组合如果你的应用使用了combineEpics组合多个 Epic依赖注入依然对所有子 Epic 生效。这是因为combineEpics本质上只是把多个 Epic 的输出流用merge合并并原封不动地把收到的全部参数转发给每一个子 Epic。源码见 src/combineEpics.tsconst merger: EpicInput, Output, State, Dependencies (...args) merge( ...epics.map((epic) { const output$ epic(...args); if (!output$) { throw new TypeError( combineEpics: one of the provided Epics ${epic.name || anonymous} does not return a stream. Double check youre not missing a return statement! ); } return output$; }) );在 test/createEpicMiddleware-spec.ts 的should inject dependencies into combined epics用例中测试构造了combineEpics(epic, epic, combineEpics(epic, combineEpics(epic, epic)))这种多层嵌套组合传入dependencies: { foo: bar, bar: foo }最终断言每个子 Epic 都收到了相同的依赖对象该用例中 epic 被调用了 5 次。此外should call epics with all additional arguments, not just dependencies用例还验证了在combineEpics转发参数的基础上Epic 甚至还能接收第三个参数以外的更多自定义参数说明依赖注入机制对参数透传是开放的。直接调用 Epic 进行单元测试依赖注入最大的收益体现在测试上由于 Epic 只是一个普通函数你可以像调用任何函数一样直接调用它自行传入 mock 的action$、state$与依赖完全不需要真的创建 Redux store 或挂载 middleware。import { of } from rxjs; import { fetchUserEpic } from ./somewhere/fetchUserEpic; const mockResponse { name: Bilbo Baggins }; const action$ of({ type: FETCH_USERS_REQUESTED }); const state$ null; // 该 Epic 用不到 state可以传 null const dependencies { getJSON: (url) of(mockResponse), }; // 请根据你的测试框架和具体场景适配此示例 const result$ fetchUserEpic(action$, state$, dependencies).pipe( toArray(), // 缓冲输出直到你的 Epic 自然完成(complete) ); result$.subscribe((actions) { assertDeepEqual(actions, [ { type: FETCH_USER_FULFILLED, payload: mockResponse, }, ]); });这里的关键技巧是toArray()操作符它会把 Epic 输出的所有 action 缓冲成一个数组等 Epic 自然完成后再一次性发出方便对整段输出做断言。上述写法可以直接嵌入 Mocha、Jest、Vitest 等任意测试框架只需把assertDeepEqual换成框架自带的断言 API 即可。值得强调的是Epics 只是使用 RxJS 的普通函数——除了约定输出{ type: string }形式的 action 外它与 Redux 本身没有任何直接耦合详见 docs/recipes/WritingTests.md。这正是直接调用 Epic 传 mock这一测试方式能够成立的根本原因。进阶用 TestScheduler 大理石测试注入依赖如果希望测试更贴近真实的时间行为例如网络延迟、并发合并docs/recipes/WritingTests.md 还提供了基于 RxJSTestScheduler的进阶方案。注入的依赖同样可以直接用 marble 冷流cold(...)来 mockimport { TestScheduler } from rxjs/testing; const testScheduler new TestScheduler((actual, expected) { // 在此断言两个对象相等例如 chai 的 expect(actual).deep.equal(expected) }); testScheduler.run(({ hot, cold, expectObservable }) { const action$ hot(-a, { a: { type: FETCH_USER, id: 123 }, }); const state$ null; const dependencies { getJSON: (url) cold(--a, { a: { url }, }), }; const output$ fetchUserEpic(action$, state$, dependencies); expectObservable(output$).toBe(---a, { a: { type: FETCH_USER_FULFILLED, response: { url: https://api.github.com/users/123, }, }, }); });在这个例子中hot(-a, ...)表示在第 1 帧模拟派发一个FETCH_USERactionmock 的getJSON用cold(--a, ...)模拟 2 帧后返回响应最终断言输出流在第 3 帧发出FETCH_USER_FULFILLEDaction。由于时间是虚拟化的这类测试既确定又极快。如果你发现大量测试以及 Epic 本身高度雷同官方文档还建议把最常用的模式抽象成自己的辅助函数以减少样板代码。TypeScript 类型支持让依赖注入类型安全redux-observable 的依赖注入在 TypeScript 下拥有完整的类型推导。createEpicMiddleware的泛型签名见 src/createEpicMiddleware.ts按Input、Output、State、Dependencies四个维度约束export function createEpicMiddleware Input unknown, Output extends Input Input, State void, Dependencies any ( options: OptionsDependencies {} ): EpicMiddlewareInput, Output, State, Dependencies { ... }实际使用时可以像 test/createEpicMiddleware-spec.ts 中的should inject dependencies into a single epic用例那样显式指定依赖类型const middleware createEpicMiddlewareunknown, unknown, UnknownAction[], string({ dependencies: deps, });这样你的 Epic 第三个参数会被类型化为string一旦某个 Epic 误用了不存在的依赖成员编译期就能直接报错。注意事项与边界条件围绕dependencies选项结合源码与测试有几个实践要点值得留意未提供 dependencies 时第三个参数是undefined而非缺省对象。测试用例should pass undefined as third argument to epic if no dependencies providedtest/createEpicMiddleware-spec.ts验证了这一点即使不传 optionsEpic 依然会被以三个参数调用第三位为undefined。因此编写 Epic 时解构依赖前务必确认 middleware 已正确注入。不要向createEpicMiddleware直接传函数旧版 API。旧版本支持createEpicMiddleware(rootEpic)的写法现已被移除。源码 src/createEpicMiddleware.ts 在非生产环境下会直接抛出 TypeError提示改用epicMiddleware.run(rootEpic)。依赖注入的正确流程是先createEpicMiddleware({ dependencies })创建实例接入 store 后再调用run(rootEpic)完整接入步骤见 docs/basics/SettingUpTheMiddleware.md。每个 store 应创建独立的 middleware 实例。源码 src/createEpicMiddleware.ts 会在同一实例被复用于多个 store 时打印警告this middleware is already associated with a store. createEpicMiddleware should be called for every store.对应 test/createEpicMiddleware-spec.ts 的should warn about reusing the epicMiddleware用例。若应用存在多个 store请为每个 store 分别调用createEpicMiddleware。依赖在 middleware 创建时被捕获。options.dependencies是在调用epic(action$, state$, options.dependencies!)时从闭包中读取的src/createEpicMiddleware.ts因此依赖是创建 middleware 那一刻的快照后续修改options对象不会影响已创建的实例。若需要动态依赖例如按 action 变化可以注入一个返回依赖的函数或使用可变的容器对象。依赖注入不只用于测试。虽然本 recipe 以测试为出发点但dependencies同样适合注入配置常量、日志器、存储后端、事件总线等横切服务让 Epic 保持纯粹、可组合、可替换。更复杂的异步添加新 Epic配合epicMiddleware.run()多次调用见 docs/api/EpicMiddleware.md 的run(rootEpic)说明与 HMR 等场景也与本文的注入方案完全兼容。小结把依赖从模块内 import改为middleware 创建时注入是 redux-observable 中让 Epic 易于测试的关键模式用createEpicMiddleware({ dependencies: { getJSON: ajax.getJSON } })统一注入依赖依赖以第三个参数的形式传给所有 Epic包括combineEpics组合后的所有子 Epic底层实现在 src/createEpicMiddleware.ts测试时直接调用 Epic用 mock 的action$、state$、dependencies驱动配合toArray()缓冲断言或用TestScheduler进行时间虚拟化的大理石测试参考 docs/recipes/WritingTests.mdTypeScript 下通过createEpicMiddlewareInput, Output, State, Dependencies泛型获得类型安全。这套模式与具体测试框架解耦是理解 redux-observable 可测试性设计的核心一环也是从能跑走向可维护、可验证的必经之路。赞分享前端【免费下载链接】redux-observableRxJS middleware for action side effects in Redux using Epics项目地址https://gitcode.com/gh_mirrors/re/redux-observable点击查看免费下载相关推荐DOPDropDownMenu测试策略如何确保iOS下拉菜单的稳定性DOPDropDownMenu测试策略如何确保iOS下拉菜单的稳定性 DOPDropDownMenu是一款为iPhone打造的网页风格下拉菜单组件能够为iOUI库/组件移动开发FastAPI 依赖注入进阶以 Python 类作为依赖Classes as Dependencies的原理与实战FastAPI 依赖注入进阶以 Python 类作为依赖Classes as Dependencies的原理与实战 本指南围绕 FastAPI 官方教程中后端Web框架API设计FastAPI 依赖注入进阶把 Python 类用作依赖项Classes as Dependencies的完整实践指南FastAPI 依赖注入进阶把 Python 类用作依赖项Classes as Dependencies的完整实践指南 本篇指南围绕 FastAPI 官方后端Web框架API设计上一篇终极指南xiaozhi-esp32-server网络隔离与VLAN配置实践下一篇ZLUDA代码生成宏系统与自动化API绑定创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。