为什么我们决定不使用GraphQL进行本地状态管理

前言和动机
在OkCupid188bet金宝搏官网,我们非常喜欢使用GraphQL.当涉及到在任何客户端平台上获取数据时,查询语言提供的抽象为我们提供了在每种情况下精确获取所需数据的灵活性。
归根结底,GraphQL实际上只是一个抽象。变异、查询和订阅类型对我们与任何数据交互的基本方式进行了抽象建模。模式充当数据源和目标数据之间的契约,它定义了可以查询哪些数据以及应该如何查询数据。传入的数据查询大纲将由我们解决GraphQL服务器实例在大多数情况下,但数据的目的地(在我们的例子中,假设它是一个移动应用程序或web应用程序作为客户端),不需要知道数据的源或解决战略问题。
这真的很好,因为这意味着数据可以来自GraphQL服务器可以访问的任何地方。也许我们希望使用文件系统中的某些东西来解决我们的数据,或者可能是本地数据库,或者可能是远程数据库。也许我们可以通过RPC或休息或任何协议将其他其他服务器调用,或任何协议。也许我们的数据目前在某处内存,这也很好!我们的数据源的漠不关心是允许此模型的允许,以及数据图的体系结构如此可扩展(Mandi Wise有一个展示这一点的很棒的视频涵盖了联邦图的概念).
不管数据的来源是什么,客户端实现实际上根本不需要更改,这就是最重要的是理解使用GraphQL进行本地状态管理的概念非常重要。想象一下应用程序的本地状态:它实际上只是另一个数据源,不是吗?那么,接下来的问题是,是什么阻止我们利用这个查询语言范例提供给我们的抽象来管理这些数据呢?
它是如何工作的
答案是什么都没有。如果您将状态存储在应用程序的某个地方,理论上只要您愿意,就可以用该状态数据解析GraphQL查询。实现从阿波罗(在我们看来,所有gql的事实提供者),我们实验使用一个GraphQL指令来表示给定查询的哪些部分应该以这种方式解析:@client
.
有关那个看起来的示例,让我们想象我们希望拥有用户可以彼此发送消息的应用程序。要获取有关用户有邮件的信息,我们可能会对我们的服务器进行查询。但是,也许我们想知道用户目前是否有一个给定的对话的任何消息窗口。服务器实际上并不关心每次对话的这段信息,但我们的前端应用程序会关心它,因此我们可能会发现我们可能会选择在客户状态下存储类似的东西。假设是这种情况,然后可以为该数据构建合理的查询,包括所包括的本地状态,如下所示:
查询getAllMessages($userId: ID!)
用户(id: $ userId) {
的名字
profilePic
消息{
id
记者{
的名字
profilePic
}
isOpen @client
}
}
}
在这个例子中,我们的isOpen
每个用户消息的标志可以存储在我们的客户端状态中,因为它不是一个后台关注太多。然而,其余的数据可以从我们的服务器获取,不需要更改其他任何内容。为单个查询混合数据源(客户机vs.服务器)的能力是一个非常强大的想法,可以产生一些非常灵活的单个查询。
由于战略为解决这@client
指令是一个遍历,这意味着这个指令可以递归地应用于我们的数据中的父子关系和邻居关系,允许我们实现相同的数据图体验,除了我们的客户端状态。我们的客户端状态可以直接访问它的父(非客户端状态)结构的一部分,它可以是像布尔标志这样简单的标量字段,甚至是更深入相关和结构化的数据。
客户状态不需要与某些服务器数据相关!它真的可以是我们可能想要存储为客户状态的任何数据,例如用户当前的主题偏好的当前设置,完全是前端关注,但仍然存在一些状态。虽然暗模式是如此,但是,对了吗?
那么,这种东西是如何工作的呢?好吧,在引擎盖下,我们需要添加一些新的配置到ApolloClient
实例,以便它具有解析客户端查询的策略。为此,我们通过向客户端添加一些新的解析器来明确说明这一点,就像编写用于解析服务器实例上的查询一样。的ApolloClient
实例可以在初始化时添加解析器,也可以在特别的基础上使用client.addResolver (someNewResolverToAdd)
.Apollo定义了这样处理解析的函数签名,如果您使用过,应该会非常熟悉apollo-server
过去:
类型ResolverFn = (
父:任何,
参数:任何,
{cache: ApolloCache}
) = >;
忽略父节点和arguments形参,我们看到我们从一个名为的对象解构了一个属性缓存
,就像我们破坏一个数据源
财产的apollo-server
与此函数类型平行。这是因为在这个场景中,我们的缓存承担了客户端世界中的数据源的责任。让我们看看在这个上下文中客户端解析器的设置是什么样的:
const defaultResolver = {
查询:{
用户:{
消息:{
isOpen: (parent, args, {cache}) => {
//引用缓存获取数据返回
cache.readQuery ({
查询:MESSAGE_IS_OPEN_QUERY,
变量:{
消息id: parent.id,
},
});
},
},
},
},
};
在此之后,我们还需要为缓存提供一些初始状态,以便第一次读取缓存时进行解析,因此我们也需要定义该状态,并在初始化时将其提供给缓存。
//定义初始客户端状态
const defaultState = {
用户:{
消息:{
isOpen:假的,
}
}
}const client = new ApolloClient({
//其他Apollo配置,例如link
//和缓存定义
Resolvers:DefaulTresolver,
});//初始状态
client.writeData ({
查询:MESSAGE_IS_OPEN_QUERY,
data: {defaultState},
});
在本例中,我们只设置给定消息的默认状态
阿波罗的直觉是,不仅要提供这个指令和在客户端解析查询的选项,还要提供最多的客户端缓存ApolloClient
实例将无论如何(通常用于存储实际上在服务器上解析的查询的响应)定义为存储此客户端状态的位置。这很有道理,我们在当地有一家商店(我们的ApolloClient
我们有一种与存储交互的方法(使用gql)…本质上,这就是解决方案回来的(我想现在不需要介绍了)或者MobX给我们提供了对吧?
嗯,是。它也完全良好!当我们探索这一点时,我们注意到一些最终导致我们的一些事情导致我们决定不依赖于Apollo进行国家管理。
我们遇到的问题
那么为什么我们当时决定不执行这个?好吧,这个决定背后的原因当然是特定于我们的情况,也许对其他人不那么重要,但确实包含了一些见解,我认为将是任何人沿着阿波罗道路将不得不权衡的考虑。
开发人员开销和学习曲线
虽然这是一个阶段管理解决方案,但它带来了一些新的开销。人们现在必须为他们的客户端状态编写解析器,尽管情况确实如此任何国家管理的选择,并不像看起来那么简单。
要真正正确地编写这些解析器,您需要有一些直接使用缓存的经验/能力。从3.0版本开始,阿波罗/客户端
已经取得了一些对缓存处理规范化和非规范化数据的方式进行了非常激烈的更新.理解缓存如何使用id
年代和__typename
s,决定是否合并或替换数据,并学习如何使用此范例的课程标准。《阿波罗》刚刚出版一篇不可思议的博客文章深入了解阿波罗缓存并理解缓存规范化。这为我们的缓存数据提供了两种选择:要么确保每个查询请求我们所请求的数据的唯一标识字段(这是否违背了请求任何我们想要的字段的目的?typePolicies
告诉缓存如何规范化数据。从编写客户端库的人的角度来看,这个库必须正确地用于许多不同的用例,我理解这种解决方案的动机。然而,通过像Redux这样的解决方案,这并不是一个客户端状态的问题。
对比这种范例对抗类似的东西反应的上下文API与useReducer钩阿波罗解决方案似乎更适合从开发人员的角度来理解和管理。尽管如此,对于这种权衡,我们确实获得了思考和互动的能力我们所有的应用程序的数据以同样的方式,这无疑是一个令人敬畏的好处。但这值得吗?
是新的还是借来的?
我们在OkC已经在使用Redux了,在一些较老的代码中,甚至还有回流。在我们的应用中添加状态管理的新选项确实会造成混乱,这对于我们团队的新成员来说无疑是一件很复杂的事情。就我个人而言,我认为开发人员的经验和可维护性应该是决定任何架构或框架决策的因素。
“Redux已经死了”的说法已经被提出过很多次了,与之相关的传统成本是以吨样板文件和包装组件很容易被认为伸缩性不强,因为在到达任何地方之前必须对少量文件进行更改。尽管如此,多年来它确实成熟了。如果做得好,使用它绝对是一件轻而易举的事,而且它显然有一定的持久性(更不用说,使用Redux钩子实际上真的很好)。更不用说,删除我们现有的架构需要花费几个月的时间,而在现有的状态管理模式的同时添加另一个范例去学习和遵循将会在开发人员编写代码时导致更多的上下文切换,并且可能会使他们更加困惑和负担。
最重要的是,还有无数的资源可供人们使用和理解Redux(或者任何成熟的OSS,就此而言),有一件事我个人在研究Apollo客户端状态管理时肯定有问题;关于阿波罗的方法没有那么多的文档、视频和文章,也许是因为与Redux相比,它更模糊或处于早期阶段。此外,一个更成熟的解决方案在为开发人员定义稳定的API方面可能会提供更多的价值,而一个年轻的解决方案在这方面可能会更混乱(不过,我承认,这取决于具体情况)。
然而,反对Redux必须“更改这么多文件”和“它过于复杂”的常见观点似乎并没有被Apollo的解决方案所纠正。我们仍然需要合理地定位解析器和初始状态的定义,在我自己做了类似的事情之后,我觉得我只是在为某些Redux编写样板文件,这对我来说非常讽刺。对我来说,直接使用缓存似乎并不比使用存储简单。
阿波罗也提供了一个开发工具,我真的很喜欢使用它,也发现它很有用,但当它与Redux类似的东西相比较时,感觉有点不成熟。有时候,它不想发射。它不提供像Time Travel这样有趣的功能,我对使用它感到兴奋的一件事是客户端内省,而这需要定义typedef
在ApolloClient
实例(这本质上是为我们的客户端状态创建一个模式的过程,这实际上只是另一组需要担心和管理的事情,但我承认也许Typescript或codegen在这种情况下真的很出色)。在我使用过的其他库中,不需要多次定义我们的客户端状态的形状,如果有的话,它看起来就像阿波罗端样板文件一样,正在积累。
其他选项
我们也在尝试React的Context API,并且可能会在未来考虑一些其他的选择。然而,我们所选择的内容对我们的捆绑包大小的影响也非常重要。Context/Apollo是否可以完全消除对状态管理依赖的需要?对于一些更简单的应用程序,我认为已经有一些例子证明了Context已经足够了。同样,也有一些阿波罗方案的例子是绰绰有余的!还有Facebook的新开源产品MobX反冲,甚至使用状态机对客户端状态进行建模XSTATE..
我们在哪里捞出?
我们很难不承认阿波罗所做的不可思议的工作。Apollo和GraphQL在清理我们的api和客户端的一般网络层方面做出了惊人的贡献。然而,将其作为状态管理的一种选择,对我们的代码库有重大的影响,而且鉴于Apollo的客户端状态管理的成熟度和我们的客户端状态的当前设置,此时我们只是觉得这个争论不够有说服力。在我看来,在有很多现有架构的代码库中实现新东西的投资回报需要相对较高。我只是不确定阿波罗项目的投资回报是否足够高。
带着一些新的见解后阿波罗的探索提供一些更多,不过,我们希望能够得出更好的结论最终我们应该依靠为了解决这个问题,我们应该如何思考的方法,架构,和权衡任何库、框架或工具我们决定卷。在此之前,我们将继续关注Apollo的客户端状态解决方案如何发展。
最初发表在https://tech.188bet金宝搏官网okcupid.com2020年8月20日。