☰
redux-observable 1.0.0 迁移指南:RxJS v6、Redux v4 与新的 Epic 中间件 API
2026/9/27 8:51:15 网站建设 项目流程
  • 前端

【免费下载链接】redux-observable

RxJS middleware for action side effects in Redux using "Epics"

项目地址:https://gitcode.com/gh_mirrors/re/redux-observable
点击查看免费下载

本篇指南完整讲解从旧版 redux-observable 迁移到 1.0.0 的全部破坏性变更,覆盖 RxJS v6 / Redux v4 的版本升级、createEpicMiddleware与epicMiddleware.run(rootEpic)的新式装配流程、动作队列调度(queueScheduler)对 Epic 通信顺序的影响,以及store.dispatch移除后如何改用state$与管道操作符完成副作用编程。读完本文,你将能够把旧代码库平稳迁移到新 API,并理解新中间件底层(src/createEpicMiddleware.ts)为何如此设计。

版本前提:RxJS v6 与 Redux v4

redux-observable 1.0.0 对依赖库有硬性版本要求,迁移的第一步是同步升级这两个依赖:

  • RxJS v6.0.0 及以上:1.0.0 基于 RxJS v6 编写,全面采用 pipeable 操作符(如pipe(ofType(...))),不再支持 v5 的链式操作符语法。RxJS 官方提供了独立的 v5→v6 迁移指南,并提供了rxjs-compat兼容层,允许你临时继续使用 v5 的旧导入路径与旧 API,实现渐进式迁移。
  • Redux v4:Redux v4 相比 v3 只有少量破坏性变更(如不再建议在中间件装配期间 dispatch 动作),但足以影响 redux-observable 的中间件初始化时序,这正是下述 API 重构的直接原因。

从当前仓库源码看,1.0.0 的导出面(src/index.ts)只暴露createEpicMiddleware、combineEpics、StateObservable、Epic类型与ofType操作符,迁移后你的业务代码几乎只与这些符号打交道。

中间件装配:rootEpic 交给run(),不再传入工厂

旧写法(已废弃)

旧版允许(甚至推荐)把根 Epic 直接传给createEpicMiddleware(rootEpic):

// 旧版(1.0.0 中会直接抛出 TypeError) const epicMiddleware = createEpicMiddleware(rootEpic);

新写法

1.0.0 要求先以无参方式创建中间件,将其放入 Redux 的中间件链,待 store 创建完成后再调用实例方法run(rootEpic):

const epicMiddleware = createEpicMiddleware(); const store = createStore(rootReducer, applyMiddleware(epicMiddleware)); epicMiddleware.run(rootEpic);

这一变更的动机在于 Redux v4 的约束:中间件装配期间不应 dispatch 动作,而旧 API 下 Epic 可能在 store 尚未就绪时就触发动作。将 Epic 的启动推迟到run()之后,从时序上彻底规避了该问题。

仓库源码印证了这一点:src/createEpicMiddleware.ts 中createEpicMiddleware仅在非生产环境下检测“传入函数”的旧式用法并抛出明确错误:

if (process.env.NODE_ENV !== 'production' && typeof options === 'function') { throw new TypeError( 'Providing your root Epic to `createEpicMiddleware(rootEpic)` is no longer supported, instead use `epicMiddleware.run(rootEpic)`' ); }

对应测试用例(test/createEpicMiddleware-spec.ts 的should throw an error if you provide a function to createEpicMiddleware)直接断言了这条报错信息,说明这是有意设计的护栏而非文档噪声。

多次调用run():Epic 合并而非替换

新 API 还带来了一个额外能力:后续的run(epic)调用不会替换之前的 Epic,而是全部合并运行。这意味着:

  • 异步懒加载(async lazy loading)新 Epic 变得非常容易;
  • 代码分割、按需注入业务模块时,只需在拿到模块后再次run();
  • 热替换场景下可用它装载新的根 Epic。

官方 API 文档(docs/api/EpicMiddleware.md)对run(rootEpic)的说明与此一致:应用实现代码分割并希望动态加载部分 Epics 或使用热重载时,可能需要多次调用该方法。源码层面,run每次执行epic$.next(rootEpic),而内部epic$是一个Subject,后续所有 Epic 都被mergeMap订阅合并,因此天然支持多次装载。

注意:如果run()在中间件被 store 装配之前调用,非生产环境下会打印一条警告(epicMiddleware.run(rootEpic) called before the middleware has been setup by redux...)。务必先createStore再run。

