首页
/ Apollo Client 中使用 cache.modify 处理订阅数据的类型安全实践

Apollo Client 中使用 cache.modify 处理订阅数据的类型安全实践

2025-05-11 19:42:19作者:明树来

背景介绍

在现代前端开发中,GraphQL 订阅(Subscription)是实现实时功能的重要手段。Apollo Client 作为流行的 GraphQL 客户端,提供了强大的缓存机制来处理订阅数据。然而,在使用 TypeScript 开发时,如何类型安全地操作缓存成为了一个常见挑战。

核心问题

当应用程序通过订阅接收到新数据时,需要将这些数据合并到现有的缓存中。典型的场景包括:

  1. 帖子(Post)和评论(Comment)的关系
  2. 对话(Conversation)和步骤(Step)的关系

在这些关系中,父实体(如 Post 或 Conversation)通常通过连接(Connection)字段(如 comments 或 steps)关联子实体列表。当订阅推送新数据时,我们需要将这些新数据追加到缓存中的对应列表。

解决方案演进

初始方案:直接修改缓存

最初的尝试是直接使用 cache.modify 方法修改缓存:

cache.modify({
  id: cache.identify({
    __typename: "Post",
    id: comment.post.id,
  }),
  fields: {
    comments(existing) {
      return {
        ...existing,
        edges: [...existing.edges, { node: comment }],
      };
    },
  },
});

这种方法虽然能工作,但存在明显的类型安全问题:

  • existing 参数类型为 any,缺乏类型检查
  • 无法处理缓存引用(Reference)的情况
  • 忽略了连接字段的完整类型定义(如缺少 cursor 等必需字段)

改进方案:使用 readField 辅助方法

为了解决类型安全问题,引入了 readField 方法:

cache.modify<Conversation>({
  id: cache.identify({
    __typename: "Conversation",
    id: step.conversation.id,
  }),
  fields: {
    steps(existing, { readField }) {
      const edges = readField("edges", existing) || [];
      return {
        ...existing,
        edges: [...edges, { node: step }],
      };
    },
  },
});

这种方法仍然存在问题:

  • readField 返回类型过于宽泛
  • 无法确保返回的数据符合 Connection 类型的完整定义
  • 类型断言可能导致运行时错误

最终方案:结合 updateQuery 和类型保护

经过多次尝试,最终确定了一个更可靠的方案:

const onNewStep = (step: DetailedStepFragment) => {
  cache.updateQuery(
    {
      query: GQL.Conversation.StepsQuery,
      variables: { id: step.conversation.id, last: 8 },
    },
    (existing) => {
      if (existing?.node?.__typename !== "Conversation") return;
      const node = frag(GQL.Conversation.WithSteps, existing.node);
      return {
        ...existing,
        node: {
          ...node,
          steps: {
            ...node.steps,
            edges: [
              ...node.steps.edges,
              { __typename: "ConversationStepEdge", node: step },
            ],
          },
        },
      };
    }
  );
};

这个方案的优点:

  1. 通过 updateQuery 直接操作查询结果,类型更明确
  2. 使用类型保护确保操作的安全性
  3. 完整维护了连接类型的所有必需字段
  4. 与现有查询结构保持一致

最佳实践建议

  1. 优先使用 updateQuery:当需要精确控制缓存更新时,updateQuery 通常比 modify 更类型安全。

  2. 正确处理连接类型:确保在添加新边(edge)时包含所有必需字段,如 __typenamecursor

  3. 考虑分页策略:如果使用了 relayStylePagination,需要注意手动更新可能会绕过分页策略。

  4. 使用最新 API:Apollo Client 3.11+ 提供了改进的 useSubscription hook,支持 ignoreResults 选项,可以更好地控制组件重渲染。

  5. 避免 useEffect 中的订阅处理:推荐使用 onData 回调来处理订阅数据,而不是依赖 useEffect

总结

在 Apollo Client 中处理订阅数据时,类型安全是关键考量。通过合理选择缓存操作方法(modifyupdateQuery)并结合 TypeScript 的类型保护,可以构建既安全又高效的实时数据更新机制。开发者应当根据具体场景选择最适合的方法,同时遵循 Apollo Client 的最佳实践。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
27
11
docsdocs
OpenHarmony documentation | OpenHarmony开发者文档
Dockerfile
466
3.47 K
nop-entropynop-entropy
Nop Platform 2.0是基于可逆计算理论实现的采用面向语言编程范式的新一代低代码开发平台,包含基于全新原理从零开始研发的GraphQL引擎、ORM引擎、工作流引擎、报表引擎、规则引擎、批处理引引擎等完整设计。nop-entropy是它的后端部分,采用java语言实现,可选择集成Spring框架或者Quarkus框架。中小企业可以免费商用
Java
10
1
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Java
65
19
flutter_flutterflutter_flutter
暂无简介
Dart
715
172
giteagitea
喝着茶写代码!最易用的自托管一站式代码托管平台,包含Git托管,代码审查,团队协作,软件包和CI/CD。
Go
23
0
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
203
81
RuoYi-Vue3RuoYi-Vue3
🎉 (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统
Vue
1.26 K
695
rainbondrainbond
无需学习 Kubernetes 的容器平台,在 Kubernetes 上构建、部署、组装和管理应用,无需 K8s 专业知识,全流程图形化管理
Go
15
1
apintoapinto
基于golang开发的网关。具有各种插件,可以自行扩展,即插即用。此外,它可以快速帮助企业管理API服务,提高API服务的稳定性和安全性。
Go
22
1