Relay Store API 参考:使用 RecordSourceSelectorProxy、RecordProxy 与 ConnectionHandler 编程式更新客户端数据
【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay
Relay 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 中:
RecordSourceSelectorProxy:updater收到的store的类型,负责在 Store 层面创建、删除、查找记录,以及访问根字段。RecordProxy:单条记录的读写代理,负责读取/写入字段值、关联记录,以及失效单条记录。ConnectionHandler:relay-runtime暴露的辅助工具,专门用于定位和编辑@connection连接字段(增删边等)。
从源码结构看,RecordSourceSelectorProxy继承自更底层的RecordSourceProxy(RelayStoreTypes.js),后者额外提供readUpdatableQuery/readUpdatableFragment以配合可更新数据读取;RecordSourceSelectorProxy则在 L561-L565 增加了getRootField、getPluralRootField两个访问 Selector 根字段的方法。接口注释也指出,这些代理接口的设计意图是"允许看起来像直接操作 Record,同时允许不同实现(例如记录变更集 changeset)",因此你写入的修改会由具体实现统一打包提交。
RecordSourceSelectorProxy
RecordSourceSelectorProxy是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();失效后,在重新获取之前检查查询将返回 stale:
environment.check(query) === 'stale'注意:v17 文档中用
=== 'stale'示意语义;在当前仓库的测试中,environment.check()实际返回的是{status: 'stale'}对象(见 RelayModernEnvironment-ExecuteMutationWithGlobalInvalidation-test.js),请在编写断言时以实际运行时返回值为准。
RecordProxy
RecordProxy是对单条记录读写操作的接口。官方文档的接口如下:
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,与之并存的还有只读变体ReadOnlyRecordProxy(L504-L510),用于只读场景。下面按方法逐一展开。
getDataID(): string
返回当前记录的dataID。
const id = record.getDataID();getType(): string
返回当前记录在 GraphQL schema 中定义的类型名。
const type = user.getType(); // "User"getValue(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); // 修改的是 `record`setLinkedRecord(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()中会返回 stale:
environment.check(query) === 'stale'invalidateRecord与invalidateStore的差异在于作用域:前者只影响引用该记录的查询,后者影响整个 Store。本地失效对应的测试见 RelayModernEnvironment-CheckWithLocalInvalidation-test.js。
ConnectionHandler
ConnectionHandler是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)完成定位。getRelayHandleKey(getRelayHandleKey.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),仅供参考