前端【免费下载链接】urqlThe highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.项目地址https://gitcode.com/gh_mirrors/ur/urql点击查看免费下载在 urql 的 Graphcache 规范化缓存中查询结果会以实体为节点、链接为边被自动写入缓存但当mutation或subscription返回的数据粒度不足以覆盖所有受影响的实体例如列表新增、删除、字段排序变化时就需要通过updates配置编写更新器updater来手动维护缓存中的链接与记录。本文基于 docs/graphcache/cache-updates.md 展开结合 exchanges/graphcache 的源码实现系统讲解cache.writeFragment、cache.updateQuery、cache.link、cache.inspectFields、cache.invalidate与乐观更新optimistic的完整用法。读完本文你将掌握如何判断一个 mutation 是否需要更新器、如何用四种 cache 方法维护实体与列表、如何精准失效单个字段或整个类型以及如何用乐观更新让应用瞬间响应。从规范化缓存说起为什么 mutation 结果不能总是自动更新一切要理解 Cache Updates 的价值需要先回顾 Graphcache 的存储模型详见 规范化缓存当 Graphcache 收到 API 结果时它会遍历结果并将所有数据以规范化结构存入缓存——每个可键控keyable的实体都按其实体键如Todo:1存储查询结果是一张从根实体Query出发的图也可理解为树Query通过**链接link连接到其他实体链接本质上是存储的实体键每个实体内部又用记录record**保存标量值即树的叶子。正如 规范化缓存 中所述任何 mutation 或 subscription 也可以被写入这个数据结构。一旦 Graphcache 在其结果中发现可键控实体就会把它写入关系表这可能更新应用中其他查询。也就是说mutation 和 subscription 的结果同样会被写入并更新缓存中的实体这些更新会自动反映到应用所有活跃查询上。但这种方式有局限resolver 可以被动地改变查询数据的形态详见 本地 Resolver而mutation 和 subscription 往往无法被动计算数据——当它们返回的结果比缓存更新所有受影响实体所需的信息更碎片化时我们就必须编写updater更新器来主动更新链接与关系。updates配置与 updater 的四个参数cacheExchange的updates选项接收一个映射键为Mutation或Subscription值为各字段对应的更新函数。这些函数与 本地 Resolver 以及 GraphQL.js 服务端 resolver 的形态类似cacheExchange({ updates: { Mutation: { mutationField: (result, args, cache, info) { // ... }, }, Subscription: { subscriptionField: (result, args, cache, info) { // ... }, }, }, });在 exchanges/graphcache/src/types.ts 中UpdateResolver的类型签名确认了这一结构——它接收四个位置参数并返回voidresult对应类型定义中的parent正在被写入缓存的完整 API 结果。通常我们应避免耦合只关注 updater 所挂载字段的数据但值得注意的是你可以访问结果的任何部分。args该字段被调用时携带的参数若字段未携带参数则被替换为空对象{}。cachecache实例提供读写本地缓存的方法其完整 API 见 API 文档。本文后面会频繁使用它来读写缓存。info不应频繁使用但它包含查询文档遍历的运行时信息如variables、parent、parentKey等可用于让 resolver/updater 可复用或获取整个查询的信息完整 API 见 API 文档。由于 updater 的返回值被忽略TypeScript 中类型为void它们对cache实例的所有调用都是副作用——这些副作用可能触发额外的缓存变更并在修改的同时更新所有受影响查询。从源码 exchanges/graphcache/src/operations/write.ts 可以看到updater 是在字段被正常写入缓存之后才执行的We run side-effect updates after the default, normalized updates so that the data is already available in-store if necessary此时被更新的数据已经就位updater 可以直接读取。Graphcache v7 起的默认 mutation 失效行为从 Graphcache v7 开始见 exchanges/graphcache/CHANGELOG.md没有配置updates.Mutation.fieldName更新器的 mutation拥有一套回退行为如果 mutation 返回了一个在缓存中尚找不到的实体Graphcache 将其视为一次创建createmutationGraphcache 随后会失效invalidate与该返回实体__typename相同的所有已缓存实体从而触发相关查询重新请求。只要为该 mutation 字段定义了 updater这套回退行为就不再运行你的 updater 将完全接管 mutation 写入之后的处理。这段回退逻辑在源码中清晰可见exchanges/graphcache/src/operations/write.ts 中的else if (typename ctx.store.rootFields[mutation] !ctx.optimistic)分支会检查 mutation 返回的字段值若为对象或数组则通过store.keyOfEntity计算其实体键再用readRecord(key, __typename)与getRefCount(key)判断该实体是否已存在于缓存若没有已解析的__typename或引用计数为 0就调用invalidateType(fieldValue.__typename, ...)失效该类型下的其他实体。什么时候才需要编写缓存更新器设计良好的 GraphQL schema 通常不需要编写太多缓存更新器。例如一个更新User用户名的 mutation如果它也查询并返回了User实体就能平凡地自动更新缓存query User($id: ID!) { user(id: $id) { __typename # User id username } } mutation UpdateUsername($id: ID!, $username: String!) { updateUser(id: $id, username: $username) { __typename # User id username } }上例中Query.user返回UserMutation.updateUser也查询并返回同一个User因此更新后的用户名会被 Graphcache 自动应用。如果 mutation 字段不返回User这一自动更新就不可能发生——虽然仍可为它编写 updater但这通常意味着 schema 设计不佳。updater 真正变得不可或缺的场景是mutation 无法合理地返回已变更的内容或我们无法手动定义一个能选出所有可能变更字段的选择集。常见例子Mutation.deleteUser需要失效一个实体Mutation.createUser某个列表现在可能需要包含新实体Mutation.createBook某个既有实体如User.books字段的链接需要更新。简而言之**任何我们无法通过 mutation 直接查询到的关系链接**都可能需要编写缓存更新器——因为这些数据变更 Graphcache 无法自行看到并存储。后文 cache.link 小节 会说明cache.link用于把字段指向不同的实体即把一个实体字段的关系更新到另一个或一组子实体这是最常见的更新需求优先尝试使用cache.link除非你需要更新的是标量。手动更新实体cache.writeFragment如果 mutation 字段的结果没有返回它要更新的完整实体Graphcache 就无法自动更新该实体。例如mutation UpdateTodo($todoId: ID!, $date: String!) { updateTodoDate(id: $todoId, date: $date) }这里Mutation.updateDate解析为一个标量而非完整的Todo对象类型。修复方式之一是修改 API schema让该 mutation 返回完整的Todo实体这样 mutation 就能写成下面这样并让Todo在缓存中自动更新mutation UpdateTodo($todoId: ID!, $date: String!) { updateTodoDate(id: $todoId, date: $date) { ...Todo_date } } fragment Todo_date on Todo { id updatedAt }如果无法修改 schema则可以编写 updater通过cache.writeFragment手动更新Todo实体import { gql } from urql/core; cacheExchange({ updates: { Mutation: { updateTodoDate(_result, args, cache, _info) { const fragment gql fragment _ on Todo { id updatedAt } ; cache.writeFragment(fragment, { id: args.id, updatedAt: args.date }); }, }, }, });cache.writeFragment与 本地 Resolver 页 介绍过的cache.readFragment类似区别在于它不是读取而是向缓存写入数据。注意上例使用了gql标签函数见 core API 文档因为writeFragment只接受 GraphQLDocumentNode作为输入而不接受字符串。从源码看其写入路径exchanges/graphcache/src/store/store.ts 中writeFragment调用_writeFragmentexchanges/graphcache/src/operations/write.ts后者会先从文档中提取片段、用{ __typename: typename, ...data }补全数据再通过store.keyOfEntity(dataToWrite)生成实体键——如果无法生成键缺少id/_id且无自定义keys配置写入会被拒绝并告警。这与 规范化缓存 中自定义键的规则一致。缓存更新不能发生在 updater 之外缓存更新不能在updates的函数之外进行。如果试图把cache存到变量里、并在任何updates函数或其他回调如resolvers之外调用它的方法Graphcache 会直接抛错。原因在于这些方法之所以不能被外部调用是因为所有更新都被隔离为对 mutation 和 subscription 事件的响应式处理。Graphcache 不允许带外out-of-band更新因为缓存只应代表服务器的状态——这一限制让缓存数据始终忠实于 API 结果行为更加可预测。如果你真的在配置回调之外调用了 cache 方法会收到 (2) Invalid Cache Call 错误详见 错误说明。该限制的底层实现位于 exchanges/graphcache/src/store/data.tsgetCurrentDependencies中的 invariant 断言了全局数据状态非空错误文案正是 Invalid Cache call: The cache may only be accessed or mutated during operations like write or query, or as part of its resolvers, updaters, or optimistic configs.同样地data.ts 中还有一个 invariant 会拒绝在缓存读取期间写入Invalid Cache write: ... Accesses tocache.writeFragment,cache.updateQuery, andcache.linkmay not be made insideresolversfor instance.。任意类型上的 updater缓存更新可以配置在任意类型上而不仅限于Mutation或Subscription字段。不过这可能有潜在危险是一个容易踩的坑——它之所以被允许是因为能实现一些巧妙的技巧和变通。给定一个任意类型上的 updater例如Todo.author每当该字段被写入时我们都可以链式追加更新。该 updater 会在任何操作mutation、query、subscription期间被 Graphcache 触发触发时允许我们向该字段追加更多任意更新。注意如果你打算用它来实现嵌套在对象类型上的 mutation例如Mutation.author.updateName请先考虑修改 schema 再使用这个特性。命名空间的 mutation 不被推荐并且使用多个嵌套 mutation 字段会把执行顺序从顺序改为并发。更新列表或链接cache.updateQuery与cache.link创建新实体的 mutation 很常见在创建类 mutation 结果返回时更新缓存是常见的做法因为可以避免对 API 的一次额外往返。虽然这类 mutation 也可以返回携带列表的受影响实体但列表通常位于Query根类型之上或之下的字段意味着要发送相当大的 API 结果——当页数很多时尤其不可行。因此大多数 schema 选择只返回刚创建的实体mutation NewTodo($text: String!) { createTodo(id: $todoId, text: $text) { id text } }如果Query.todos上有一个包含所有Todo实体的对应字段就需要创建 updater 把新Todo自动加入列表cacheExchange({ updates: { Mutation: { createTodo(result, _args, cache, _info) { const TodoList gql { todos { id } } ; cache.updateQuery({ query: TodoList }, data { return { ...data, todos: [...data.todos, result.createTodo], }; }); }, }, }, });这里用到cache.updateQuery它与 本地 Resolver 页 中的cache.readQuery类似它接受一个回调回调会收到从本地缓存读出的查询data我们返回一个更新后的数据版本。虽然我们可能本能地想不可变地复制并修改这份数据但实际上直接修改它也是允许的——因为它只是缓存读出的一份副本。需要注意如果缓存中没有足够信息满足查询这份data可能是null。这一点很重要因为updater 中的 cache 方法并不会应用 resolvers——所有 resolver 都会被忽略因此不可能意外把转换后的数据提交到缓存。例如你可以安全地为Todo.createdAt添加 resolver而不必担心 updater 误把它写进缓存内部数据结构。从源码看updateQuery的实现exchanges/graphcache/src/store/store.ts会先通过createRequest构造请求并readQuery读取数据然后把 updater 回调的结果交给_write写回如果回调返回null则不会发生写入。单独写链接cache.link只要更新的只是链接即关系我们也可以使用cache.link方法。它是 本地 Resolver 页 中cache.resolve的写入等价物。我们可以用它更新缓存中的任何关系所以上面的例子也可以用cache.link与cache.resolve重写而不用cache.updateQuerycacheExchange({ updates: { Mutation: { createTodo(result, _args, cache, _info) { const todos cache.resolve(Query, todos); if (Array.isArray(todos)) { cache.link(Query, todos, [...todos, result.createTodo]); } }, }, }, });cache.link可以与其他方法组合使用而不仅仅是cache.resolve例如与cache.inspectFields搭配效果很好。不过当你要写**记录即标量值**时cache.writeFragment和cache.updateQuery仍是仅有的可用方法。由于这类数据通常已被规范化缓存自动写入更新链接常常是我们唯一需要做的修改。从源码看cache.link的实现exchanges/graphcache/src/store/store.ts接受实体key 或可键控实体、字段名以及可选的参数通过keyOfEntity计算实体键后用keyOfField组合字段键最后调用内部InMemoryData.writeLink写入链接传入的实体或实体数组会经ensureLinkexchanges/graphcache/src/operations/shared.ts转换为实体键——如果传入的实体无法生成键会触发告警。更新大量未知链接cache.inspectFields上一节我们看到了 mutation 结果进入缓存时如何更新数据如列表但例子只针对已知字段上的单一列表比较简单。在实际 schema 中分页非常常见例如删除一个 todo 时需要更新的列表可能变得不可知我们无法提前知道已经访问过多少页以及每页的变量。事实上这份知识不应该对 Graphcache 可见——查询Client是完全独立的关注点通常与 UI 代码的某一部分放在一起。mutation RemoveTodo($id: ID!) { removeTodo(id: $id) }假设有上述 mutation它按 ID 删除Todo实体。我们的应用可能通过多次向 API 发送独立查询来分页查询这些条目这让我们很难知道应该检查哪些字段query PaginatedTodos($skip: Int) { todos(skip: $skip) { id text } }此时我们可以改用cache.inspectFields内省实体的字段动态找出可能需要更新的字段。该方法的用法见 API 文档是传入一个 key 或可键控实体——就像 本地 Resolver 页 中cache.keyOfEntity接受的内容或cache.resolve的第一个参数cacheExchange({ updates: { Mutation: { removeTodo(_result, args, cache, _info) { const TodoList gql query (skip: $skip) { todos(skip: $skip) { id } } ; const fields cache .inspectFields(Query) .filter(field field.fieldName todos) .forEach(field { cache.updateQuery( { query: TodoList, variables: { skip: field.arguments.skip }, }, data { data.todos data.todos.filter(todo todo.id ! args.id); return data; } ); }); }, }, }, });为实现removeTodo的 updater我们用cache.inspectFields(Query)获取Query根实体上的所有已知字段列表。每个字段被描述为包含三个属性的对象对应 exchanges/graphcache/src/types.ts 中FieldInfo接口fieldName字段名本例中我们筛选所有todos列表字段。arguments该字段的参数——由于接受参数的字段可以用不同参数多次访问这里我们读取arguments.skip以找出所有独立的分页页。fieldKey字段的键当需要通过cache.resolve(entityKey, fieldKey)取回字段时很有用可以避免反复序列化参数。总结一下在例子中我们把字段列表筛选到只剩todos字段然后遍历该字段的每一组arguments过滤所有列表以移除被删除的Todo。实现提示inspectFields在 exchanges/graphcache/src/store/store.ts 中通过InMemoryData.inspectFields(entityKey)返回字段列表types.ts 的注释也提醒该方法需要解码字段键理论上比简单缓存查找更慢建议只在 updater 中使用。检查任意实体的字段我们不必只检查Query根实体上的字段可以向cache.inspectFields传入任意部分可键控实体或 key 来检查任何实体的字段。例如如果我们有一个Todo实体想获取它所有已知字段可以传入一个部分Todo实体cache.inspectFields({ __typename: Todo, id: args.id, });失效实体Invalidation坦白说有时为所有 mutation 编写 updater 几乎是不可能的。我们甚至很难预测 API 收到 mutation 后会做什么——实体的更新可能改变列表的排序或以我们无法预料的方式把条目移出列表因为我们无法访问完整数据库在本地运行 API。在这种情况下更可取的做法是触发重新请求refetch让缓存通过把与失效数据相关的查询再次发送到 API 来自我更新。这个过程称为失效invalidation因为它从 Graphcache 的本地缓存数据中删除了数据。我们可以使用cache.invalidate来失效整个实体或单个字段。它拥有与cache.resolve相同的签名也见 本地 Resolver 页。前面写的更新可以用一次cache.invalidate简化cacheExchange({ updates: { Mutation: { removeTodo(_result, args, cache, _info) { cache.invalidate({ __typename: Todo, id: args.id, }); }, }, }, });与其他缓存更新一样这会导致所有使用该Todo实体的查询针对缓存进行更新由于我们已经失效了它们使用的Todo这些查询会被重新请求并发往 API。如果启用了 Schema Awareness这些查询的结果可能暂时以部分结果更新但通常我们会观察到数据被失效的查询会被重新请求因为部分数据不再缓存。从源码看exchanges/graphcache/src/store/store.ts 与 exchanges/graphcache/src/operations/invalidate.tsinvalidate先通过keyOfEntity生成实体键再调用invalidateEntity——它会检查传入字段是否存在存在则只对keyOfField(field, args)这一个字段写入undefined不存在则遍历inspectFields得到的全部字段并逐一置空若实体键生成失败会触发 invariant 19 报错。失效单个字段我们可能只想失效个别字段因为也许不是所有查询都需要立即更新。可以给cache.invalidate传入字段以及可选参数来只失效单个字段。例如可以用它失效列表而不是失效实体本身——如果你知道修改实体会导致列表重新排序这会很有用cacheExchange({ updates: { Mutation: { updateTodo(_result, args, cache, _info) { const key Query; const fields cache .inspectFields(key) .filter(field field.fieldName todos) .forEach(field { cache.invalidate(key, field.fieldKey); // 或者用另一种写法 cache.invalidate(key, field.fieldName, field.arguments); }); }, }, }, });这个例子把 updater 挂在Mutation.updateTodo字段上我们通过cache.inspectFields枚举所有todos列表字段然后精准失效这些字段从而让所有使用这些列表字段的查询被重新请求。注field.fieldKey形式与field.fieldName field.arguments形式等价——前者由 exchanges/graphcache/src/store/keys.ts 的keyOfField组合生成invalidateEntity内部也正是用keyOfField(field, args)定位目标字段。失效整个类型我们还可以失效某一给定类型的所有实体这在列表更新、或不确定哪个实体受影响时非常方便。只需向invalidate传入相关__typename即可cacheExchange({ updates: { Mutation: { deleteTodo(_result, args, cache, _info) { cache.invalidate(Todo); }, }, }, });源码层面当传入的是字符串、且没有字段和参数、且resolve(entity, __typename)无结果时exchanges/graphcache/src/store/store.tsinvalidate会走invalidateType分支exchanges/graphcache/src/operations/invalidate.ts通过getEntitiesForType(typename)取得该类型全部实体并逐一失效。乐观更新Optimistic Updates如果我们已经知道 mutation 可能返回什么结果为什么要等待 GraphQL API 完成 mutation 呢在updates配置之外cacheExchange还接受optimistic选项。这是一个工厂函数集合允许我们为 mutation 创建一个虚拟结果。这个临时结果可以立即应用到缓存给用户造成 mutation 已即时执行的错觉——这是减少等待时间、让应用感觉更敏捷的绝佳方法。该技术常用于一次性且假定会成功的 mutation比如给仓库加星、给推特点赞此时让交互尽可能即时是非常理想的。optimistic配置与resolvers/updates配置相似区别在于它只接收一张 mutation 字段的映射。我们可以为任何 mutation 字段挂乐观函数使其在Client等待 API 响应期间生成一个应用到缓存的乐观结果。乐观函数接收三个位置参数与 resolver/updater 的参数一致唯独没有第一个parent参数因为我们没有可处理的服务器数据数据是创建的args字段被调用时携带的参数若字段未携带参数则替换为空对象{}。cachecache实例提供读写本地缓存的方法完整 API 见 API 文档。info不应频繁使用包含查询文档遍历的运行时信息可用于让 resolver 可复用或获取整个查询的信息完整 API 见 API 文档。当运行一个包含一个或多个乐观 mutation 字段的 mutation 时Graphcache 会捕获它们并生成即时变更应用到缓存resolvers函数也会像处理真实服务器结果一样被触发。这个修改是临时的一旦 API 结果返回它就会被回滚让缓存处于可应用真实结果的状态。注意在乐观 mutation 等待 API 结果期间所有可能改变我们乐观数据的查询都会被暂停或者说排队所有乐观 mutation 会在同一时刻被回滚。这意味着乐观结果可以叠加但绝不会与配置中的真实数据混淆。假设我们想为favoriteTodomutation 实现乐观结果mutation FavoriteTodo(id: $id) { favoriteTodo(id: $id) { id favorite updatedAt } }mutation 相当简单我们要做的只是创建一个模拟 API 假定返回结果的函数const cache cacheExchange({ optimistic: { favoriteTodo(args, cache, info) { return { __typename: Todo, id: args.id, favorite: true, }; }, }, });这个乐观 mutation 会被应用到缓存如果存在针对Mutation.favoriteTodo的updates配置它会用乐观结果执行。一旦 mutation 结果从 API 返回这个临时变更会被回滚并丢弃。在上面的乐观 mutation 函数中可以看到updatedAt不在乐观返回值里——因为我们不必也无法完全匹配 mutation 的选择集。Graphcache 会跳过缺失的字段对未提供的字段使用已缓存的字段。这甚至可以作用于嵌套实体和字段。不过省略字段有时会导致乐观更新不生效如果我们的乐观更新意外导致某个需要更新的查询只被部分缓存cache miss我们就看不到更新被应用。有时我们需要把乐观更新应用到接受参数的字段上。比如favorite字段可能有一个日期截止参数mutation FavoriteTodo(id: $id) { favoriteTodo(id: $id) { id favorite(since: ONE_MONTH_AGO) updatedAt } }解决办法是在乐观更新函数返回的乐观结果中把该字段写成方法const cache cacheExchange({ optimistic: { favoriteTodo(args, cache, info) { return { __typename: Todo, id: args.id, favorite(_args, cache, info) { return true; }, }; }, }, });这个函数的签名与参数和顶层乐观函数一致本质上就是一个嵌套乐观函数。这一能力对应 exchanges/graphcache/src/types.ts 中的MakeFunctional类型——返回的乐观对象中可以包含函数它们会被作为嵌套乐观 resolver 执行。乐观更新所需的额外变量有时我们无法从缓存或既有变量中取到乐观更新创建假结果所需的全部数据。为此 Graphcache 提供了一个小的逃生舱允许访问额外的变量这些变量可以从 UI 代码传递给 mutation。例如给定如下 mutation我们可以添加比 mutation 声明更多的变量mutation UpdateTodo($id: ID!, $text: ID!) { updateTodo(id: $id, text: $text) { id text } }上述 mutation 只定义了$id和$text变量。Graphcache 通常按查询文档中的变量定义过滤变量见 exchanges/graphcache/src/cacheExchange.tsprepareForwardedOperation中通过filterVariables(getMainOperation(operation.query), operation.variables)过滤这意味着 API 永远不会收到定义之外的变量。然而我们仍可以向 mutation 传递额外变量例如{ extra }由于$extra未定义它在 mutation 发送到 API 时会被过滤掉。但乐观 mutation 仍然能访问到这个变量如下所示cacheExchange({ updates: { Mutation: { updateTodo(_result, _args, _cache, info) { const extraVariable info.variables.extra; }, }, }, });这与 exchanges/graphcache/src/types.ts 中ResolveInfo.variables的说明一致——它保存的是OperationRequest上的完整原始变量对象。乐观更新的底层机制从 exchanges/graphcache/src/cacheExchange.ts 可以窥见乐观更新的完整生命周期mutation 且请求策略非network-only时prepareForwardedOperation会以乐观模式initDataState(write, store.data, operation.key, true, false)执行一次_write把乐观结果写入缓存并收集依赖cacheExchange.ts随后这些依赖被记入blockedDependencies相关查询被收集并重新执行。store/data.ts的 layer层机制reserveLayer、hasLayer、commutativeKeys、squashLayer见 data.ts保证乐观数据与真实数据隔离、可整体回滚。当真实结果返回时optimisticMutationCompletion$会把多个乐观 mutation 的结果先缓冲进mutationResultBuffer待全部完成后再统一清除blockedDependencies并批量写回真实结果cacheExchange.ts避免乐观数据与真实数据互相混淆。仓库中的 cacheExchange.test.ts如 writes optimistic mutations to the cache、batches optimistic mutation result application对这些行为有完整测试覆盖。继续阅读本文是 Graphcache 文档Cache Updates篇章的完整展开。接下来可以继续阅读 Schema Awareness它允许 Graphcache 利用 introspection 数据在缓存不完整时返回部分结果并在后台请求完整响应从而与失效机制协同工作其他相关主题还包括 规范化缓存、本地 Resolver 与 缓存错误处理。赞分享前端【免费下载链接】urqlThe highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.项目地址https://gitcode.com/gh_mirrors/ur/urql点击查看免费下载相关推荐CANN文档JPEGE图片编码JPEGE图片编码 本节介绍JPEGE图片编码的接口调用流程同时配合示例代码辅助理解该接口调用流程。 JPEGEJPEG Encoder负责完成图像编码功文档CANN人工智能Relay 数据更新完全指南Mutation、Subscription 与本地存储更新机制Relay 数据更新完全指南Mutation、Subscription 与本地存储更新机制 Relay 在客户端维护一个归一化的内存数据存储normaliz前端开发工具Apollo Client 突变Mutation实战指南useMutation、乐观 UI 与缓存更新Apollo Client 突变Mutation实战指南useMutation、乐观 UI 与缓存更新 导读 本文聚焦 Apollo Client 中用于前端GraphQL上一篇Kubernetes 中的进程 IDPID限制与预留机制详解下一篇Olivia安全与隐私保护构建可信赖的AI对话系统创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
