Relay Store API 参考使用 RecordSourceSelectorProxy、RecordProxy 与 ConnectionHandler 编程式更新客户端数据【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relayRelay Store 是 Relay 运行时中负责保存和检索已获取 GraphQL 数据的客户端存储层。本指南以 v17 版本官方 API 参考文档store.md为骨架深入讲解如何在updater函数中通过RecordSourceSelectorProxy、RecordProxy与ConnectionHandler编程式地读取、写入和失效客户端数据。读完本文你将掌握完整的 Store 接口签名、每个方法的参数语义与典型用法并能结合源码理解连接Connection字段在底层是如何被定位和编辑的从而安全地编写变更、订阅和本地更新的 updater 逻辑。在 Relay 中服务端响应会由relay-runtime自动规范化并写入 Store而客户端侧的临时数据修改、乐观更新、变更提交后的本地补偿等场景则需要开发者通过updater函数手工操作 Store。所有updater函数的第一个参数store的类型就是RecordSourceSelectorProxy。关于 updater 函数的使用场景可参考 GraphQL Mutations 指南。本文其余结构如下先给出 Store 的三个核心接口概览然后逐一详解RecordSourceSelectorProxy、RecordProxy、ConnectionHandler的方法签名、参数、返回值与可运行示例最后补充失效机制在源码与测试中的印证帮助你在真实项目中精准落笔。接口总览Store 编程式操作主要涉及三个接口它们都定义在 RelayStoreTypes.js 中RecordSourceSelectorProxyupdater收到的store的类型负责在 Store 层面创建、删除、查找记录以及访问根字段。RecordProxy单条记录的读写代理负责读取/写入字段值、关联记录以及失效单条记录。ConnectionHandlerrelay-runtime暴露的辅助工具专门用于定位和编辑connection连接字段增删边等。从源码结构看RecordSourceSelectorProxy继承自更底层的RecordSourceProxyRelayStoreTypes.js后者额外提供readUpdatableQuery/readUpdatableFragment以配合可更新数据读取RecordSourceSelectorProxy则在 L561-L565 增加了getRootField、getPluralRootField两个访问 Selector 根字段的方法。接口注释也指出这些代理接口的设计意图是允许看起来像直接操作 Record同时允许不同实现例如记录变更集 changeset因此你写入的修改会由具体实现统一打包提交。RecordSourceSelectorProxyRecordSourceSelectorProxy是updater函数接收的store参数的类型。官方文档给出的接口如下interface RecordSourceSelectorProxy { create(dataID: string, typeName: string): RecordProxy; delete(dataID: string): void; get(dataID: string): ?RecordProxy; getRoot(): RecordProxy; getRootField(fieldName: string): ?RecordProxy; getPluralRootField(fieldName: string): ?Array?RecordProxy; invalidateStore(): void; }create(dataID, typeName): RecordProxy按给定的dataID与 GraphQL schema 中定义的typeName在 Store 中新建一条记录返回一个RecordProxy用于继续修改这条新记录。const record store.create(dataID, Todo);需要注意create只是把记录放入 Store若需要把新记录挂到某个父记录或连接上还需配合setLinkedRecord/setLinkedRecords/ConnectionHandler完成关联。delete(dataID): void按dataID从 Store 中删除一条记录。store.delete(dataID);get(dataID): ?RecordProxy按dataID取回一条记录返回RecordProxy以便修改若记录不存在则返回null。const record store.get(dataID);getRoot(): RecordProxy返回代表 GraphQL 文档根节点的RecordProxy即 root query / mutation / subscription 的根记录。// 对应 GraphQL 文档 // viewer { // id // } // 返回 root query const root store.getRoot();getRootField(fieldName): ?RecordProxy按 GraphQL 文档中定义的字段名取回根字段对应的记录。注意这里的fieldName是当前 mutation/subscription 文档中出现的顶层字段名如viewer、createTodo而不是 schema 中的任意字段。// 对应 GraphQL 文档 // viewer { // id // } const viewer store.getRootField(viewer);getPluralRootField(fieldName): ?Array?RecordProxy当根字段是一个集合列表时返回RecordProxy数组。// 对应 GraphQL 文档 // nodes(first: 10) { // # ... // } const nodes store.getPluralRootField(nodes);invalidateStore(): void全局失效整个 Store任何在失效发生之前写入 Store 的数据都会被标记为过期stale当下一次用environment.check()检查查询时会被判定为需要重新获取refetch。store.invalidateStore();失效后在重新获取之前检查查询将返回 staleenvironment.check(query) stale注意v17 文档中用 stale示意语义在当前仓库的测试中environment.check()实际返回的是{status: stale}对象见 RelayModernEnvironment-ExecuteMutationWithGlobalInvalidation-test.js请在编写断言时以实际运行时返回值为准。RecordProxyRecordProxy是对单条记录读写操作的接口。官方文档的接口如下interface RecordProxy { copyFieldsFrom(sourceRecord: RecordProxy): void; getDataID(): string; getLinkedRecord(name: string, arguments?: ?Object): ?RecordProxy; getLinkedRecords(name: string, arguments?: ?Object): ?Array?RecordProxy; getOrCreateLinkedRecord( name: string, typeName: string, arguments?: ?Object, ): RecordProxy; getType(): string; getValue(name: string, arguments?: ?Object): mixed; setLinkedRecord( record: RecordProxy, name: string, arguments?: ?Object, ): RecordProxy; setLinkedRecords( records: Array?RecordProxy, name: string, arguments?: ?Object, ): RecordProxy; setValue(value: mixed, name: string, arguments?: ?Object): RecordProxy; invalidateRecord(): void; }源码中的RecordProxy定义位于 RelayStoreTypes.js与之并存的还有只读变体ReadOnlyRecordProxyL504-L510用于只读场景。下面按方法逐一展开。getDataID(): string返回当前记录的dataID。const id record.getDataID();getType(): string返回当前记录在 GraphQL schema 中定义的类型名。const type user.getType(); // UsergetValue(name, arguments?): mixed读取当前记录指定字段的值。若字段带参数可以传入变量袋arguments。// 对应 GraphQL 文档 // viewer { // id // name // } const name viewer.getValue(name);带参数字段// 对应 GraphQL 文档 // viewer { // id // name(arg: $arg) // } const name viewer.getValue(name, {arg: value});getLinkedRecord(name, arguments?): ?RecordProxy取回与当前记录通过指定字段关联的另一条记录。// 对应 GraphQL 文档 // rootField { // viewer { // id // name // } // } const rootField store.getRootField(rootField); const viewer rootField.getLinkedRecord(viewer);带参数字段// 对应 GraphQL 文档 // rootField { // viewer(arg: $arg) { // id // } // } const rootField store.getRootField(rootField); const viewer rootField.getLinkedRecord(viewer, {arg: value});getLinkedRecords(name, arguments?): ?Array?RecordProxy取回与当前记录通过指定字段关联的一组记录列表字段。// 对应 GraphQL 文档 // rootField { // nodes { // # ... // } // } const rootField store.getRootField(rootField); const nodes rootField.getLinkedRecords(nodes);带参数字段// 对应 GraphQL 文档 // rootField { // nodes(first: $count) { // # ... // } // } const rootField store.getRootField(rootField); const nodes rootField.getLinkedRecords(nodes, {count: 10});getOrCreateLinkedRecord(name, typeName, arguments?)取回与当前记录关联的记录若不存在则按typeName创建一条新记录。常用于为尚未存在的关联对象做获取或创建。// 对应 GraphQL 文档 // rootField { // viewer { // id // } // } const rootField store.getRootField(rootField); const newViewer rootField.getOrCreateLinkedRecord(viewer, User); // 不存在则创建同样支持可选的变量袋arguments。setValue(value, name, arguments?): RecordProxy把新值写入当前记录的指定字段返回被修改的记录便于链式调用。// 对应 GraphQL 文档 // viewer { // id // name // } viewer.setValue(New Name, name);带参数字段viewer.setValue(New Name, name, {arg: value});copyFieldsFrom(sourceRecord): void把传入记录sourceRecord的所有字段复制到当前记录覆盖同名字段缺失字段则补全常用于将一条临时记录的内容同步到正式记录。const record store.get(id1); const otherRecord store.get(id2); record.copyFieldsFrom(otherRecord); // 修改的是 recordsetLinkedRecord(record, name, arguments?)把一条关联记录挂到当前记录的指定字段上单数关联字段。// 对应 GraphQL 文档 // rootField { // viewer { // id // } // } const rootField store.getRootField(rootField); const newViewer store.create(/* ... */); rootField.setLinkedRecord(newViewer, viewer);支持可选的变量袋arguments。setLinkedRecords(records, name, variables?)把一组关联记录挂到当前记录的指定字段上复数/列表关联字段通常先读取现有列表再追加新元素。// 对应 GraphQL 文档 // rootField { // nodes { // # ... // } // } const rootField store.getRootField(rootField); const newNode store.create(/* ... */); const newNodes [...rootField.getLinkedRecords(nodes), newNode]; rootField.setLinkedRecords(newNodes, nodes);支持可选的变量袋arguments。invalidateRecord(): void只失效当前这条记录任何引用了这条记录的查询都会被标记为 stale直到重新获取数据。const record store.get(4); record.invalidateRecord();失效后引用该记录且尚未重新获取的查询在environment.check()中会返回 staleenvironment.check(query) staleinvalidateRecord与invalidateStore的差异在于作用域前者只影响引用该记录的查询后者影响整个 Store。本地失效对应的测试见 RelayModernEnvironment-CheckWithLocalInvalidation-test.js。ConnectionHandlerConnectionHandler是relay-runtime提供的连接操作工具模块接口如下interface ConnectionHandler { getConnection( record: RecordProxy, key: string, filters?: ?Object, ): ?RecordProxy, createEdge( store: RecordSourceProxy, connection: RecordProxy, node: RecordProxy, edgeType: string, ): RecordProxy, insertEdgeBefore( connection: RecordProxy, newEdge: RecordProxy, cursor?: ?string, ): void, insertEdgeAfter( connection: RecordProxy, newEdge: RecordProxy, cursor?: ?string, ): void, deleteNode(connection: RecordProxy, nodeID: string): void }getConnection(record, key, filters?)给定父记录、连接 key 和可选的 filters取回被connection指令标注的连接字段对应的RecordProxy。先看普通未加connection的连接字段如何访问fragment FriendsFragment on User { friends(first: 10) { edges { node { id } } } }未加指令时连接字段与普通字段一样访问// friends 连接记录可以这样取 const user store.get(userID); const friends user user.getLinkedRecord(friends); // 访问连接上的字段 const edges friends friends.getLinkedRecords(edges);使用 usePaginationFragment我们通常会给连接字段加上connection指令告诉 Relay 哪一部分需要分页fragment FriendsFragment on User { friends(first: 10, orderby: firstname) connection( key: FriendsFragment_friends, ) { edges { node { id } } } }对于这类连接ConnectionHandler帮助定位记录import {ConnectionHandler} from relay-runtime; // friends 连接记录可以这样取 const user store.get(userID); const friends ConnectionHandler.getConnection( user, // 父记录 FriendsFragment_friends, // 连接 key {orderby: firstname} // 用于识别连接的 filters ); // 访问连接上的字段 const edges friends.getLinkedRecords(edges);从源码看getConnection的实现ConnectionHandler.js先把key与 handle 名组合成 handle key再调用record.getLinkedRecord(handleKey, filters)完成定位。getRelayHandleKeygetRelayHandleKey.js会生成__key_connection形式的内部字段名这正是connection编译后写入 Store 的字段filters参与存储 key 的计算因此同一 key 下不同的 filters 会被识别为不同的连接实例。createEdge(store, connection, node, edgeType)给定store、连接记录、边指向的节点记录和边的类型名创建一条边并返回其RecordProxy。源码实现ConnectionHandler.js基于connection与node的 dataID 生成确定性的客户端 ID因此同一节点重复插入同一连接时不会产生重复 ID同时会把cursor显式设为null而非undefined避免被误判为缺失数据。insertEdgeBefore(connection, newEdge, cursor?)把边插入到连接的开头若指定cursor则插入到该游标之前。源码ConnectionHandler.js会读取edges列表按游标定位插入点找不到游标时回退为插入到最前面。insertEdgeAfter(connection, newEdge, cursor?)把边追加到连接的末尾若指定cursor则插入到该游标之后。源码ConnectionHandler.js在找不到游标时回退为追加到末尾。边的创建与插入示例const user store.get(userID); const friends ConnectionHandler.getConnection(user, FriendsFragment_friends); const newFriend store.get(newFriendId); const edge ConnectionHandler.createEdge(store, friends, newFriend, UserEdge); // 不传 cursor把边追加到末尾 ConnectionHandler.insertEdgeAfter(friends, edge); // 不传 cursor把边插入到最前面 ConnectionHandler.insertEdgeBefore(friends, edge);deleteNode(connection, nodeID): void给定连接删除所有node.id与给定 id 匹配的边。源码ConnectionHandler.js遍历edges逐个检查edge.node.dataID是否等于nodeID并将过滤后的列表写回。const user store.get(userID); const friends ConnectionHandler.getConnection(user, FriendsFragment_friends); ConnectionHandler.deleteNode(friends, idToDelete);典型实战组合在 updater 中更新连接综合以上 API一个常见的完整场景是mutation 成功后把新建的节点插入connection连接或删除某个节点。整体流程为通过store.getRootField(someMutationField)拿到 mutation 响应根字段用store.get(userID)定位父记录并用ConnectionHandler.getConnection拿到连接记录用ConnectionHandler.createEdge创建边再用insertEdgeBefore/insertEdgeAfter插入或直接用deleteNode删除目标节点对应的边。function todoUpdater(store) { const rootField store.getRootField(createTodo); const newTodo rootField.getLinkedRecord(todo); const user store.get(userID); const todos ConnectionHandler.getConnection( user, UserFragment_todos, ); const edge ConnectionHandler.createEdge(store, todos, newTodo, TodoEdge); ConnectionHandler.insertEdgeBefore(todos, edge); }以上模式在仓库测试中均有对应印证例如 RelayModernEnvironment-Connection-test.js 与 RelayModernEnvironment-ExecuteWithHandlerAndUpdater-test.js 覆盖了连接编辑、updater 执行以及connectionhandler 的实际调用链路连接 handler 的单测见 ConnectionHandler-test.js。小结与注意事项RecordSourceSelectorProxy负责Store 级操作增删记录、访问根字段、全局失效RecordProxy负责记录级操作读写字段与关联、单条失效ConnectionHandler负责连接级编辑。所有写操作都会进入 Store 的变更集changeset在 updater 执行完成后统一提交不要依赖写入的即时可见性之外的其他副作用。connection连接记录并非用字段名直接寻址而是通过__key_connection内部 handle key 与 filters 定位手工编辑连接时务必使用ConnectionHandler而非getLinkedRecord否则可能定位到错误的连接实例。invalidateStore与invalidateRecord都是标记过期真正触发 refetch 取决于后续的environment.check()/environment.retain等机制而不是立即清理数据。【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
Python木薯叶病虫害分类:Transformer源码工程从零到落地 简介:一套基于Transformer模型的木薯叶病虫害分类Python源码项目,适合作为期末大作业、课程设计或深度学习入门实践参考。源码已在本地编译通过,难度适中,内容经助教审定,可放心参考运行。压缩包共12个文件,… · 2026/9/23 6:44:47
前端框架表单自动填充失效?原理剖析与通用解决方案 不知道你有没有遇过这种经典场景:写油猴脚本、浏览器控制台调试、或者搞自动化脚本的时候,想给页面的用户名密码框自动填值。最直觉的写法就是:document.getElementById(username).value admin;
document.getElementById(password).value 1… · 2026/9/23 6:44:35
Ionic单选框组件开发实战与优化技巧 1. Ionic单选框组件深度解析作为移动端混合开发的主流框架,Ionic的单选框(Radio)组件在实际业务场景中使用频率极高。不同于Web原生的input radio,Ionic的单选框组件经过深度封装,在UI表现、交互体验和功能扩展上都做了… · 2026/9/23 6:44:35
射影几何与透视单应性矩阵(Homography Matrix):前端四点畸变矫正 射影几何与透视单应性矩阵(Homography Matrix):前端四点畸变矫正在计算机视觉(Computer Vision)、文档扫描识别 App(如扫描全能王)、以及网页端先锋 3D 投影海报映射中,“四点透视畸… · 2026/9/23 7:33:44
一块陶泥在拉坯机上的旋转与前端无级缩放的向心对称 一块陶泥在拉坯机上的旋转与前端无级缩放的向心对称在美院陶艺工坊幽静的下午,空气中弥漫着高岭土与潮湿泥浆的清香。
每一个初学陶艺的人,在拉坯机(Potters Wheel)前遭遇的第一场残酷洗礼,叫做——“找正(… · 2026/9/23 7:33:44
后端老鸟带你一文搞懂如何实名认证底层逻辑 后端老鸟带你一文搞懂如何实名认证底层逻辑 面试被问原理答不上来?别慌,很多后端开发在接支付或登录模块时,对“如何实名认证”这件事,只停留在调接口的层面。一旦面试官追问:“用户输入了身份证和姓名,后端到底怎么校验通过率的?如果并发请求怎么处理… · 2026/9/23 7:33:38
大模型推理实战:从框架选型到部署优化的完整指南 1. 大模型推理到底在做什么很多人第一次听到“大模型推理”这个词,脑子里浮现的是一台机器在“思考”。这个理解方向没错,但不够准确。我更喜欢用一个生活化的类比来解释:训练像是把一个学生从小学教到大学,推理则是这个学生毕业后… · 2026/9/23 7:33:38
Design Token 多品牌多租户架构:动态 CSS 变量分层注入实战 Design Token 多品牌多租户架构:动态 CSS 变量分层注入实战在大型企业级 B2B SaaS 平台与跨国集团数字产品矩阵建设中,“多品牌与白标定制(Multi-Brand Theming & White-Labeling)”是一项极具含金量的核心架构能力࿱… · 2026/9/23 7:33:38
红黑树封装map和set:从底层原理到迭代器与operator[]设计 红黑树写完别急着把它丢一边,真正的进阶题目是:怎么用同一棵自底向上改出来的红黑树,把标准库里的map和set同时造出来。这篇就是传说中的“28.C进阶:map和set封装”,核心围绕三点:insert的返回值设计、迭代… · 2026/9/23 7:33:38
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29