配置项收敛:createEpicMiddleware(options)

旧版中用于注入依赖、适配器等配置的可选参数,在 1.0.0 中收敛为createEpicMiddleware(options)的第一个且唯一一个参数(详见 docs/api/createEpicMiddleware.md):

interface Options<D = any> { dependencies?: D; } createEpicMiddleware<Input, Output, State, Dependencies>({ dependencies: { api: apiClient, logger }, });

源码(src/createEpicMiddleware.ts)中dependencies会在每个 Epic 被订阅时作为第三个参数传入:

const output$ = epic(action$, state$, options.dependencies!);

与 src/epic.ts 中定义的Epic签名完全对应:

(action$: Observable<Input>, state$: StateObservable<State>, dependencies: Dependencies): Observable<Output>

Adapters 已移除:用自定义根 Epic 实现等价转换

1.0.0 不再支持 adapter(用于把 RxJS Observable 转换成其他流库的适配层)。官方给出的等价方案是:在自定义根 Epic 内完成流转换。

例如,要把 Observable 转换为 Most.js 流、处理完再转回 Observable:

import most from 'most'; import { from } from 'rxjs'; // 一个基于 Most.js 的 combineEpics 实现 const combineEpics = (...epics) => (...args) => most.merge( ...epics.map(epic => epic(...args)), ); const rootEpic = (action$, state$, ...rest) => { const epic = combineEpics(epic1, epic2, ...etc); // action$ 和 state$ 由 Observable 转换为 Most.js 流 const output = epic( most.from(action$), most.from(state$), ...rest ); // 再把 Most.js 流转回 Observable return from(output); };

这一设计的合理性可以从标准combineEpics源码得到佐证:src/combineEpics.ts 本质上只是用 RxJS 的merge把多个 Epic 的输出流合并成一个流,并未对输入流做任何假设。既然 Epic 的输入是流、输出也是流,任何能完成“流→流”转换的适配逻辑,都能以组合函数的形式内联进根 Epic,adapter 因此不再是库的职责。

Epic 产出的动作现在由队列调度(queueScheduler)

1.0.0 中最微妙、也最影响行为的一项变更:中间件订阅根 Epic 并派发其产出的动作时,使用的是 RxJS 的queueScheduler。

队列调度的含义

简单来说:如果队列为空,动作照常立即发出;但如果某个动作同步触发了其他动作,后续动作会被排队,直到第一个动作的调用栈返回。用官方文档的例子理解:

const epic1 = (action$) => action$.pipe( ofType('FIRST'), mergeMap(() => of({ type: 'SECOND' }, { type: 'THIRD' })), ); const epic2 = (action$) => action$.pipe( ofType('SECOND'), map(() => ({ type: 'FOURTH' })), startWith({ type: 'FIRST' }), ); // 注意 epic2 排在 epic1 之后 const rootEpic = combineEpics(epic1, epic2);

旧版行为:reducer 收不到 FOURTH,动作序列为

FIRST SECOND THIRD

1.0.0行为:reducer 能在最后看到 FOURTH,动作序列为

FIRST SECOND THIRD FOURTH

原因在于:中间件订阅(setup)Epics 与动作发出被安排在同一个队列调度器上,且中间件总是先完成对所有 Epics 的订阅、再发出任何动作,所以 epic2 不会错过任何动作。此外,当一个 Epic 同步连续发出多个动作时,它们会严格按给定顺序连续发出,其他 Epic 无法插入中间——of({ type: 'SECOND' }, { type: 'THIRD' })中 THIRD 必然紧随 SECOND,而旧版中另一个监听 SECOND 的 Epic 可能在同一调用栈里抢先发出别的动作。

源码证据:专用的唯一队列调度器

src/createEpicMiddleware.ts 没有直接复用全局queueScheduler单例,而是通过其构造函数派生了一个独立实例,避免与库外 RxJS 代码共享同一队列:

const QueueScheduler: any = queueScheduler.constructor; const uniqueQueueScheduler: typeof queueScheduler = new QueueScheduler( (queueScheduler as any).schedulerActionCtor );

随后:

  • action$输出流observeOn(uniqueQueueScheduler);
  • state$输出流同样observeOn(uniqueQueueScheduler);
  • 每个 Epic 的输出流在合并进result$时,先subscribeOn(uniqueQueueScheduler)再observeOn(uniqueQueueScheduler),最后result$.subscribe(store.dispatch)。

