前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载本文是 Relayrelay-runtimeStore API 的权威技术参考。在 Relay 中mutation 的updater函数、乐观更新optimistic updates以及订阅subscription响应处理都需要通过命令式 API 直接操作客户端 Store。读完本文你将完整掌握RecordSourceSelectorProxy、RecordProxy、RecordSourceProxy三大核心接口的全部方法语义以及ConnectionHandler操作连接Connection的标准姿势并理解这些 API 在relay-runtime源码中的真实实现与底层行为。背景为什么需要直接操作 StoreRelay 的 Store 是客户端唯一的权威数据源。虽然绝大多数场景下useMutation配合optimisticResponse、updater或声明式指令如deleteRecord、appendEdge即可完成更新但在以下场景你必须直接面对 Store API自定义updater函数中需要精细地创建、删除、修改记录需要手动维护复杂的连接Connection边edge增删需要通过invalidateStore()/invalidateRecord()驱动数据失效与重取。updater函数收到的第一个参数store的类型就是RecordSourceSelectorProxy。它是一组以RecordProxy单条记录的读写句柄为基本单位的OO 风格接口底层实际读写的是 RelayRecordSourceMutator真正的类型定义位于 packages/relay-runtime/store/RelayStoreTypes.js具体实现类为 RelayRecordSourceProxy 与 RelayRecordProxy。本参考文档围绕四个部分展开RecordSourceSelectorProxyupdater拿到的 store 参数RecordProxy单条记录的读写接口RecordSourceProxy底层不带 selector 上下文的 store 接口ConnectionHandler连接操作工具模块RecordSourceSelectorProxyRecordSourceSelectorProxy是updater函数 接收到的store参数的类型。它在RecordSourceProxy的基础上额外提供访问 GraphQL 文档根字段的能力类型定义可参见 RelayStoreTypes.jsinterface 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; }在源码中RecordSourceSelectorProxy extends RecordSourceProxy即它包含RecordSourceProxy的所有方法create/delete/get/getRoot/invalidateStore/readUpdatableFragment/readUpdatableQuery这里为了可读性单独展开为 7 个方法。create(dataID: string, typeName: string): RecordProxy根据dataID和 GraphQL schema 中定义的typeName在 Store 中创建一条新记录返回一个用于修改该新记录的RecordProxy。示例const record store.create(dataID, Todo);从 RelayRecordSourceProxy.js 的实现可以看到create会先调用底层 mutator 创建记录再通过get(dataID)返回代理句柄如果创建失败记录不存在会直接抛出 invariant 断言错误。delete(dataID: string): void根据dataID从 Store 中删除一条记录。注意两点行为细节对于指向已删除记录的既有边edge默认情况下即使字段被类型标注为非空non-nullable读取时也会返回undefined当字段带有throwOnFieldError指令时缺失数据会直接抛出错误。示例store.delete(dataID);源码中delete有一个重要保护不允许删除根记录ROOT_ID否则抛出Cannot delete the root record.断言见 RelayRecordSourceProxy.js。get(dataID: string): ?RecordProxy根据dataID从 Store 中取出一条记录返回可读可写的RecordProxy记录不存在时返回null。示例const record store.get(dataID);底层实现会基于 mutator 的记录状态做缓存EXISTENT状态创建代理NONEXISTENT缓存null处于未知状态则返回undefined见 RelayRecordSourceProxy.js。getRoot(): RecordProxy返回代表 GraphQL 文档根root query / root mutation 等的RecordProxy。根记录的数据 ID 是固定的ROOT_ID类型为ROOT_TYPE实现中若根记录不存在会自动创建见 RelayRecordSourceProxy.js。示例给定如下 GraphQL 文档viewer { id }用法// Represents root query const root store.getRoot();getRootField(fieldName: string): ?RecordProxy根据 GraphQL 文档中定义的fieldName获取根字段对应的记录返回可读写的RecordProxy。示例给定如下 GraphQL 文档viewer { id }用法const viewer store.getRootField(viewer);getPluralRootField(fieldName: string): ?Array?RecordProxy根据 GraphQL 文档中定义的fieldName获取一个表示集合的根字段返回RecordProxy数组。典型场景是 mutation 返回的列表类型根字段。示例给定如下 GraphQL 文档nodes(first: 10) { # ... }用法const nodes store.getPluralRootField(nodes);invalidateStore(): void全局使 Relay Store 失效。任何在失效发生之前写入 Store 的数据都会被标记为 stale过期下一次查询通过environment.check()检查时将被视为需要重新获取refetch。示例store.invalidateStore();全局失效之后任何在重新获取之前被检查的查询都会被视为 staleenvironment.check(query) stale在实现层面invalidateStore()只是把内部标志位_invalidatedStore置为true见 RelayRecordSourceProxy.js随后的发布过程会把该状态合并进 Store驱动DataChecker在下次检查时判定数据过期。这一机制与 fetch policy 的store-or-network行为紧密相关参见 fetch policies。RecordProxyRecordProxy是单条记录的变更接口几乎所有的 Store 操作最终都收敛到这个类上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; }对应实现类是 RelayRecordProxy。值得注意的实现细节所有字段读写getValue/setValue/getLinkedRecord等都会先通过getStableStorageKey(name, args)把字段名 参数归一化成稳定的存储键storage key因此同一个字段在不同参数下会存为不同的存储键。getDataID(): string返回当前记录的dataID。示例const id record.getDataID();getType(): string获取当前记录在 GraphQL schema 中定义的类型。若记录已被删除会抛出Cannot get the type of deleted record断言见 RelayRecordProxy.js。示例const type user.getType(); // UsergetValue(name: string, arguments?: ?Object): mixed根据字段名获取当前记录中某个字段的标量值。如果字段带有参数可以传入一个variables对象参数包。示例给定 GraphQL 文档viewer { id name }用法const name viewer.getValue(name);带参数版本的示例给定文档viewer { id name(arg: $arg) }用法const name viewer.getValue(name, {arg: value});getLinkedRecord(name: string, arguments?: ?Object): ?RecordProxy根据 GraphQL 文档中定义的字段名获取与当前记录关联的单条记录返回RecordProxy。如果关联字段带参数同样可以传入参数包。示例给定 GraphQL 文档rootField { viewer { id name } }用法const rootField store.getRootField(rootField); const viewer rootField.getLinkedRecord(viewer);带参数版本给定文档rootField { viewer(arg: $arg) { id } }用法const rootField store.getRootField(rootField); const viewer rootField.getLinkedRecord(viewer, {arg: value});getLinkedRecords(name: string, arguments?: ?Object): ?Array?RecordProxy根据 GraphQL 文档中定义的字段名获取与当前记录关联的一组记录列表返回RecordProxy数组。带参数时传入参数包。示例给定 GraphQL 文档rootField { nodes { # ... } }用法const rootField store.getRootField(rootField); const nodes rootField.getLinkedRecords(nodes);带参数版本给定文档rootField { nodes(first: $count) { # ... } }用法const rootField store.getRootField(rootField); const nodes rootField.getLinkedRecords(nodes, {count: 10});getOrCreateLinkedRecord(name: string, typeName: string, arguments?: ?Object)根据 GraphQL 文档中定义的字段名获取关联记录如果该关联记录不存在则按给定的typeName创建它返回RecordProxy。示例给定 GraphQL 文档rootField { viewer { id } }用法const rootField store.getRootField(rootField); const newViewer rootField.getOrCreateLinkedRecord(viewer, User); // Will create if it doesnt exist带参数时同样可传入参数包。从 RelayRecordProxy.js 的实现可以看到如果关联记录不存在它会基于父记录 ID 稳定存储键生成一个客户端 IDgenerateClientID先尝试从 store 中取已存在的客户端记录处理字段未设置但记录已存在的边界情况否则创建新记录然后通过setLinkedRecord把字段指过去。setValue(value: mixed, name: string, arguments?: ?Object): RecordProxy在指定字段上设置新值来变更当前记录返回被修改的记录支持链式调用。示例给定 GraphQL 文档viewer { id name }用法viewer.setValue(New Name, name);带参数版本viewer.setValue(New Name, name, {arg: value});一个重要的实现约束setValue只允许设置标量或标量数组null、非对象类型、或元素全部为标量的数组传对象等复杂结构会触发Expected a scalar or array of scalars断言见 RelayRecordProxy.js。这一限制是类型安全更新器typesafe updaters的基石。copyFieldsFrom(sourceRecord: RecordProxy): void把传入记录sourceRecord上的所有字段复制到当前记录从而变更当前记录。示例const record store.get(id1); const otherRecord store.get(id2); record.copyFieldsFrom(otherRecord); // Mutates recordsetLinkedRecord(record: RecordProxy, name: string, arguments?: ?Object)在指定字段上设置一条新的关联记录从而变更当前记录。传入的record必须是RelayRecordProxy实例否则抛出断言。示例给定 GraphQL 文档rootField { viewer { id } }用法const rootField store.getRootField(rootField); const newViewer store.create(/* ... */); rootField.setLinkedRecord(newViewer, viewer);带参数时同样传入参数包。注意在 RelayRecordProxy.js 的实现中setter 系列方法都返回this因此可以链式调用。setLinkedRecords(records: ArrayRecordProxy, name: string, variables?: ?Object)在指定字段上设置一组新的关联记录从而变更当前记录。注意源码要求records必须是数组见 RelayRecordProxy.js。示例给定 GraphQL 文档rootField { nodes { # ... } }用法const rootField store.getRootField(rootField); const newNode store.create(/* ... */); const newNodes [...rootField.getLinkedRecords(nodes), newNode]; rootField.setLinkedRecords(newNodes, nodes);带参数时同样传入参数包。invalidateRecord(): void使当前记录失效。任何引用了该记录的查询在下次environment.check()检查时都会被判定为 stale需要重新获取。示例const record store.get(4); record.invalidateRecord();失效某条记录后任何引用了该记录且在被重新获取前被检查的查询都会被视为 staleenvironment.check(query) stale实现上invalidateRecord()只是把当前 dataID 加入 source proxy 的失效集合_idsMarkedForInvalidation见 RelayRecordProxy.js 与 RelayRecordSourceProxy.js。RecordSourceProxyRecordSourceProxy是脱离 selector 上下文的 Store 变更接口。它暴露了许多底层 API并非类型安全not typesafe。注意如果你的使用场景可以被以下替代方案覆盖应优先考虑它们而不是直接使用RecordSourceProxy的低层 API类型安全更新器typesafe updaters乐观更新optimistic updatesRelay Resolversinterface RecordSourceProxy { create(dataID: DataID, typeName: string): RecordProxy; delete(dataID: DataID): void; get(dataID: DataID): ?RecordProxy; getRoot(): RecordProxy; invalidateStore(): void; readUpdatableFragmentTFragmentType: FragmentType, TData( fragment: UpdatableFragmentTFragmentType, TData, fragmentReference: HasUpdatableSpreadTFragmentType, ): UpdatableDataTData; readUpdatableQueryTVariables: Variables, TData( query: UpdatableQueryTVariables, TData, variables: TVariables, ): UpdatableDataTData; }create(dataID: DataID, typeName: string): RecordProxy根据dataID和 GraphQL schema 中定义的typeName创建一条新记录返回可修改新记录的RecordProxy。语义与RecordSourceSelectorProxy.create完全一致。示例const record store.create(dataID, Todo);delete(dataID: DataID): void根据dataID删除记录。对于指向已删除记录的既有边默认情况下即使字段被类型标注为非空也会返回undefined当字段带有throwOnFieldError指令时缺失数据将抛出错误。示例store.delete(dataID);get(dataID: DataID): ?RecordProxy根据dataID获取记录返回可读写read/mutate的RecordProxy。示例const record store.get(dataID);getRoot(): RecordProxy返回代表 GraphQL 文档根的RecordProxy。示例给定 GraphQL 文档viewer { id }用法// Represents root query const root store.getRoot(); // Get the viewer linked record const viewer root.getLinkedRecord(viewer);invalidateStore(): void全局使 Relay Store 失效。失效之前写入的数据将被视为 stale下一次查询通过environment.check()检查或使用store-or-networkfetch policy 获取时都会触发重新获取。示例store.invalidateStore();全局失效后任何在重新获取前被检查的查询都会被视为 staleenvironment.check(query) stalereadUpdatableFragment(fragment: UpdatableFragmentTFragmentType, TData, fragmentReference: HasUpdatableSpreadTFragmentType): UpdatableDataTData从 Store 中读取一个可更新 fragmentupdatable fragment返回的updatableData中的字段可以被命令式地直接修改从而更新 Store 数据。这正是类型安全命令式更新的核心入口。关于命令式修改 Store 数据的完整说明参见 imperatively modifying store data 一节。示例const fragment graphql fragment StoryLikeButton_updatable on Story updatable { likeCount doesViewerLike } ; const { updatableData } store.readUpdatableFragment( fragment, story ); updatableData.likeCount updatableData.likeCount 1底层实现由 readUpdatableFragment.js 完成并通过RecordSourceProxy的readUpdatableFragment方法转发见 RelayRecordSourceProxy.js。readUpdatableQuery(query: UpdatableQueryTVariables, TData, variables: TVariables): UpdatableDataTData从 Store 中读取一个可更新查询updatable query其字段可以被命令式地直接修改以更新 Store。与readUpdatableFragment不同你不需要传入fragmentReference只需传入查询和变量。示例const {updatableData} store.readUpdatableQuery( graphql query NameUpdaterUpdateQuery updatable { viewer { name } } , {} ); const viewer updatableData.viewer; viewer.name newName;ConnectionHandlerConnectionHandler是relay-runtime导出的工具模块专门用于操作连接Connection。它屏蔽了连接记录带connection指令的存储细节让你可以按连接 key filters定位连接并增删边。其实现位于 packages/relay-runtime/handlers/connection/ConnectionHandler.js配套测试见 ConnectionHandler-test.js。ConnectionHandler暴露的接口如下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内部通过getRelayHandleKey(connection, key, null)生成 handle key形如__connection_key再以record.getLinkedRecord(handleKey, filters)定位客户端连接记录见 ConnectionHandler.js。该模块同时导出getConnectionID计算连接记录 ID与update默认的 connection 字段运行时 handler负责把服务端返回的边按分页语义合并进客户端连接。getConnection(record: RecordProxy, key: string, filters?: ?Object)给定一条记录、一个连接 key以及可选的一组 filtersgetConnection会取出一个代表带有connection指令的连接的RecordProxy。首先看一个普通的连接字段fragment FriendsFragment on User { friends(first: 10) { edges { node { id } } } }访问这种普通连接字段的方式与其他普通字段一样// The friends connection record can be accessed with: const user store.get(userID); const friends user user.getLinkedRecord(friends); // Access fields on the connection: 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; // The friends connection record can be accessed with: const user store.get(userID); const friends ConnectionHandler.getConnection( user, // parent record FriendsFragment_friends, // connection key {orderby: firstname} // filters that is used to identify the connection ); // Access fields on the connection: const edges friends.getLinkedRecords(edges);边的创建与插入createEdge(store: RecordSourceProxy, connection: RecordProxy, node: RecordProxy, edgeType: string)给定一个store、一个连接、边的节点node和边类型edgeType创建一条边并返回新的边记录。实现上createEdge用连接 ID 节点 ID生成确定性的客户端边 IDgenerateClientID(record.getDataID(), node.getDataID())如果该边已存在则直接复用随后把node链接到边的node字段并把cursor字段设为null用null而不是undefined以避免被当作缺失数据处理见 ConnectionHandler.js。insertEdgeBefore(connection: RecordProxy, newEdge: RecordProxy, cursor?: ?string)给定连接把边插入到连接的开头若提供了cursor则插入到指定 cursor 对应的边之前。insertEdgeAfter(connection: RecordProxy, newEdge: RecordProxy, cursor?: ?string)给定连接把边追加到连接的末尾若提供了cursor则插入到指定 cursor 对应的边之后。实现细节见 ConnectionHandler.js若连接当前没有边两者都会直接setLinkedRecords([newEdge], edges)带 cursor 插入时如果遍历完所有边都没找到匹配的 cursorinsertEdgeAfter会把边追加到末尾insertEdgeBefore会把边放到最前面。示例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); // No cursor provided, append the edge at the end. ConnectionHandler.insertEdgeAfter(friends, edge); // No cursor provided, insert the edge at the front: ConnectionHandler.insertEdgeBefore(friends, edge);deleteNode(connection: RecordProxy, nodeID: string): void给定一个连接删除所有node.id与给定 ID 匹配的边。示例const user store.get(userID); const friends ConnectionHandler.getConnection(user, FriendsFragment_friends); ConnectionHandler.deleteNode(friends, idToDelete);deleteNode的实现会遍历连接的edges通过每条边的node关联记录的 dataID 与nodeID比对收集需要保留的边后整体写回见 ConnectionHandler.js。这也提醒我们deleteNode移除的是边节点记录本身并不会被删除。实战组合一次完整的 mutation updater 编写综合以上 API一个典型的创建 Todo 并追加到连接的 updater 可以这样组织基于 graphql-mutations 的约定function updater(store, response) { // 1. 取出 mutation 根字段上的新记录 const todoEdge store.getRootField(createTodo)?.getLinkedRecord(todoEdge); const todoNode todoEdge?.getLinkedRecord(node); if (!todoEdge || !todoNode) { return; } // 2. 找到用户记录与其 friends 连接 const user store.get(userID); const friends ConnectionHandler.getConnection( user, FriendsFragment_friends, ); if (friends null) { return; } // 3. 把新边插入到连接最前面 ConnectionHandler.insertEdgeBefore(friends, todoEdge); }配合invalidateStore()或invalidateRecord()可以在数据模型复杂、难以逐字段维护时简单粗暴地让相关查询在下一次检查时重新获取——但要注意这会带来一次额外的网络往返应权衡使用。小结与选型建议API 分组典型用途是否类型安全RecordSourceSelectorProxyupdater的 store 参数mutation / optimistic update / subscription updater否低层RecordProxy单条记录的读写、链式修改否低层RecordSourceProxy脱离 selector 的 Store 操作、readUpdatableFragment/Query部分readUpdatableFragment/readUpdatableQuery类型安全的命令式更新是ConnectionHandler连接定位、边增删否低层优先顺序建议能用声明式指令或readUpdatableFragment/readUpdatableQuery解决的场景优先使用类型安全方案只有当需要精细控制记录创建、字段拷贝、连接边操作时才直接使用RecordProxy与ConnectionHandler。所有接口的类型定义都可以在 packages/relay-runtime/store/RelayStoreTypes.js 中查阅实现细节可深入 RelayRecordSourceProxy.js、RelayRecordProxy.js 与 ConnectionHandler.js 研读。赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay Store API 完全指南用 RecordSourceSelectorProxy、RecordProxy 与 ConnectionHandler 编程式更新客户端数据Relay Store API 完全指南用 RecordSourceSelectorProxy、RecordProxy 与 ConnectionHandler前端开发工具Relay 13 Store API 完全指南RecordSourceSelectorProxy、RecordProxy 与 ConnectionHandler 实战解析Relay 13 Store API 完全指南RecordSourceSelectorProxy、RecordProxy 与 ConnectionHandle前端开发工具Relay Store 编程接口完全指南RecordSourceSelectorProxy、RecordProxy 与 ConnectionHandler 实战参考Relay Store 编程接口完全指南RecordSourceSelectorProxy、RecordProxy 与 ConnectionHandler 实前端开发工具上一篇ShardingSphere配置详解YAML与Properties配置全解析下一篇5分钟上手DB-GPT智能合约用AI自动化业务逻辑的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考