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

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

2025-05-11 20:49:00作者:明树来

背景介绍

在现代前端开发中,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 的最佳实践。

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