ARTICLE DETAIL

资讯详情

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

Relay 18 缓存复用指南:深入解析 Fetch Policies 四种取值与 store 数据可用性判定

Relay 18 缓存复用指南:深入解析 Fetch Policies 四种取值与 store 数据可用性判定 前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载Relay 在应用运行过程中会把多次查询获取到的数据缓存到本地 store 中。为了让页面在切换 Tab、返回已访问过的帖子详情页时能立即渲染、跳过网络等待我们需要借助fetchPolicy明确告诉 Relay 何时该命中本地缓存、何时该发起网络请求。本文基于仓库内 v18.0.0 版本文档 fetch-policies.md 展开结合react-relay与relay-runtime的源码实现讲清四种 fetch policy 的行为差异、store数据可用性的判定机制以及与之配套的数据保留、失效与部分渲染方案读完即可在真实项目中落地配置。复用缓存的第一步把fetchPolicy传给loadQuery复用本地缓存数据的第一步是向loadQuery函数传入一个fetchPolicy。loadQuery通常由useQueryLoaderHook 提供完整的调用关系在 Fetching Queries 章节 中有详细介绍。下面是一个完整的示例const React require(React); const {graphql} require(react-relay); function AppTabs() { const [ queryRef, loadQuery, ] useQueryLoader(HomeTabQuery); const onSelectHomeTab () { loadQuery({id: 4}, {fetchPolicy: store-or-network}); } // ... }传入的fetchPolicy将决定两件事是否应该从本地缓存store中满足查询根据该查询的数据在 store 中的 可用性是否需要发起网络请求从服务端获取查询结果。从源码结构看useQueryLoader内部packages/react-relay/relay-hooks/useQueryLoader.js会把options中的fetchPolicy、networkCacheConfig透传给真正的 loadQuery.js 实现loadQuery在每次调用时会生成新的fetchKey保证同一个查询在多次调用时会被独立评估避免 Suspense 缓存误复用旧的查询引用见 loadQuery.js。四种 Fetch Policy 的完整语义默认情况下Relay 会先尝试从本地缓存读取查询如果该查询的任何数据 缺失 或 过期它会从网络获取整个查询。这个默认策略就叫store-or-network。具体来说fetchPolicy可以是以下四种取值之一取值复用本地缓存发起网络请求适用场景store-or-network默认会仅当查询有数据缺失或过期时才发起若查询已完全缓存则不发起大多数场景能复用缓存就复用必要时才回源store-and-network会始终发起无论 store 中数据是否缺失或过期需要即时刷新且不想等待先用缓存渲染同时后台更新network-only不会始终发起完全忽略本地缓存及其缺失/过期状态对数据新鲜度要求极高、必须走后端最新数据store-only只会永不发起网络请求纯本地数据如 client-only 数据读取与操作或由调用方自行负责拉取这四种取值的类型定义可以在 packages/relay-runtime/util/RelayRuntimeTypes.js 中找到export type FetchQueryFetchPolicy store-or-network | network-only; export type FetchPolicy FetchQueryFetchPolicy | store-and-network | store-only;注意Refetching 章节 中讨论的refetch函数同样接受一个fetchPolicy参数因此上述四种策略在重新获取不同数据的场景下同样适用。源码中的关键实现store 检查与网络请求的取舍在 packages/react-relay/relay-hooks/loadQuery.js 的checkAvailabilityAndExecute中可以清楚地看到这四种策略的分流逻辑const shouldFetch fetchPolicy ! store-or-network || environment.check(operation).status ! available;也就是说当策略不是store-or-network时shouldFetch恒为true一定会走executeDeduped发起网络执行当策略是store-or-network时才真正调用environment.check(operation)检查 store 中数据是否available可用不可用才发起请求。这里environment.check的返回结果就是 store 对一次查询的可用性判定。该类型在 packages/relay-runtime/store/RelayStoreTypes.js 中定义export type OperationAvailability | {status: available, fetchTime: ?number} | {status: stale} | {status: missing};三种状态含义如下available查询所需的全部本地数据都存在且未过期可以直接从 store 渲染不需要网络请求missing查询有部分数据在 store 中缺失必须发起网络请求stale数据存在但已被标记为过期例如记录被显式失效或超过查询缓存过期时间需要重新获取。在 RelayModernEnvironment.check 中如果配置了missingFieldHandlers或查询涉及客户端抽象类型则会走_checkSelectorAndHandleMissingFields先通过 missing field handlers 尝试补齐缺失字段再最终判定否则直接委托给this._store.check(operation)。这就是为什么数据缺失的判定并不仅仅是看 store 里有没有还受缺失字段处理器影响。store 中数据可用性由什么决定fetch policy 的行为取决于查询评估那一刻 store 中数据的可用性。可用性由两个因素决定数据的存在性presence与数据的过期性staleness详见 availability-of-data.md。数据的存在性与垃圾回收要复用 store 中的缓存数据首先要理解这些数据的生命周期数据是否存在于 store 中、能存在多久。一般地一个查询在首次被获取之后只要它还在屏幕上被渲染其数据就会存在于 store 中如果某个查询从未被获取过那它的数据自然就是缺失的。但应用运行越久累积的数据会越来越大、越来越旧Relay 不能无限期地在内存中保留所有已获取的数据。为此 Relay 运行一个名为垃圾回收Garbage Collection的机制删除不再被任何组件引用的数据。这本身与复用缓存存在张力数据过早被删除后续再想复用就得重新等网络请求。好在通常不需要自己操心 GC 与数据保留的配置——应用基础设施会在RelayEnvironment层配置好——但理解它有助于排查缓存复用失效的问题。查询保留Query Retention是控制 GC 的关键保留一个查询意味着告诉 Relay该查询及其变量对应的数据不应被删除。多个调用方可以同时保留同一个查询只要还有至少一个调用方在保留数据就不会被 GC。默认情况下使用useQueryLoader/usePreloadedQuery等 API 的查询组件在挂载期间会保留查询卸载后即释放数据随时可能被回收。若想在组件生命周期之外保留数据可以使用environment.retain()// 保留查询这会阻止该查询及其变量的数据被 Relay 垃圾回收 const disposable environment.retain(queryDescriptor); // 释放 disposable 将释放该查询及变量的数据 // 之后若没有其他方保留数据随时可能被 GC 删除 disposable.dispose();控制垃圾回收的两个 Store 配置Relay Store 提供两个选项来控制 GC 行为在 RelayModernStore 构造函数 中均有对应字段1.gcScheduler—— 决定何时调度一次 GC 执行// 示例调度函数接受一个回调并安排在未来的某个时间执行 function gcScheduler(run: () void) { resolveImmediate(run); } const store new Store(source, {gcScheduler});若不提供Relay 默认使用resolveImmediate调度 GC源码见 RelayModernStore.js可提供自定义调度函数让 GC 不那么激进例如基于时间或 React scheduler 优先级等启发式策略。按约定实现不应立即执行回调。2.gcReleaseBufferSize—— 控制 release buffer 大小。Relay Store 内部持有一个释放缓冲区在查询被原持有者释放后默认即组件卸载时仍临时保留指定数量的查询使返回之前访问过的页面/Tab 时更有可能复用数据const store new Store(source, {gcReleaseBufferSize: 10});缓冲区大小为 0 等价于没有释放缓冲区查询会被立即释放并回收默认环境下的释放缓冲区大小为 10。源码中 RelayModernStore.js 使用options?.gcReleaseBufferSize ?? DEFAULT_RELEASE_BUFFER_SIZE读取该配置_pushToReleaseBuffer在缓冲区满时挤出最早的根并调度 GCRelayModernStore.js。数据过期性显式失效与查询缓存过期时间假设数据存在于 store 中还需要考虑其过期性。默认情况下无论数据在缓存中待了多久Relay 都不会认为它过期——除非它被显式标记为过期或者超过了查询缓存过期时间query cache expiration time。全局失效整个 store调用invalidateStore()会使失效之前写入的所有数据都变为过期状态下次评估时需要重新获取function updater(store) { store.invalidateStore(); }invalidateStore可以在 mutation、subscription 或本地 store 更新的 updater 中调用。按记录失效也可以只失效 store 中的特定记录只有引用了这些记录的查询会被视为过期function updater(store) { const user store.get(id); if (user ! null) { user.invalidateRecord(); } }订阅失效事件标记为过期只会在下次评估时触发重新获取。若希望数据失效时立即重新获取例如当前页面正在展示的数据、或从未卸载的前一个视图的数据可以用useSubscribeToInvalidationStateHookfunction ProfilePage(props) { const data usePreloadedQuery( graphql..., props.preloadedQuery, ) // 订阅指定用户 ID 的失效状态变化 // 每当该记录被标记为过期时回调触发 useSubscribeToInvalidationState([props.userID], () { // 在这里可以 // - 传入新的 preloadedQuery 给 usePreloadedQuery 重新评估查询 // - 命令式地重新获取数据 // - 渲染 loading 提示或置灰页面以表示正在刷新 }) return (...); }查询缓存过期时间另一个影响过期判定的因素是queryCacheExpirationTime。一个查询如果可以用 store 中的记录满足且满足以下任一条件则被视为过期距上次获取的时间超过了查询缓存过期时间其引用的记录中至少有一条被失效过。该过期检查发生在新的请求发起时例如调用loadQuery。引用了过期数据的组件仍能继续渲染这些数据但任何会被过期数据满足的新请求都会走向网络。配置方式const store new Store(source, {queryCacheExpirationTime: 5 * 60 * 1000 });如果不提供该配置过期检查只会看引用的记录是否被失效过。源码中 getAvailabilityStatus 实现了这一逻辑当operationFetchTime与queryCacheExpirationTime均存在且operationFetchTime Date.now() - queryCacheExpirationTime时返回stale。部分缓存数据下的渲染renderPolicy 与 Suspense当使用允许复用缓存的策略store-or-network或store-and-network渲染一个部分缓存的查询时Relay 支持部分渲染立即渲染已缓存的部分而不是等整个查询全部获取完成。关键机制在于以 Fragment 作为部分渲染的边界。fragment 组件在其本地声明的数据缺失且正在获取时会挂起suspend直到其所属的父查询被获取完成。而 Relay 判定数据缺失时只看本地声明的字段——被 fragment spread 引用的数据缺失不会导致外层查询被判定为缺失。因此即使某个 fragment 的数据还没到外层查询已缓存的部分依然可以先行渲染只需用Suspense包住缺失数据的 fragment 组件即可function HomeTab() { const data usePreloadedQuery( graphql query AppQuery($id: ID!) { user(id: $id) { name ...UsernameComponent_user } } , props.queryRef, ); return ( h1{data.user?.name}/h1 {/* 用 Suspense 包裹 UsernameComponent 即使 username 缺失也可以先渲染 App 的其他部分 */} Suspense fallback{LoadingSpinner labelFetching username /} UsernameComponent user{data.user} / /Suspense / ); }嵌套 fragment 的处理方式相同只要某 fragment 所需数据已缓存就能渲染其子 fragment 数据缺失时用Suspense兜底即可。这种能力可以让我们完全跳过 loading 状态渲染出更接近最终形态的中间 UI相关完整示例与讨论见 rendering-partially-cached-data.md。让不同查询共享缓存missingFieldHandlers默认情况下Relay 只能识别完全相同的查询之间的缓存复用同一个查询获取两次第二次评估时就知道数据已缓存。但不同查询可能指向同一份数据例如# Query 1 query UserQuery { user(id: 4) { name } } # Query 2 query NodeQuery { node(id: 4) { ... on User { name } } }这两个查询不同但引用的是完全相同的数据。Relay 默认不知道node(id: 4)与user(id: 4)指向同一对象需要通过在RelayEnvironment上提供missingFieldHandlers来编码这种等价关系const {ROOT_TYPE, Environment} require(relay-runtime); const missingFieldHandlers [ { handle(field, record, argValues): ?string { // 为 node 字段添加处理器 if ( record ! null record.getType() ROOT_TYPE field.name node argValues.hasOwnProperty(id) ) { return argValues.id } if ( record ! null record.getType() ROOT_TYPE field.name user argValues.hasOwnProperty(id) ) { // 如果字段是 user(id: $id)按 $id 的值查找记录 return argValues.id; } if ( record ! null record.getType() ROOT_TYPE field.name story argValues.hasOwnProperty(story_id) ) { // 如果字段是 story(story_id: $story_id)按 story_id 值查找 return argValues.story_id; } return undefined; }, kind: linked, }, ]; const environment new Environment({/*...*/, missingFieldHandlers});要点missingFieldHandlers是一个handler 数组每个 handler 必须包含一个handle函数以及它能处理的缺失字段的kind。主要处理两类字段scalar包含标量值数字、字符串等的字段linked引用另一个对象非标量的字段。handle函数接收缺失的字段、该字段所属的记录、以及本次查询执行中传给该字段的参数处理scalar字段时返回一个标量值作为缺失字段的值处理linked字段时返回一个ID指向 store 中应替代该缺失字段的另一个对象。当 Relay 尝试从本地缓存满足查询并检测到缺失数据时会在最终判定缺失之前先运行所有与字段类型匹配的缺失字段处理器。详细讨论见 filling-in-missing-data.md。从这里可以看到fetch policy 判定数据缺失的最终结果实际上是 store 检查environment.check叠加 missing field handlers 补齐之后的综合结论。实践建议与完整决策链路综合以上内容在实际项目中选择 fetch policy 时可以参考以下思路大多数页面使用默认的store-or-network能复用缓存就复用有缺失/过期才回源兼顾速度与新鲜度需要即时展示且允许后台刷新的场景使用store-and-network先用缓存秒开同时后台拉取最新数据覆盖对新鲜度有硬性要求如支付状态、权限变更使用network-only跳过 store 直接走网络避免展示过期数据纯本地数据client-only、本地更新产生的数据使用store-only永不发网络请求数据保留与 GC依赖默认的 release buffer默认 10 条让返回过的页面快速复用必要时用environment.retain()延长关键数据的存活时间需要更长时间保留数据时可通过queryCacheExpirationTime与gcScheduler调整策略跨查询复用当不同查询指向同一数据对象时配置missingFieldHandlers让 store 检查能识别等价字段从而提升store-or-network的缓存命中率渲染体验结合 fragment 边界与Suspense在部分数据缺失时先渲染已缓存部分避免整页 loading。最终一次loadQuery(..., {fetchPolicy})的完整决策链路可以概括为loadQuery 收到 fetchPolicy ├─ store-only直接返回不发网络请求 ├─ store-and-network / network-only必定发起网络请求 └─ store-or-networkenvironment.check(operation) ├─ available直接用 store 数据渲染 ├─ stale视为需要重新获取发起网络请求 └─ missing先经 missingFieldHandlers 补齐尝试 └─ 仍缺失 → 发起网络请求理解这条链路你就能准确预判每种策略下用户会看到缓存数据、loading 状态还是最新数据从而把 Relay 的缓存能力真正用起来。赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay 的 Fetch Policies数据获取策略完整指南从 store-or-network 到 store-only 的缓存复用实战Relay 的 Fetch Policies数据获取策略完整指南从 store or network 到 store only 的缓存复用实战 导读 本文前端开发工具Relay 缓存数据复用实战指南Fetch Policy、数据可用性与垃圾回收全解析Relay 缓存数据复用实战指南Fetch Policy、数据可用性与垃圾回收全解析 在 Relay 驱动的数据型 React 应用中随着用户不断使用Re前端开发工具Relay Fetch Policy 完全指南用 loadQuery 与四种 fetchPolicy 精准控制缓存复用与网络请求Relay Fetch Policy 完全指南用 loadQuery 与四种 fetchPolicy 精准控制缓存复用与网络请求 Relay 的数据获取是缓前端开发工具上一篇3步彻底解决Windows桌面混乱NoFences免费开源桌面分区工具完全指南下一篇AutoDock Vina终极指南快速掌握分子对接与虚拟筛选技术创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表