测试(test/createEpicMiddleware-spec.ts 的should queue state$ updates与should not allow interference from the public queueScheduler singleton)专门验证了排队顺序以及"外部queueScheduler.schedule不能干扰中间件内部队列"这一隔离设计。

对绝大多数应用的影响

官方文档明确:在绝大多数情况下,这一变更没有可感知的影响;只有复杂、环状的 Epic 间通信顺序可能发生变化(且多数人认为新顺序更符合直觉)。若你的应用存在多个 Epic 相互触发、且依赖严格时序,迁移时请重点关注动作顺序测试。

epicMiddleware.replaceEpic已移除:用END动作 +run()替代

旧版replaceEpic用于热替换根 Epic。1.0.0 移除了该方法,官方推荐的等价做法是:

  1. 根 Epic 内部通过函数组合包一层takeUntil,监听自定义的END动作;
  2. 需要替换时,先store.dispatch({ type: 'END' })终止旧 Epic,再epicMiddleware.run(nextEpic)装载新 Epic。
// 根 Epic 使用函数组合加入 takeUntil: // 先组合各子 Epic,再以当前 action$ 调用它,对结果流施加 takeUntil const rootEpic = (action$, ...rest) => combineEpics(epic1, epic2, ...etc)(action$, ...rest).pipe( takeUntil(action$.pipe( ofType('END'), )), ); function replaceRootEpic(nextRootEpic) { store.dispatch({ type: 'END' }); epicMiddleware.run(nextRootEpic); }

之所以可行,正是前文所述的"多次run()会合并而非替换":旧 Epic 被END终止后,新 Epic 的加入不会导致旧流残留。这也让热重载(HMR)场景的实现变得清晰——替换逻辑完全掌握在应用层手中。

Epic 内禁止store.dispatch:改为通过返回流发出动作

旧版把store.dispatch()作为"逃生舱"开放给 Epic 内部使用,但实践中大量开发者滥用它,破坏了"Epic 通过返回的 Observable 发出动作"这一核心范式。1.0.0 干脆完全移除了该能力。

注意:这只针对 Epic内部调用store.dispatch。UI 组件或其他 redux-observable 之外的地方继续正常使用store.dispatch,不受影响。

迁移对照

旧写法(在副作用回调里直接 dispatch):

const somethingEpic = (action$) => action$ .ofType(SOMETHING) .switchMap(() => ajax('/something') .do(() => store.dispatch({ type: SOMETHING_ELSE })) .map((response) => ({ type: SUCCESS, response })), );

新写法(rxjx v6 pipeable 操作符,把额外动作合并进返回流):

// 现在使用 rxjs v6 pipeable 操作符 const somethingEpic = (action$) => action$.pipe( ofType(SOMETHING), switchMap(() => getJSON('/something').pipe( mergeMap((response) => of( { type: SOMETHING_ELSE }, { type: SUCCESS, response }, )), ), ), );

核心思路:一次副作用可能派生多个动作时,用mergeMap+of(...)把它们作为一个流返回,中间件会自动按序派发。ofType的实现在 src/operators.ts,它本质是对 RxJSfilter的封装,并带类型推导(Extract<Input, Action<Type>>),因此上述代码中ofType(SOMETHING)之后能获得类型收窄的动作对象。

访问状态:第二个参数升级为state$(StateObservable)

随着store.dispatch的移除,同时应需求演进,Epic 的第二个参数从store变为一个自定义的StateObservable,官方文档称其为state$。

命令式读取:state$.value

state$.value属性始终持有 Redux state 的最新值,等价于旧版store.getState():

// 旧写法 const fetchUserEpic = (action$, store) => action$ .ofType(FETCH_USER) .mergeMap((action) => ajax(`/users/${action.id}`, { Authorization: `Bearer ${store.getState().authToken}` }) .map((response) => fetchUserFulfilled(response)), );
// 新写法(同时改用 v6 pipe 操作符) const fetchUserEpic = (action$, state$) => action$.pipe( ofType(FETCH_USER), mergeMap(action => ajax(`/users/${action.id}`, { 'Authorization': `Bearer ${state$.value.authToken}` }).pipe( map(response => fetchUserFulfilled(response)) ) ) );

响应式组合:把state$并入流

state$本身是 Observable,可以像普通流一样组合。官方推荐在需要"响应状态变化"时用withLatestFrom之类的操作符:

