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

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