
前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载导读在长时间运行的应用中Relay 会将在内存 store 中累积并缓存一段时间内多次查询获取到的数据。本指南基于 Relay 官方 Guided Tour 中 Reusing Cached Data 章节对应版本化文档及未版本化的 最新版文档系统讲解如何复用本地已缓存的数据让页面在无网络等待的情况下立即渲染。读完本篇你将掌握通过fetchPolicy控制查询的数据来源、理解数据存在性与时效性两个决定因素、通过数据保留与垃圾回收配置延长缓存生命周期以及用数据失效 API 与部分渲染机制构建流畅的用户体验。为什么需要复用缓存数据Relay 在应用使用过程中会为多次获取过的查询累积缓存数据introduction.md开篇即阐明这一点。在很多场景下我们希望直接复用本地已缓存的数据并立即渲染而不是重新等待一次网络请求Tab 切换每个 Tab 渲染一个查询。若某个 Tab 已被访问过再次切换过去时应立即渲染无需重新请求已经获取过的数据。从 Feed 进入帖子详情页帖子之前在 Feed 中渲染过其数据应当已在缓存中跳转到 permalink 页面时应立即渲染。部分数据缺失的情况即使渲染 permalink 页面需要比 Feed 中更多的数据我们也希望尽可能多地复用本地已有的数据而不是因为缺失一小块数据就阻塞整个页面的渲染。从源码结构看这一套查询 - 检查缓存 - 决定是否发起网络请求的机制核心实现在 RelayModernStore.js 与 OperationExecutor.js 中下文各节会结合具体实现逐步展开。用 Fetch Policy 控制查询的数据来源复现本地缓存数据的第一步是向loadQuery函数传入fetchPolicyloadQuery由useQueryLoader提供相关查询加载 API 参见 查询获取章节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 中的 可用性是否应该向服务器发起网络请求获取查询。四种 Fetch Policy 详解Relay 默认会先尝试从本地缓存读取查询如果该查询的任何数据 缺失 或 过期则从网络获取整个查询。这个默认策略被称为store-or-network。完整的可选值如下Fetch Policy是否复用本地缓存是否发起网络请求适用场景store-or-network默认是仅当有数据缺失或过期时Tab 切换、回退到已访问页面追求有缓存立即渲染无缓存自动请求store-and-network是总是发起既想立即渲染缓存又希望后台刷新保证数据最新network-only否总是发起强一致场景完全忽略本地缓存无论缺失或过期store-only是从不发起完全离线/本地数据操作取数责任由调用方承担也可用于读取纯 本地数据注意在 获取与渲染不同数据refetch 章节讨论的refetch函数同样接受fetchPolicy参数可用于分页、重取查询等场景。数据可用性两个决定性因素fetch policy 的实际行为取决于查询在 store 中的数据可用性见 availability-of-data.md。可用性由两个因素共同决定数据存在性Presence of Data数据是否在 store 中、会在 store 中保留多久数据时效性Staleness of Data数据是否已因失效 API 或缓存过期时间而被标记为过期。数据存在性理解缓存的生命周期当尝试复用 store 中缓存的查询数据时一个重要的认知是数据的生命周期它是否存在于 store 中以及会存在多久见 presence-of-data.md。一般而言某个查询的数据在其首次被获取后就会存在于 store 中只要该查询仍在屏幕上渲染。如果某个查询从未被获取过那它的数据在 store 中就是缺失的。然而我们无法将所有查询的数据无限期保存在内存中——随着时间推移数据会变得过于庞大和过于陈旧。为此Relay 会运行一个名为垃圾回收Garbage Collection的进程删除不再使用的数据。Relay 中的垃圾回收Relay 对本地内存 store 执行垃圾回收的方式是删除任何不再被应用中任何组件引用的数据对应实现在 RelayModernStore.js 的scheduleGC/_gcStep等逻辑中。但这与复用缓存数据的目标存在矛盾如果数据被过早删除在我们稍后尝试复用它之前就无法复用这些数据来免去网络等待地渲染页面。为此需要了解如何确保想复用的数据被保留足够长的时间。通常你不需要关心配置垃圾回收与数据保留这应由应用基础设施在RelayEnvironment层面配置这里讲解仅作参考。查询保留Query Retention保留Retain一个查询是向 Relay 表示该查询及其变量对应的数据不应被删除即不应被垃圾回收。多个调用方可以同时保留同一个查询只要至少有一个调用方在保留它该查询就不会从 store 中被删除。默认情况下任何使用useQueryLoader/usePreloadedQuery等 API 的查询组件会在其挂载期间保留对应查询组件卸载后会释放查询这意味着此后任意时刻该查询都可能被删除。如果需要在组件生命周期之外保留某个查询可以使用retain操作// Retain query; this will prevent the data for this query and // variables from being garbage collected by Relay const disposable environment.retain(queryDescriptor); // Disposing of the disposable will release the data for this query // and variables, meaning that it can be deleted at any moment // by Relays garbage collection if it hasnt been retained elsewhere disposable.dispose();这允许你在查询组件卸载后仍保留查询让其他组件或未来重新挂载的同一组件实例能够复用被保留的数据。控制垃圾回收策略的两个选项可以在创建 Relay Store 时提供两个选项来控制垃圾回收行为对应类型定义见 RelayModernStore.d.ts。GC SchedulergcScheduler是一个提供给 Relay Store 的函数用于决定何时调度一次 GC 执行// Sample scheduler function // Accepts a callback and schedules it to run at some future time. function gcScheduler(run: () void) { resolveImmediate(run); } const store new Store(source, {gcScheduler});默认情况下若未提供gcSchedulerRelay 会使用resolveImmediate函数调度垃圾回收见 resolveImmediate.js对应实现在 RelayModernStore.js 中this._gcScheduler options?.gcScheduler ?? resolveImmediate你可以提供自定义调度函数让 GC 的执行比默认不那么激进例如基于时间或 React Scheduler 的优先级或任何其他启发式策略。按约定实现不应立即执行传入的回调。GC Release Buffer SizeRelay Store 内部持有一个释放缓冲区release buffer用于在查询被其原始持有者释放后默认发生在渲染该查询的组件卸载时将一定数量可配置的查询临时保留一段时间。这使得在回退到之前访问过的页面、Tab 或内容时更有可能复用缓存数据。配置方式是指定gcReleaseBufferSize选项const store new Store(source, {gcReleaseBufferSize: 10});缓冲区大小为0等价于没有释放缓冲区查询会被立即释放并被收集默认情况下环境environment的释放缓冲区大小为10常量定义见 RelayModernStore.js 中const DEFAULT_RELEASE_BUFFER_SIZE 10;并在 构造函数 中应用。从源码看RelayModernStore.js 中retain/release相关逻辑会检查_gcReleaseBufferSize 0且缓冲区未满时将被释放的 root entry 放入_releaseBuffer超出容量时再触发 GC——这解释了为何缓冲区能暂时挽留已释放的查询。数据时效性失效与过期假设数据已存在于 store 中我们仍需考虑其时效性见 staleness-of-data.md。默认情况下Relay不会认为 store 中的数据过期无论它在缓存中存在了多久除非它被显式地使用数据失效 API 标记为过期或它超过了查询缓存过期时间。将数据标记为过期适用于我们明确知道某些数据已不再新鲜的场景例如执行了一次 Mutation 之后。Relay 提供以下 API 在 store 更新过程中将数据标记为过期。全局失效整个 Relay Store最粗粒度的失效方式是使整个 store失效即所有已缓存数据在失效后都将被视为过期。在 updater 函数中调用invalidateStore()function updater(store) { store.invalidateStore(); }调用invalidateStore()会使失效发生前写入 store 的所有数据都被视为过期并要求任何查询在下一次被求值时重新获取updater 函数可以作为 mutation、subscription 或 本地 store 更新 的一部分指定。失效 store 中的特定数据我们也可以更细粒度地只失效 store 中的特定记录record与全局失效相比只有引用了被失效记录的查询才会被视为过期。在 updater 函数中调用invalidateRecord()function updater(store) { const user store.get(id); if (user ! null) { user.invalidateRecord(); } }对user记录调用invalidateRecord()会将该用户在 store 中标记为过期任何缓存中引用了该用户的查询都会被视为过期并在下一次求值时被重新获取同样updater 可以来自 mutation、subscription 或本地 store 更新。订阅数据失效useSubscribeToInvalidationState仅仅把 store 或记录标记为过期会导致查询在下一次被求值时重新获取例如下次导航回渲染过期查询的页面时即使数据已缓存查询也会被重新获取。这对很多场景有用但有些场景我们希望在失效发生时立即重新获取失效了当前页面已经可见的数据由于没有发生导航当前页面的查询不会被重新求值即使数据过期也不会被立即重新获取用户会看到过期数据失效了上一个从未被卸载的视图所渲染的数据由于视图未被卸载导航回去时查询也不会被重新求值同样会展示过期数据。为支持这些场景Relay 提供了useSubscribeToInvalidationStatehookfunction ProfilePage(props) { // Example of querying data for the current page for a given user const data usePreloadedQuery( graphql..., props.preloadedQuery, ) // Here we subscribe to changes in invalidation state for the given user ID. // Whenever the user with that ID is marked as stale, the provided callback will // be executed useSubscribeToInvalidationState([props.userID], () { // Here we can do things like: // - re-evaluate the query by passing a new preloadedQuery to usePreloadedQuery. // - imperatively refetch any data // - render a loading spinner or gray out the page to indicate that refetch // is happening. }) return (...); }useSubscribeToInvalidationState接收一个 id 数组和一个回调当这些 id 对应的任意记录被标记为过期时提供的回调会被触发在回调内部可以做出相应反应并重新获取/更新任何渲染过期数据的当前视图。例如将preloadedQuery保存在 state 中并在此处设置新的值从而重新执行顶层的usePreloadedQuery由于此时查询已过期即使数据在 store 中被缓存查询也会被重新获取。该 hook 的实现可参见 useSubscribeToInvalidationState.js。查询缓存过期时间Query Cache Expiration Time此外查询缓存过期时间会影响某些操作即一个查询与其变量组合是否能由 store 中已有的数据满足即该查询的数据是否已过期。一个查询被视为过期是指它可以用 store 中的记录满足且满足以下任一条件距其上次被获取的时间大于查询缓存过期时间其中包含至少一条被失效过的记录。该过期检查发生在发起新请求时例如调用loadQuery。引用了过期数据的组件仍然可以继续渲染这些数据但任何本应由过期数据满足的额外请求都会转向网络。配置方式是指定queryCacheExpirationTime选项const store new Store(source, {queryCacheExpirationTime: 5 * 60 * 1000 });上述示例将过期时间设为 5 分钟毫秒单位如果未提供queryCacheExpirationTime时效性检查将只看所引用记录是否被失效过。从源码看RelayModernStore.js 中定义了isOperationStale辅助函数当operationFetchTime ! null且queryCacheExpirationTime ! null时比较operationFetchTime Date.now() - queryCacheExpirationTime来判断操作是否过期RelayModernStore.js 中同样使用rootEntry.fetchTime与过期时间比较来判断查询入口是否过期。填充缺失数据Missing Field Handlers前面章节讨论了如何复用完全或部分缓存的数据但仍存在 Relay 无法自动判断可以用已有查询的数据满足另一个查询的情况见 filling-in-missing-data.md。具体来说Relay 知道如何复用之前被获取过的同一个查询的缓存数据——如果完全相同的查询被获取两次第二次求值时 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 { // Make sure to add a handler for the node field 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) ) { // If field is user(id: $id), look up the record by the value of $id return argValues.id; } if ( record ! null record.getType() ROOT_TYPE field.name story argValues.hasOwnProperty(story_id) ) { // If field is story(story_id: $story_id), look up the record by the // value of $story_id. return argValues.story_id; } return undefined; }, kind: linked, }, ]; const environment new Environment({/*...*/, missingFieldHandlers});要点说明missingFieldHandlers是一个handler 数组。每个 handler 必须指定一个handle函数以及它能处理的缺失字段的kind。主要有两类字段scalar包含标量值的字段例如数字或字符串linked引用另一个对象的字段即非标量。handle函数接收三个参数缺失的字段field、该字段所属的记录record以及当前查询执行中传给该字段的参数argValues。处理scalar字段时handle函数应返回一个标量值作为缺失字段的值处理linked字段时handle函数应返回一个ID引用 store 中另一个应替代缺失字段的对象。当 Relay 尝试从本地缓存满足查询时一旦检测到任何缺失数据会先运行任何与字段类型匹配的 missing field handler然后才最终判定数据缺失。从源码结构看missingFieldHandlers贯穿环境、多 Actor 环境与数据写入链路它在 MultiActorEnvironment.js 中被收集并传递给子环境并在 RelayRecordSourceProxy.js 与 RelayRecordSourceSelectorProxy.js 中被用于在写入 store 时回填缺失字段。渲染部分缓存数据Partial Rendering在 Relay 中渲染缓存数据时可以执行部分渲染partial rendering——即立即渲染一个部分被缓存的查询查询的一部分数据可能缺失另一部分可能已缓存我们希望立即渲染已缓存的部分而不必等整个查询获取完成见 rendering-partially-cached-data.md。这在以下场景中很有用希望尽可能快地渲染一个页面且知道该页面的部分数据已缓存可以跳过加载状态。例如个人资料页用户姓名很可能在应用使用过程中已被缓存访问资料页时若姓名已缓存即使页面其余数据尚未就绪也应立即渲染姓名。Fragment 作为部分渲染的边界实现该能力依赖 fragment 组件的挂起suspend机制参见 Loading States with Suspense。fragment 组件在渲染时若其本地声明的数据缺失且正在被获取就会挂起具体而言它会一直挂起到所需数据被获取即其父查询被获取完成。用一个例子说明。假设有如下 fragment 组件/** * 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;以及如下查询组件它查询了一些数据并内联了上述 fragment/** * AppTabs.react.js * * Query Loader Component */ // .... const onSelectHomeTab () { loadHomeTabQuery({id: 4}, {fetchPolicy: store-or-network}); } // ... /** * 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 usePreloadedQuery( graphql query HomeTabQuery($id: ID!) { user(id: $id) { name ...UsernameComponent_user } } , props.queryRef, ); return ( h1{data.user?.name}/h1 UsernameComponent user{data.user} / / ); }假设渲染HomeTab时我们已经只获取过User{id: 4}的name字段且它已缓存在当前 Relay environment 关联的 store 中。如果使用允许复用本地缓存数据的fetchPolicystore-or-network或store-and-network渲染该查询将发生以下过程查询会检查其本地必需数据是否缺失。本例中并不缺失查询只直接选择了name字段而该字段在 store 中可用。Relay 仅在数据是本地声明的且缺失时才认为数据缺失。换言之fragment 展开内选择的数据不会影响外层查询或 fragment 是否被判定为缺失数据。由于查询没有缺失数据它会渲染然后尝试渲染子组件UsernameComponent。当UsernameComponent尝试渲染UsernameComponent_userfragment 时Relay 会发现渲染所需的某些数据缺失——具体是username缺失。此时由于UsernameComponent有缺失数据它会挂起渲染直到网络请求完成。注意无论选择哪种fetchPolicy只要完整查询包括 fragment中有任何数据缺失就总会发起网络请求。此时当UsernameComponent因username缺失而挂起时理想情况下我们仍应立即渲染User的name因为它是本地缓存的。但由于没有用Suspense组件捕获 fragment 的挂起挂起会向上冒泡导致整个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 usePreloadedQuery( graphql query AppQuery($id: ID!) { user(id: $id) { name ...UsernameComponent_user } } , props.queryRef, ); return ( h1{data.user?.name}/h1 {/* Wrap the UserComponent in Suspense to allow other parts of the App to be rendered even if the username is missing. */} Suspense fallback{LoadingSpinner labelFetching username /} UsernameComponent user{data.user} / /Suspense / ); }上述过程对嵌套 fragment即 fragment 内再包含的 fragment同样有效。这意味着如果渲染某个 fragment 所需的数据已被本地缓存该 fragment 组件就能渲染无论其子/后代 fragment 的数据是否缺失。如果子 fragment 的数据缺失可以将其包裹在Suspense组件中让其他 fragment 和应用的其他部分继续渲染。正如开篇场景所述这一能力之所以可取是因为它能让我们完全跳过加载状态渲染部分可用的数据让我们渲染出更接近最终状态的中间 UI。小结复用缓存数据的完整工作流综合全部分节一个完整的复用缓存数据决策链路如下判断存在性查询数据是否在 store 中未获取过则为缺失组件卸载后依赖retain与gcReleaseBufferSize决定数据能保留多久底层由 RelayModernStore.js 的垃圾回收逻辑驱动判断时效性数据是否被invalidateStore()/invalidateRecord()标记失效或超过queryCacheExpirationTime选择 fetchPolicystore-or-network默认在有缺失/过期数据时走网络store-and-network总是同时走缓存与网络network-only忽略缓存store-only永不联网补齐认知盲区通过missingFieldHandlers告诉 Relay 不同查询字段间的等价关系如node(id)与user(id)渲染策略用Suspense包裹子 fragment 组件让已缓存的部分立即渲染、缺失部分优雅挂起并显示局部加载状态最终利用useSubscribeToInvalidationState在数据失效时主动刷新当前视图。上述所有配置项gcScheduler、gcReleaseBufferSize、queryCacheExpirationTime、missingFieldHandlers均可在创建Store或Environment时提供其类型签名可在 RelayModernStore.d.ts 中查阅你也可以在 relay-runtime 的 store 测试目录 中找到大量验证这些行为如 GC、失效、check 逻辑的测试用例作为深入理解与二次开发的参考。赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay 缓存数据复用完全指南Fetch Policy、数据保留与局部渲染实战Relay 缓存数据复用完全指南Fetch Policy、数据保留与局部渲染实战 缓存复用是 Relay 数据获取体系中最能直接影响用户体验的一环应用运行期前端开发工具Relay 缓存复用完整指南fetchPolicy、数据保留与局部渲染机制Relay 缓存复用完整指南fetchPolicy、数据保留与局部渲染机制 本文基于仓库内 website/versioned_docs/version v1前端开发工具Relay 数据存在性Presence of Data指南垃圾回收、查询保留与缓存复用机制Relay 数据存在性Presence of Data指南垃圾回收、查询保留与缓存复用机制 缓存数据能否被复用的前提是理解这些数据在 Relay Sto前端开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考