ARTICLE DETAIL

资讯详情

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

Relay Typesafe Updaters 全面指南:用 `readUpdatableQuery` 与 `readUpdatableFragment` 安全地命令式修改 Store 数据

Relay Typesafe Updaters 全面指南:用 `readUpdatableQuery` 与 `readUpdatableFragment` 安全地命令式修改 Store 数据 前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载Relay 的 Typesafe Updaters类型安全更新器是一套在 store 上命令式imperatively修改本地数据的类型安全且更符合人体工程学的 API 体系核心由readUpdatableQuery与readUpdatableFragment两个入口构成。本文以官方 FAQ 为骨架结合仓库源码与实战示例讲清它解决了什么问题、底层如何工作、有哪些使用约束以及在哪里拿到store来调用这两类 API帮助你安全地管理客户端本地状态例如 client schema extension 中的字段。Typesafe Updaters 是什么项目背景与命名由来Typesafe updaters类型安全更新器是一个项目的名字目标是提供一套类型安全typesafe且符合人体工程学ergonomic的替代 API用于在 Relay store 上命令式地更新数据。readUpdatableFragment和readUpdatableQuery就是 store 暴露出的两个核心 typesafe updater 入口它们的完整签名定义在 store API 参考文档 中readUpdatableFragmentTFragmentType: FragmentType, TData( fragment: UpdatableFragmentTFragmentType, TData, fragmentReference: HasUpdatableSpreadTFragmentType, ): UpdatableDataTData; readUpdatableQueryTVariables: Variables, TData( query: UpdatableQueryTVariables, TData, variables: TVariables, ): UpdatableDataTData;为什么需要它WhyRelay 在“获取和管理来自服务端的数据”这一侧提供了类型安全且易用的 API同时Relay 也支持在client schema extensions中定义仅存在于客户端的字段。然而过去用于修改这些字段数据的 API 冗长且不友好以至于官方无法将 Relay 推荐为管理本地状态的方案。Typesafe updaters 正是为了补上这块短板。旧 API 的问题在哪里旧有的命令式更新 API 存在两个明显的缺陷冗长verbose需要开发者写出大量样板代码。非类型安全not typesafe极易犯下各类低级错误例如字段名拼错、类型不匹配等。更关键的是旧 API 要求开发者只有在编写 updater 时才需要去学习一套全新的 API 集合例如setValue、setLinkedRecord、getLinkedRecord等RecordProxy方法学习成本高且与日常开发模式割裂。Typesafe updaters 的优势在于复用 Relay 早已为人熟知的习惯用法query、fragment、类型收窄type refinement用getter 与 setter属性读写取代需要单独记忆的方法集——updatableData.name Godzilla这种写法对任何 JavaScript 开发者都直观易懂赋值操作在底层仍然会被转译为对旧 API 的调用但被类型系统严格约束错误会在编译期暴露。开发者如何使用 Typesafe Updaters使用流程可以概括为三步声明编写一个 updatable query 或 fragment显式指定要命令式更新的数据读取从 store 中读出这些数据得到一个所谓的updatable proxy可更新代理对象修改通过 setter 修改这个 updatable proxy例如updatableData.name Godzilla。第 3 步的赋值动作最终会转译为对旧 API 的调用详见下文源码分析但整个过程有了类型安全保证。什么是 updatable query 或 fragment所谓 updatable query 或 fragment就是带有updatable指令的 query 或 fragment。例如# updatable fragment fragment StoryLikeButton_updatable on Story updatable { likeCount doesViewerLike } # updatable query query NameUpdaterUpdateQuery updatable { viewer { name } }updatable指令会在编译期被 Relay 编译器识别并特殊处理详见下文“编译器如何对待 updatable 操作”一节。关键认知updatable queries / fragments 不会被真正抓取这是 Typesafe Updaters 最核心、也最容易误解的一点。updatable query / fragment 中选择的字段会从服务端抓取吗不会服务端根本不知道 updatable queries 和 fragments 的存在它们的字段永远不会被发送网络请求抓取。即使在普通 query / fragment 中 spread 了一个 updatable fragment该 updatable fragment 所选中的字段也不会作为那次请求的一部分被抓取。updatable 操作本质上只是“对 store 中已有数据的读写描述”而非网络请求描述。如果我想同时抓取并修改某个字段怎么办你需要在普通 query/fragment和updatable query/fragment中分别选择该字段# 1) 在普通 fragment 中抓取字段用于渲染 fragment StoryLikeButton on Story { id likeCount doesViewerLike ...StoryLikeButton_updatable # 2) 同时 spread updatable fragment } # 3) updatable fragment 只描述“要修改哪些字段” fragment StoryLikeButton_updatable on Story updatable { likeCount doesViewerLike }两个 fragment 各自负责各自的职责普通 fragment 负责抓取与渲染updatable fragment 负责允许命令式修改。由此带来的一系列后果FAQ 明确列出了这一设计带来的一系列约束理解它们能避免踩坑读取 updatable 数据时可能缺失当从 store 中读出 updatable 数据时如果该数据当前不在 store 中结果可能是缺失的需要做空值检查不能在 updatable query/fragment 中 spread 普通 fragment普通 fragment 依赖服务端抓取的数据与 updatable 的语义冲突生成的 artifact 不包含 query ID也不包含 normalization ASTnormalization AST 原本用于把网络数据写入 store而 updatable 操作根本不参与网络抓取自然不需要它defer等指令在此上下文中没有意义会被禁止这些指令都服务于网络数据的渐进式交付与纯本地读写场景无关。编译器如何对待 updatable 操作源码佐证上述行为可以从编译器源码中得到印证。在 apply_transforms.rs 中编译管线会对 updatable 操作执行专门的变换。其中 skip_updatable_queries.rs 里的SkipUpdatableQueriesTransform会遍历整个 program凡是带updatable指令的操作定义都会执行Transformed::Delete也就是直接把 updatable query 从生成网络中剔除fn transform_operation(mut self, operation: OperationDefinition) - TransformedOperationDefinition { if operation .directives .iter() .any(|directive| directive.name.item *UPDATABLE_DIRECTIVE) { Transformed::Delete } else { Transformed::Keep } }与此同时annotate_updatable_fragment_spreads.rs 等变换会把 updatable fragment spread 标注为内部指令__updatable。这两点共同说明了 FAQ 所述事实的底层机制updatable 操作不参与网络抓取管线自然也不会产生 query ID 与 normalization AST。updatable proxy 的底层实现运行时入口readUpdatableQuery的运行时实现在 packages/relay-runtime/mutations/readUpdatableQuery.js 中。它从 store 的根记录proxy.getRoot()出发结合 variables 与updatableQuery.fragment.selections调用createUpdatableProxy构建代理对象function readUpdatableQuery(query, variables, proxy, missingFieldHandlers) { const updatableQuery getUpdatableQuery(query); return { updatableData: createUpdatableProxy( proxy.getRoot(), variables, updatableQuery.fragment.selections, proxy, missingFieldHandlers, ), }; }readUpdatableFragment的实现位于 packages/relay-runtime/mutations/readUpdatableFragment.js。它首先通过fragmentReference[ID_KEY]拿到目标记录 id再从 store 中取出对应记录作为代理根同时通过getVariablesFromFragment解析 fragment 变量const fragmentRoot proxy.get(id); invariant(fragmentRoot ! null, No record with ${id} was found. ...);注意源码中的注释复数形式的 fragment references 目前不被支持plural fragment references are currently not supported。getter/setter 如何生成真正生成 updatable proxy 的核心逻辑在 packages/relay-runtime/mutations/createUpdatableProxy.js 中它逐条遍历 selections用Object.defineProperty为每个字段挂上 getter 与 setter标量字段ScalarFieldgetter 调用updatableProxyRootRecord.getValue(...)读取setter 调用setValue__UNSAFE(...)写回。值得注意的是源码中有一个nonUpdatableKeys [id, __id, __typename, js]数组——这些键的 setter 被显式置为undefined也就是说像id、__typename这类元数据字段是不可赋值的关联字段LinkedField单数与复数getter 使用getLinkedRecord/getLinkedRecords并递归为关联记录构建子代理setter 则通过setLinkedRecord/setLinkedRecords建立关联要求传入的对象必须携带__id字段内联 fragmentInlineFragment仅当记录的getType()与selection.type匹配时才递归展开——这正是 FAQ 提到的type refinement类型收窄在底层的体现ClientExtension直接递归展开天然支持 client schema extension 中的字段FragmentSpread被显式忽略其余变体Defer、Stream、RelayResolver等会抛出错误因为它们在 updatable 上下文中没有意义。赋值语义上还有一些值得注意的细节给复数关联字段赋null会抛错提示“应该赋空数组而不是 null”复数关联字段的数组中不允许出现 null 或 undefined 元素关联字段 setter 要求目标记录已存在于 store 中否则抛错Did not find item with data id ... in the store.。当字段缺失时getter 还会尝试调用missingFieldHandlers如getLinkedRecordUsingMissingFieldHandlers、getScalarUsingMissingFieldHandlers来兜底解析缺失数据。在__DEV__环境下生成的代理对象会被Object.freeze冻结帮助尽早发现误用。在哪里拿到store并调用这些 APIFAQ 的 Misc 部分回答了“store从哪里来”这一高频问题。包含readUpdatableQuery和readUpdatableFragment方法的类包括RelayRecordSourceSelectorProxy、RecordSourceProxy与RelayRecordSourceProxy。你可以通过以下途径获取其实例mutation / subscription 的 updater 函数中mutation 的 optimistic updater 中使用RelayModernEnvironment的commitUpdate、applyUpdate等方法时使用独立的commitLocalUpdate方法时。commitLocalUpdate的实现很轻量见 packages/relay-runtime/mutations/commitLocalUpdate.js它只是把环境与 updater 转发给environment.commitUpdate(updater)function commitLocalUpdate(environment, updater) { environment.commitUpdate(updater); }实战示例一在 mutation updater 中初始化客户端字段下面这个完整示例来自 imperatively-modifying-store-data 指南。场景通过 client schema extension 给Feedback类型新增一个is_new_comment字段并在创建 Feedback 的 mutation 完成后将其设为true。先定义 schema extension# Feedback.graphql extend type Feedback { is_new_comment: Boolean }再在 mutation 的updater中通过readUpdatableFragment完成更新// CreateFeedback.js function commitCreateFeedbackMutation(environment, input) { return commitMutation(environment, { mutation: graphql mutation CreateFeedbackMutation($input: FeedbackCreateData!) { feedback_create(input: $input) { feedback { id # Step 1: 在 mutation 响应中 spread updatable fragment ...CreateFeedback_updatable_feedback } } } , variables: {input}, // Step 2: 定义 updater updater: (store, response) { // Step 3: 取回并空值检查 feedback 对象 const feedbackRef response?.feedback_create?.feedback; if (feedbackRef null) { return; } // Step 4: 调用 readUpdatableFragment 得到 updatable proxy const {updatableData} store.readUpdatableFragment( graphql fragment CreateFeedback_updatable_feedback on Feedback updatable { is_new_comment } , feedbackRef, ); // Step 5: 直接给属性赋值 updatableData.is_new_comment true; }, }); }这个例子完整展示了三步走的流程先在 mutation 响应中 spread updatable fragment目的是拿到 fragment reference 并确保记录已写入 store再调用readUpdatableFragment读取代理最后通过 setter 赋值。updater 执行完后所有记录下来的更新会被写入 store所有受影响的组件都会重新渲染。实战示例二在用户交互中切换本地状态再看一个更贴近日常的场景——点击按钮切换is_selected字段同样定义在 client schema extension 中# User.graphql extend type User { is_selected: Boolean }// UserSelectToggle.react.js function UserSelectToggle({userId, viewerRef}) { const viewer useFragment( graphql fragment UserSelectToggle_viewer on Viewer { user(user_id: $user_id) { id name is_selected ...UserSelectToggle_updatable_user } } , viewerRef, ); const environment useRelayEnvironment(); return ( button onClick{() { commitLocalUpdate(environment, (store) { const userRef viewer.user; if (userRef null) { return; } const {updatableData} store.readUpdatableFragment( graphql fragment UserSelectToggle_updatable_user on User updatable { is_selected } , userRef, ); updatableData.is_selected !viewer?.user?.is_selected; }); }} {viewer?.user?.is_selected ? Deselect : Select} {viewer?.user?.name} /button ); }与上一个例子的区别在于commitLocalUpdate的 updater不接受第二个参数没有关联的网络 payload。指南中还提到这个例子可以用environment.commitPayloadAPI 改写但那样会失去类型安全。何时该用readUpdatableQuery而非readUpdatableFragmentreadUpdatableQuery与readUpdatableFragment的核心区别是前者不需要传 fragment reference只需要你从根Query类型到目标记录之间有一条已知的路径。官方指南明确列出推荐使用readUpdatableQuery的几种场景手头没有现成的 fragment reference例如commitLocalUpdate的调用与某个组件并无直接关联拿不到选择“父记录”的 fragment——由于 Relay 存在一个已知的类型空洞known type holeupdatable fragments 不能 spread 在顶层希望在 updatable fragment 中使用变量目前 updatable fragments 会复用传入 query 的变量这意味着你无法让 updatable fragment 拥有 fragment-local 变量也无法多次调用readUpdatableFragment并每次传入不同变量。一个使用readUpdatableQuery的完整例子改写自 imperatively-modifying-store-data 指南// NameUpdater.react.js const onSubmit () { commitLocalUpdate(environment, (store) { const {updatableData} store.readUpdatableQuery( graphql query NameUpdaterUpdateQuery updatable { viewer { name } } , {}, ); const viewer updatableData.viewer; if (viewer ! null) { viewer.name newName; } }); };注意这里通过readUpdatableQuery直接以{}作为 variables 调用无需任何 fragment referenceupdatableData.viewer仍是一个可为空的代理对象需要空值检查后再赋值。使用建议与边界总结综合 FAQ、指南与源码可以把 Typesafe Updaters 的正确使用姿势归纳为以下几点用途命令式修改 store 中的本地数据尤其适合 client schema extensions 字段的初始化与更新、复杂客户端更新以及invalidateStore、删除节点、查找连接等只有 updater 才能做到的操作不要用它触发副作用需要触发副作用时请使用onCompleted回调——它保证只调用一次而 updater / optimistic updater 可能被重复调用读写分离要展示的数据用普通 query/fragment 抓取要修改的数据在 updatable query/fragment 中声明两者各自选择所需字段注意空值由于 updatable 数据依赖 store 中已有的记录读取结果可能缺失务必做空值检查或用required指令理解执行时机optimistic updater 在 mutation 触发时执行、完成或失败后回滚普通 updater 在 mutation 成功完成后执行。若两个 optimistic response 都修改同一值第一个回滚时第二个不会被重新计算该值会保持“叠加后”的结果。如果你还想了解更底层的RecordProxy、RecordSourceProxy方法如setValue、setLinkedRecord等可以进一步阅读 store API 参考 与旧式命令式更新指南 imperatively-modifying-store-data-legacy.md对比新旧两套 API 的差异后你会更深刻地体会 Typesafe Updaters 在类型安全与开发体验上的改进。赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay Typesafe Updaters 全面指南用 readUpdatableQuery 与 readUpdatableFragment 类型安全地命令式更新 Store 数据Relay Typesafe Updaters 全面指南用 readUpdatableQuery 与 readUpdatableFragment 类型安全地命前端开发工具Adblock Fast Chrome扩展开发从零开始构建浏览器广告拦截器Adblock Fast Chrome扩展开发从零开始构建浏览器广告拦截器 Adblock Fast是一款适用于Windows、Android、iOS、Chr前端开发工具Relay 命令式修改 Store 数据readUpdatableFragment 与 readUpdatableQuery 类型安全 Updater 实战指南Relay 命令式修改 Store 数据 readUpdatableFragment 与 readUpdatableQuery 类型安全 Updater 实战前端开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表