const fetchUserEpic = (action$, state$) => action$.pipe( ofType(FETCH_USER), withLatestFrom(state$), mergeMap(([action, state]) => getJson(`/users/${action.id}`, { 'Authorization': `Bearer ${state.authToken}` }).pipe( map(respose => fetchUserFulfilled(response)) ) ) );

官方还给出了一个"基于状态变化自动保存"的示意(官方标注为 UNTESTED、仅展示思路):通过state$观察googleDocument字段变化,配合distinctUntilChanged、throttleTime、concatMap实现保存,而无需关心到底是哪些动作改动了该状态:

const autoSaveEpic = (action$, state$) => action$.pipe( ofType(AUTO_SAVE_ENABLE), exhaustMap(() => state$.pipe( pluck('googleDocument'), distinctUntilChanged(), throttleTime(500, { leading: false, trailing: true }), concatMap((googleDocument) => saveGoogleDoc(googleDocument).pipe( map(() => saveGoogleDocFulfilled()), catchError((e) => of(saveGoogleDocRejected(e))), ), ), takeUntil(action$.pipe(ofType(AUTO_SAVE_DISABLE))), ), ), );

官方也提示:多数场景下命令式的state$.value更简洁,因为大多数时候你并不需要响应状态变化,只是想在发起请求那一刻读取当前值。

StateObservable的底层实现

src/StateObservable.ts 的实现值得注意:

export class StateObservable<S> extends Observable<S> { value: S; private __notifier = new Subject<S>(); // ... constructor(input$: Observable<S>, initialState: S) { super((subscriber) => { const subscription = this.__notifier.subscribe(subscriber); if (subscription && !subscription.closed) { subscriber.next(this.value); } return subscription; }); this.value = initialState; input$.subscribe((value) => { // 只有当值真正变化时才更新并通知, // 因为 Redux 要求 reducer 遵循不可变更新模式, // 这等价于 distinctUntilChanged() if (value !== this.value) { this.value = value; this.__notifier.next(value); } }); } }

两个关键点:

  1. 订阅即推送当前值:任何订阅者在订阅时都会立即收到this.value,因此merge(state$, ...)这类用法在 Epic 启动时就能拿到当前状态。
  2. 引用相等去重:由于 Redux 约定 reducer 用不可变模式返回新引用,StateObservable只在value !== this.value时通知,相当于内置了一个极简的distinctUntilChanged,避免无意义的状态流推送。

中间件内部对state$的时序保证(src/createEpicMiddleware.ts):下游中间件(含 reducer)先处理动作、更新 state,然后中间件才依次stateSubject$.next(store.getState())与actionSubject$.next(action)——这保证了 Epic 收到动作时state$.value已经是该动作生效后的最新状态。对应测试should update state$ after an action goes through reducers but before epics验证了"reducer 先于 Epic 更新状态"这一顺序。

迁移清单速查

旧 API / 行为1.0.0 新方式关键依据
createEpicMiddleware(rootEpic)createEpicMiddleware()+epicMiddleware.run(rootEpic)src/createEpicMiddleware.ts 的显式报错
单次run替换 Epic多次run自动合并;配合END+takeUntil实现替换同上,epic$为Subject
适配器(adapters)在自定义根 Epic 内做流转换src/combineEpics.ts 只做merge
Epic 内store.dispatch通过返回流 +mergeMap/of发出动作见上文迁移对照
store.getState()state$.valuesrc/StateObservable.ts
链式操作符(RxJS v5)pipeable 操作符(RxJS v6)src/operators.ts 的ofType
Redux v3Redux v4本文"版本前提"一节

建议的迁移顺序:先升级 RxJS v6(可借助rxjs-compat过渡)→ 升级 Redux v4 → 调整中间件装配(run())→ 替换 Epic 内的store.dispatch与store.getState()→ 处理热重载/替换逻辑 → 最后运行测试重点核对动作顺序相关的用例。仓库内的测试套件(test/createEpicMiddleware-spec.ts、test/StateObservable-spec.ts)覆盖了上述绝大部分行为,迁移完成后可对照这些用例验证你的应用行为是否符合预期。

  • 前端

【免费下载链接】redux-observable

RxJS middleware for action side effects in Redux using "Epics"

项目地址:https://gitcode.com/gh_mirrors/re/redux-observable
点击查看免费下载
上一篇:终极Haskell学习指南:从零开始掌握函数式编程的10个技巧
下一篇:Crater服务监控告警:异常情况及时响应机制

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询