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),仅供参考
