ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Relay 缓存复用完全指南:fetchPolicy、数据可用性与部分渲染实战

Relay 缓存复用完全指南:fetchPolicy、数据可用性与部分渲染实战 Relay 缓存复用完全指南fetchPolicy、数据可用性与部分渲染实战【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relayRelay 在应用运行过程中会把多次查询获取到的数据缓存在本地内存 Store 中而复用缓存数据Reusing Cached Data讨论的正是如何在发起网络请求之前优先复用这些本地已缓存数据从而让已访问过的页面、Tab 或内容实现秒开。本文基于 Relay 官方 guided-tour 中 reusing-cached-data 章节含 fetch policies、数据存在性与陈旧性、垃圾回收、缺失数据处理、部分缓存渲染等子主题并结合relay-runtime与react-relay的源码实现系统讲解如何用fetchPolicy控制缓存与网络的取舍、如何判断数据是否可用、如何通过保留机制和失效机制管理缓存生命周期以及如何实现能渲染多少先渲染多少的部分渲染。读完本文你将能在自己的 Relay 应用中把缓存复用的能力用到极致。为什么需要复用缓存数据Relay 在应用运行期间会为多次查询积累数据并在内存中缓存一段时间。当我们再次执行某条查询时理想情况下应当直接复用本地缓存并立即渲染而不是重新等待一次网络请求。这正是本主题要解决的问题见 introduction.md。典型场景包括Tab 切换应用中每个 Tab 都渲染一条查询。若某个 Tab 已被访问过再次切回时应立即渲染无需等待网络请求重新拉取已获取过的数据。从 Feed 跳转到详情页一条帖子已在 Feed 中渲染过进入其 permalink 页面时应能立即渲染因为帖子的数据基本都已在缓存中。部分数据缺失的情况即使 permalink 页面比 Feed 需要更多数据我们也希望能尽量复用本地已有的那部分数据并立即渲染而不是因为缺少一小块数据就阻塞整个页面的渲染。这些场景背后有两个核心问题本地缓存里有没有这份数据存在性 陈旧性以及如何让 Relay 知道该复用哪些缓存fetch policy 与缺失字段处理。第一步用 fetchPolicy 控制缓存与网络的取舍复用本地缓存数据的第一步是向loadQuery函数传入fetchPolicy。loadQuery由useQueryLoader提供详见 guided-tour 中关于查询渲染与加载的章节典型用法如下源自 fetch-policies.mdconst React require(React); const {graphql} require(react-relay); function AppTabs() { const [ queryRef, loadQuery, ] useQueryLoaderHomeTabQueryType(HomeTabQuery); const onSelectHomeTab () { loadQuery({id: 4}, {fetchPolicy: store-or-network}); } // ... }fetchPolicy决定了两件事是否从本地缓存满足查询是否发起网络请求从服务端拉取数据——具体取决于该查询数据在 Store 中的可用性数据是否存在、是否陈旧。默认情况下Relay 会先尝试从本地缓存读取查询只要该查询有任何数据缺失或陈旧就整条从网络获取。这个默认策略被称为store-or-network。fetchPolicy共支持以下四种取值fetchPolicy是否复用本地缓存是否发起网络请求适用场景store-or-network默认复用仅当数据缺失或陈旧时缓存完整则零网络请求是日常首选store-and-network复用总是发起既想立即展示缓存内容又希望后台刷新到最新network-only不复用总是发起强制走网络忽略本地缓存及其状态store-only复用永不发起纯本地数据场景取数职责交由调用方值得说明的是store-only策略还会配合数据完全本地化的使用场景——即通过本地更新local data updates写入并读取不经过服务端的客户端数据。此外refetching 相关章节讨论的refetch函数同样接收fetchPolicy参数可用于按需刷新局部数据。从源码看fetchPolicy的合法值在类型层面被严格约束。ReactRelayTypes.d.ts 中定义了PreloadFetchPolicy store-or-network | store-and-network | network-only而 QueryResource.js 中的getDefaultFetchPolicy展示了默认策略的分支逻辑live query 使用专门的 live 默认策略当请求既没有id也没有text即纯客户端操作时默认退化为store-only其余情况才使用DEFAULT_FETCH_POLICY即store-or-network。这印证了文档中默认 store-or-network的说法也解释了为什么纯本地查询天然不会发网络请求。数据可用性存在性 × 陈旧性fetch policy 的行为取决于查询被求值时 Store 中数据的可用性availability-of-data.md。数据的可用性由两个因素共同决定数据的存在性Presence这条查询的数据是否已经写入 Store以及能保留多久数据的陈旧性Staleness数据虽然存在但是否已被标记为过期而需要重新获取。只有在数据既存在又不陈旧时Relay 才会认为它可以直接满足该查询。接下来分别深入这两个因素。数据存在性查询保留与垃圾回收GC想复用缓存数据首先得理解缓存数据的生命周期presence-of-data.md一条查询在首次被获取之后只要它仍被屏幕上的组件渲染着其数据通常会一直存在于 Store 中如果从未获取过某条查询它的数据自然缺失。但应用不可能无限期保留所有查询的数据——内存会膨胀数据也会越来越陈旧。为此Relay 会运行一个名为Garbage Collection垃圾回收的过程删除不再被任何组件引用的数据。这与缓存复用存在天然的张力数据被过早回收再次访问时就得等待网络。下面介绍如何按需控制数据的保留与回收。Query Retention手动保留查询Retain保留一条查询是告诉 Relay该查询及其变量的数据不应被垃圾回收。多个调用方可以同时保留同一条查询只要至少有一个保留者存在这条查询就不会被删除。默认行为下使用useQueryLoader/usePreloadedQuery等 API 的查询组件会在挂载期间保留其查询组件卸载后即释放此后该查询随时可能被回收。若想在组件生命周期之外保留查询可以调用environment.retain// 保留查询这能防止该查询及其变量的数据被 Relay 垃圾回收 const disposable environment.retain(queryDescriptor); // 调用 dispose() 释放数据若没有其他保留者 // 该数据之后随时可能被 Relay 的 GC 删除 disposable.dispose();这样即使查询组件已经卸载数据仍可被其他组件或未来重新挂载的同一组件复用。从源码看RelayModernStore.js 的retain(operation)内部通过rootEntry.refCount引用计数实现每调用一次retain引用计数加一dispose时递减计数归零才进入可释放状态。Disposable类型则定义在 RelayRuntimeTypes.js 中是所有需要显式释放资源的通用接口约定。控制 GC 的两个配置项Relay Store 目前提供两个选项来控制垃圾回收行为1. GC SchedulergcSchedulergcScheduler是一个函数决定何时调度一次 GC 执行// 示例调度器接收回调并在未来某个时刻运行它 function gcScheduler(run: () void) { resolveImmediate(run); } const store new Store(source, {gcScheduler});默认情况下未提供gcScheduler时Relay 使用resolveImmediate函数调度 GC。可以提供自定义调度器让 GC 不像默认那样激进例如基于时间、基于 React scheduler 优先级或其他启发式策略。按照约定实现不应立即同步执行回调。2. GC Release Buffer SizegcReleaseBufferSizeStore 内部持有一个释放缓冲区即使查询已被其原始持有者释放组件卸载时的默认行为也会被临时多保留一段时间。这使得返回之前访问过的页面、Tab 或内容时更有可能复用数据。const store new Store(source, {gcReleaseBufferSize: 10});缓冲区大小为 0 等价于没有释放缓冲区查询会被立即释放并回收默认情况下环境Environment的释放缓冲区大小为10。这一点在源码中有直接对应RelayModernStore.js 定义了DEFAULT_RELEASE_BUFFER_SIZE 10并在构造函数中通过options?.gcReleaseBufferSize ?? DEFAULT_RELEASE_BUFFER_SIZE应用默认值第 169-170 行GC 执行时只有当_releaseBuffer.length未达到上限时才把释放的查询送入缓冲区第 618-619 行、第 644 行。另外文档与代码都强调通常你不需要手动配置 GC 与数据保留这部分应由应用基础设施在 RelayEnvironment 层面统一配置此处仅为参考。数据陈旧性失效标记与缓存过期时间即便数据存在仍需考虑其陈旧性staleness-of-data.md。默认情况下Relay不认为Store 中的数据陈旧——无论缓存了多久——除非它被数据失效 API 显式标记为陈旧或超过查询缓存过期时间。标记数据为陈旧适用于我们明确知道某些数据已不新鲜的场景例如执行了一次 Mutation 之后。Relay 提供以下 API 在 Store 更新过程中标记数据全局失效invalidateStore()最粗粒度的失效方式是把整个 Store作废——失效后所有已缓存数据都被视为陈旧。在 updater 函数中调用function updater(store) { store.invalidateStore(); }调用后所有在失效发生前写入 Store 的数据都被视为陈旧下次求值时相关查询需要重新获取。updater 函数既可以出现在 mutation、subscription 中也可以出现在纯本地 Store 更新中。按记录失效invalidateRecord()更细粒度的方式是只作废特定记录相比全局失效只有引用了被作废记录的查询才会被视为陈旧。在 updater 中调用function updater(store) { const user store.get(id); if (user ! null) { user.invalidateRecord(); } }对被作废的user记录任何缓存中引用它的查询都会被标记为陈旧下次求值时需重新获取。订阅失效事件useSubscribeToInvalidationState仅标记陈旧只会在查询下一次被求值时触发重取。但在某些场景下我们希望在失效发生时立即刷新失效的数据已经显示在当前页面上没有发生导航当前页的查询不会被重新求值即使数据已陈旧也不会立即重取失效的数据渲染在一个从未卸载过的旧视图上切回时该视图的查询同样不会被重新求值。为此 Relay 提供了useSubscribeToInvalidationStatehookfunction ProfilePage(props) { // 示例查询当前页面某个用户的数据 const data usePreloadedQuery( graphql..., props.preloadedQuery, ) // 订阅指定用户 ID 的失效状态变化 // 只要该 ID 对应的记录被标记为陈旧回调就会执行 useSubscribeToInvalidationState([props.userID], () { // 在这里可以 // - 传入新的 preloadedQuery 给 usePreloadedQuery 来重新求值查询 // - 命令式地重新获取数据 // - 渲染 loading spinner 或置灰页面提示正在刷新 }) return (...); }useSubscribeToInvalidationState接收一个 id 数组和一个回调只要数组中任一 id 对应的记录被标记为陈旧回调就会触发。在回调中我们可以按需重取或更新正在渲染陈旧数据的视图。例如把preloadedQuery存在 state 中并在回调里更新它从而重新执行顶层的usePreloadedQuery由于此时查询已陈旧即使数据在 Store 中也会被重新获取。查询缓存过期时间queryCacheExpirationTime此外查询缓存过期时间也决定了某次操作查询 变量能否用 Store 中已有数据满足。一条查询被视为陈旧当且仅当它可以用 Store 中的记录满足且满足以下任一条件它最后被获取的时间距今已超过查询缓存过期时间或者它引用的至少一个记录被作废。这种陈旧性检查发生在新的请求发起时例如调用loadQuery。组件仍可继续渲染引用陈旧数据的界面但任何会用陈旧数据满足的新请求都会改走网络。配置方式是在创建 Relay Store 时传入queryCacheExpirationTimeconst store new Store(source, {queryCacheExpirationTime: 5 * 60 * 1000 });如果未提供该选项陈旧性检查只考虑引用的记录是否被作废。源码层面RelayModernStore.js 的getAvailabilityStatus完整实现了上述判定若操作引用的记录存在mostRecentlyInvalidatedAt且操作从未写入或写入时间早于最近一次失效时间则返回stale若数据缺失则返回missing若operationFetchTime与queryCacheExpirationTime均已配置且operationFetchTime Date.now() - queryCacheExpirationTime同样返回stale否则才返回available。可以看到存在 未失效 未过期是缓存可复用的完整条件集。填补缺失数据missingFieldHandlers前面讨论的都是同一条查询的缓存复用——Relay 能自动识别完全相同的查询被再次求值时的缓存命中。但不同的查询也可能指向同一份数据此时 Relay 默认并不知道这一点需要显式配置filling-in-missing-data.md。例如下面两条查询引用的其实是同一份数据// Query 1 query UserQuery { user(id: 4) { name } } // Query 2 query NodeQuery { node(id: 4) { ... on User { name } } }这两条查询不同但指向完全相同的记录。理想情况下只要其中一条已被缓存渲染另一条时就应该能复用。Relay 默认不具备这种知识需要我们通过missingFieldHandlers告诉它node(id: 4)等价于user(id: 4)。在创建RelayEnvironment时传入missingFieldHandlersconst {ROOT_TYPE, Environment} require(relay-runtime); const missingFieldHandlers [ { handle(field, record, argValues): ?string { if ( record ! null record.__typename ROOT_TYPE field.name user argValues.hasOwnProperty(id) ) { // 如果字段是 user(id: $id)用 $id 的值查找记录 return argValues.id; } if ( record ! null record.__typename 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函数并声明它能处理的缺失字段类型。主要有两种类型scalar字段包含标量值例如数字或字符串linked字段引用另一个对象即非标量。handle函数接收缺失的字段、该字段所属的记录以及当前查询执行时传给该字段的参数。处理scalar字段时handle应返回一个标量值作为缺失字段的值处理linked字段时handle应返回一个ID指向 Store 中应被用作替代的另一个对象。当 Relay 尝试从本地缓存满足查询、检测到缺失数据时会先运行所有与字段类型匹配的 handler然后才最终判定数据是否缺失。正是这套机制让不同查询、同一数据的跨查询复用成为可能。渲染部分缓存的数据片段边界 Suspense除了复用完整缓存Relay 还支持部分渲染Partial Rendering立即渲染部分被缓存的查询缺失部分照常等待网络rendering-partially-cached-data.md。这在想尽快画出页面、且确信部分数据已缓存时非常有用——例如个人资料页用户name很可能早就在应用使用过程中被缓存访问资料页时即使其余数据尚未就绪也希望立刻渲染出名字跳过 loading 状态。片段组件作为部分渲染的边界部分渲染依赖片段组件**挂起suspend**的能力参见 guided-tour 中关于 Suspense 加载状态的章节如果片段组件本地声明的任何数据在渲染时缺失且正在被获取该组件就会挂起直到所属父查询的数据到达。结合示例理解。假设有一个片段组件/** * UsernameComponent.react.js * * Fragment Component */ import type {UsernameComponent_user$key} from UsernameComponent_user.graphql; const React require(React); const {graphql, useFragment} require(react-relay); type Props { user: UsernameComponent_user$key, }; function UsernameComponent(props: Props) { const user useFragment( graphql fragment UsernameComponent_user on User { username } , props.user, ); return (...); } module.exports UsernameComponent;以及如下查询组件它查询部分数据并包含上述片段/** * HomeTab.react.js * * Query Component */ const React require(React); const {graphql, usePreloadedQuery} require(react-relay); const UsernameComponent require(./UsernameComponent.react); function HomeTab(props: Props) { const data usePreloadedQueryAppQuery( graphql query HomeTabQuery($id: ID!) { user(id: $id) { name ...UsernameComponent_user } } , props.queryRef, ); return ( h1{data.user?.name}/h1 UsernameComponent user{data.user} / / ); }假设此前我们只获取过User{id: 4}的name且它已缓存在 Relay Store 中。此时以允许复用缓存的fetchPolicystore-or-network或store-and-network渲染该查询会发生以下过程查询先检查自身直接需要的数据是否缺失——这里并不缺失。查询本身只直接查询name而name可用就查询自身渲染所需的数据而言什么都不缺。这是关键设计渲染查询时Relay 会急切地读出数据并渲染整棵组件树而不是等整条查询含嵌套片段全部取回后再一次性渲染数据是否缺失是针对当前组件自身声明的数据判断的而非其后代组件的数据。查询没有缺失数据于是渲染然后尝试渲染子组件UsernameComponent。UsernameComponent渲染UsernameComponent_user片段时发现自身所需数据缺失——具体是username。此时它会挂起等待网络请求完成。注意无论选择哪种fetchPolicy只要整条查询含片段有任何数据缺失就总会发起网络请求。此刻UsernameComponent因缺失username而挂起但理想情况下User的name已缓存应当立即渲染出来。然而若没有用Suspense组件捕获这个挂起挂起会向上冒泡导致整个App都进入挂起状态。要实现name可用就先渲染、username缺失再等待只需把UsernameComponent用Suspense包裹起来允许App的其他部分继续渲染/** * HomeTab.react.js * * Query Component */ const React require(React); const {Suspense} require(React); const {graphql, usePreloadedQuery} require(react-relay); const UsernameComponent require(./UsernameComponent.react); function HomeTab() { const data usePreloadedQueryAppQuery( 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 / ); }上述机制对嵌套片段同样成立只要渲染某个片段所需的数据已缓存无论其子片段/后代片段是否缺失该片段组件都能渲染若某个子片段数据缺失用Suspense包裹它即可让应用其他部分继续渲染。这正是开头场景中即使 permalink 页面比 Feed 多要求少量数据也先用已缓存的部分渲染的实现基础——它让我们可以完全跳过 loading 状态渲染出更接近最终形态的中间 UI。小结复用缓存数据是 Relay 数据流中本地优先思想的集中体现其完整链路可以归纳为用fetchPolicy声明取数策略store-or-network/store-and-network/network-only/store-only决定缓存与网络的取舍用存在性管理缓存寿命——默认保留机制、environment.retain手动保留、gcScheduler与gcReleaseBufferSize控制垃圾回收节奏用陈旧性保证数据新鲜——invalidateStore()/invalidateRecord()标记失效、useSubscribeToInvalidationState立即响应失效、queryCacheExpirationTime设置缓存过期用missingFieldHandlers打通跨查询复用让指向同一记录的不同查询共享缓存用片段边界 Suspense实现部分渲染让已缓存的部分数据先上屏。本主题在仓库中的最新版文档位于 website/docs/guided-tour/reusing-cached-data可作为持续更新的参考而本文依据的 v13 版各子章节fetch-policies.md、presence-of-data.md、staleness-of-data.md、filling-in-missing-data.md、rendering-partially-cached-data.md与 RelayModernStore.js、QueryResource.js 等源码一一对应读者可按图索骥深入理解 Relay 缓存机制的全貌。【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表