尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Sanity Studio 源码解析:RxJS 加载状态三大模式(startWith、withLoadingState 自定义算子与 scan 保留旧值)

Sanity Studio 源码解析:RxJS 加载状态三大模式(startWith、withLoadingState 自定义算子与 scan 保留旧值) Sanity Studio 源码解析RxJS 加载状态三大模式startWith、withLoadingState 自定义算子与 scan 保留旧值【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity本文以 Sanity 仓库中rxjs-like-a-pro技能参考文档 loading-state-patterns.md 为主体系统讲解异步数据流中加载状态loading/error/data 生命周期的三个核心模式在switchMap链内用startWith推导 loading 状态、抽取可复用的withLoadingState自定义算子、用scan在加载期间保留上一次的结果。读完本文你将掌握如何在 React RxJS 的 Studio 类应用中消除可变变量跟踪状态并且可以在 Sanity 源码中找到这三个模式的真实落地实现作为对照验证。一、核心问题不要用可变变量跟踪 loading 状态RxJS 中最常见的反模式之一是在订阅回调外维护可变状态let loading false这类标志位再手动翻转它。这种做法丢失了响应式链的可组合性无法重试、无法去抖、无法取消每加一个需求就多一处手工状态管理。该文档所属的技能总纲 SKILL.md 将这类过早订阅 订阅内做命令式工作列为头号反模式并明确指向本文档作为加载状态的正确解法For loading state, derive it inside the chain usingstartWith。正确思路是loading/error 状态不是被记录下来的而是从流本身推导出来的。二、模式一在 switchMap 链内用 startWith 推导 loading 状态文档给出的第一个模式是在switchMap的内部流上用startWith注入一个加载中哨兵值再用map和catchError把成功/失败映射成带loading标志的对象const dataWithLoadingState$ input$.pipe( switchMap((value) fetchData(value).pipe( map((data) ({loading: false, data})), catchError((error) of({loading: false, error})), startWith({loading: true}), ), ), )各算子的职责拆解switchMap当新输入到达时自动取消上一个内部可观察对象自动取消过期的请求这是只有最新输入有意义场景搜索框、路由变化的默认选择技能总纲中的扁平化算子对比表也推荐 UI/请求场景默认使用它map((data) ({loading: false, data}))把成功结果包装为已加载状态catchError((error) of({loading: false, error}))把错误降级为一条普通的错误状态输出——关键在于它放在内部流上外层流不会因此终止下一次输入到达时依然能继续工作这也是总纲中catchError 放在内层原则的直接体现startWith({loading: true})内部流建立后先同步发出一个{loading: true}哨兵使加载中成为流的第一次发射而不是某个外部变量的初始值。这个模式在 Sanity 源码中有近乎逐字对应的真实实现。以 useProjectOrganizationData.ts 为例它从项目 store 取出组织数据流并在链内推导出{value, loading}状态const INITIAL_STATE {value: null, loading: true} export function useProjectOrganizationData() { const projectStore useProjectStore() const obs$ useMemo( () projectStore.getOrganizationData().pipe( map((res) { return {value: res, loading: false} }), startWith({value: null, loading: true}), ), [projectStore], ) return useObservable(obs$, INITIAL_STATE) }可以看到map把成功结果映射为loading: falsestartWith注入初始的loading: true哨兵与文档模式一完全同构再配合react-rx的useObservable把流接入 React 渲染INITIAL_STATE只作为订阅前的渲染占位。三、模式二抽取可复用的 withLoadingState 自定义算子模式一写一次没问题但每个异步取数场景都复制一遍switchMap map catchError startWith四件套既啰嗦又容易各写各的。文档给出的第二个模式是把它抽成一个带类型安全约束的自定义算子import {OperatorFunction, Observable, of} from rxjs import {switchMap, map, catchError, startWith} from rxjs/operators type LoadingStateT | {loading: true} | {loading: false; data: T} | {loading: false; error: unknown} function withLoadingStateT, R( project: (value: T) ObservableR, ): OperatorFunctionT, LoadingStateR { return (source) source.pipe( switchMap((value) project(value).pipe( map((data) ({loading: false, data}) as const), catchError((error) of({loading: false, error} as const)), startWith({loading: true} as const), ), ), ) } // Now any stream can use it: const results$ searchInput$.pipe(withLoadingState((query) apiService.search(query)))这里有两个值得注意的设计点判别联合类型discriminated unionLoadingStateT用loading: true/loading: false data/loading: false error三个变体穷举了加载生命周期的全部可能类型系统保证了下游消费方要么处于加载中要么有数据要么有错误——不存在三样都有的非法状态。as const断言则让联合类型的每个分支被精确推导而不是宽化成{loading: boolean, ...}。高阶算子的签名withLoadingState本身不直接操作流而是接收一个项目函数输入值到内部可观察对象的映射并返回OperatorFunctionT, LoadingStateR因此它可以像内建算子一样插入任意.pipe()链。这与仓库中另一份参考文档 custom-operators.md 阐述的原则一致算子就是(source: ObservableA) ObservableB的函数重复的.pipe()链应当被提取成命名算子。文档特别强调了收益一旦模式进入算子它只被测试一次并且到处可复用loading/error/data 生命周期在每一个使用它的流上保证一致。Sanity 源码中同样存在这类把数据流包装成加载状态的自定义算子useLoadable.ts 定义了LoadableStateT三态LoadingState/LoadedStateT/ErrorState和自定义算子asLoadable/** internal */ export function asLoadableT(): OperatorFunctionT, LoadableStateT { return (value$: ObservableT) value$.pipe( map((value) ({isLoading: false, value, error: null}) as const), catchError((error): ObservableErrorState of({isLoading: false, value: undefined, error}), ), ) }其结构map 包装成功态 catchError 降级为错误态与withLoadingState的核心段落一一对应只是初始的isLoading: true态通过useLoadable中useObservable的initialValue参数提供而非链内startWith——两种写法在状态由流推导、而非可变变量这一点上完全一致也印证了这类状态包装算子是大型 Studio 应用中反复出现的标准构件。四、模式三用 scan 在加载期间保留上一次结果switchMap有一个副作用新输入到达时它会取消上一个内部流并以{loading: true}重新开始——这意味着在加载阶段 UI 会丢失上一次的结果用户看到的是空白或转圈而不是他刚刚还在查看的旧但仍有价值的数据。文档给出的第三个模式是用scan把上一次的结果携带过来const results$ searchInput$.pipe( withLoadingState((query) apiService.search(query)), scan((previous, current) { if (current.loading) { // Keep showing previous data while loading return {...current, data: data in previous ? previous.data : undefined} } if (error in current) { // On error, keep the previous data so the UI doesnt blank out, // but surface the error so it can be displayed return {...current, data: data in previous ? previous.data : undefined} } return current }), )scan的 reducer 对每种状态转移做显式决策加载中current.loading返回新状态但把previous.data补进去——UI 可以同时显示加载指示器和上一份结果直到新数据到达错误error in current错误照常向下游发出以供展示同时保留上一份成功数据避免一次瞬时网络错误就把用户正在看的完好数据整个抹掉的糟糕体验成功原样透传用全新数据替换一切。文档还指出同一个scan模式适用于任何跨发射记忆某个东西的场景累积列表、跟踪累计值、保留否则会随流进入下一状态而丢失的上下文。这个scan 保留旧值的思路在 Sanity 源码中有一个非常接近的工程化实现createHookFromObservableFactory.ts 用于把可观察对象工厂自动转成返回[值, 是否加载中]二元组的 React Hook。其核心管道如下第 47–64 行of(arg).pipe( switchMap((_arg) concat( of({type: loading} as const), observableFactory(_arg).pipe(map((value) ({type: value, value}) as const)), ), ), scan(([prevValue], next): LoadingTupleT | undefined { if (next.type loading) return [prevValue, true] return [next.value, false] }), // ... )对照可以发现两条实现路径的等价性文档用startWith({loading: true})在内部流头部注入加载哨兵这里改用concat(of({type: loading}), 实际流)达到同样效果而scan(([prevValue], next) ...)在收到loading标记时返回[prevValue, true]——正是加载时保留上一次值、同时把 loading 置真的文档模式。区别在于这里用元组[值, 布尔]而非判别联合对象表达状态联合类型表达力更强能区分 data/error元组则更轻量值域上天然保留旧值无需in判断。两者是同一思想在不同 API 约束下的形态。仓库里还有一类与模式三精神相通、但机制不同的构件值得了解rxSwr.ts 实现的createSWR算子Stale-While-Revalidate。它用concat(defer(缓存命中则同步发出旧值), 上游流 tap 写缓存)让新订阅者先拿到缓存里的 stale 值、再等待新值到达return concat( defer(() (cache.has(key) ? of({fromCache: true, value: cache.get(key)}) : EMPTY)), input$.pipe( tap((result) cache.set(key, result)), map((value) ({fromCache: false, value})), ), )scan保留的是同一条流上上一次发射的值记忆窗口 流自身的历史SWR 保留的是跨订阅、由 LRU 缓存提供的历史值记忆窗口 缓存。两者解决的都是避免 UI 在重新取数期间变白屏的问题选型时可以按需取用。五、三个模式的组合与验证建议把三个模式串起来一个完整的、带状态保留的搜索流形态如下输入流先做稳定化去抖、去重、过滤空值可参考 custom-operators.md 中的stabilizeInput算子withLoadingState(projectFn)派生LoadingStateR追加scanreducer 保留旧数据末端用tap消费UI 更新、日志等副作用都在链内.subscribe()只负责激活流——这也是总纲中的通用约定。测试与验证建议由于模式本身是算子测试一次、处处受益成立只需对withLoadingState写一组基于时间控制如VirtualTimeScheduler/TestScheduler的发射序列断言——输入发射后先收到{loading: true}、成功路径收到{loading: false, data}、失败路径收到{loading: false, error}且外层流仍存活scan层再单独验证加载/错误时 data 回退到上一次的语义Sanity 仓库内可参考的验证风格见 createHookFromObservableFactory 的测试其中使用了TestScheduler与自定义发射序列匹配器如 toMatchEmissions来断言流的完整发射时间线。六、小结loading-state-patterns.md 给出的三个模式构成了一条清晰的演进路线模式解决的问题关键算子仓库中的真实对照链内推导 loading消除可变标志位switchMapmapcatchErrorstartWithuseProjectOrganizationData.tswithLoadingState算子模式一致性与单点测试高阶算子 判别联合类型useLoadable.ts 的asLoadablescan保留旧值加载/错误期间 UI 不白屏scan携带previouscreateHookFromObservableFactory.ts、rxSwr.ts核心结论只有一句话loading/error/data 是流的形状不是外部变量——把它留在.pipe()里用算子固定下来状态的生命周期就永远不会散架。【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表