前言和动机

Here at OkCupid, we're pretty big fans of usingGraphQL.。何时提取在我们的任何客户平台上的数据时,查询语言提供授予我们在每种情况下所需的数据的灵活性的抽象。

在一天结束时,GraphQL真的就是:抽象。突变,查询和订阅类型抽象地模拟了我们与任何数据交互的基本方式。该模式在某些数据源和目的地之间作为合同,它定义了可以查询的数据以及它应该如何查询。我们的GraphQL服务器实例在大多数情况下将解决传入查询概述的数据,但该数据的目的地(在我们的情况下,让我们说这是一个移动应用程序或作为客户端的Web应用程序,并不真正需要要了解有关数据的来源或分辨率策略。

This is really nice, because it means that the data can come from anywhere that the GraphQL server has access to. Maybe we want to resolve our data using something in the filesystem, or maybe a local database, or maybe a remote one. Perhaps we can call some other server exposed to us, via RPC or REST or any protocol, really. Maybe our data is currently in memory somewhere and that's technically fine as well! That indifference of our datasource(s) is what allows this model, and the architecture of a data graph to be so scalable (Mandi Wise has a great video that demonstrates this while涵盖联邦图的概念)。

无论数据的来源如何,客户端实施都没有真正需要更改,这是至关重要的了解使用GraphQL获取本地管理的概念很重要。想象一下你的应用程序的当地国家:它真的只是另一个数据来源,毕竟不是吗?所以,那么问题是,这是什么阻止我们利用此查询语言范例提供给我们的抽象来管理此数据?

这个怎么运作

好吧,答案是什么。如果您在应用程序中的某个位置存储状态,则可以理解地刚刚将GraphQL查询解析为具有该状态数据,如果需要。实施从阿波罗(in our opinion, the de facto providers of all things gql) that we experimented with uses a GraphQL directive to denote which pieces of a given query should be resolved in this manner:@客户

有关那个看起来的示例,让我们想象我们希望拥有用户可以彼此发送消息的应用程序。要获取有关用户有邮件的信息,我们可能会对我们的服务器进行查询。但是,也许我们想知道用户目前是否有一个给定的对话的任何消息窗口。服务器实际上并不关心每次对话的这段信息,但我们的前端应用程序会关心它,因此我们可能会发现我们可能会选择在客户状态下存储类似的东西。假设是这种情况,然后可以为该数据构建合理的查询,包括所包括的本地状态,如下所示:

查询getAllMessages($ UserID:ID!){用户(ID:$ UserID){name profilepic消息{id eperadent {name profilepic} isopen @client}}}

In this example, our开了用户邮件的标志可以存储在我们的客户状态中,因为它并不大部分问题。但是,可以从我们的服务器获取其余的数据,并且没有其他需要更改。能够将我们的数据源(客户端与服务器)与单个查询混合是一个非常强大的想法,可以导致一些令人难以置信的灵活的单个查询。

因为战略for resolving this@客户指令是一个遍历,这意味着该指令可以在我们的数据中递归地应用于父母和邻居关系,允许我们实现数据图的相同体验,除了我们的客户状态。我们的客户状态可以直接访问其父级(非客户端状态)结构,并且它可以像Boolean标志等标量字段一样简单,甚至是更深入地相关和结构的数据。

这client state doesn't need to be related to some server data, either! It could really be any data we might want to store as client state, like perhaps a user's current setting for theme preference, an entirely frontend concern but something stateful nonetheless. Dark mode is so in these days, though, right?

那么,这样的工作如何?嗯,在引擎盖下,我们需要向我们的新配置添加一些新配置apolroclient.实例,以便它具有解决客户端查询的策略。为此,我们通过向我们的客户端添加一些新的解压器来显式拼写这一点,就像我们写入我们的服务器实例上的解析查询一样。这apolroclient.实例可以在初始化后以及使用ad-hoc时添加resolversclient.addresolver(Somenewresolvertoadd)。Apollo定义了处理如此如此的函数签名,如果您合作,它应该看起来非常熟悉Apollo-Server.在过去:

键入resolverfn =(父级:ANY,ARGS:ANY,{Cache}:{Cache:Apolrocache })=>任何;

忽略父节点和参数参数,我们看到我们破坏了对象的属性,调用缓存,就像我们破坏a数据源物业在Apollo-Server.与此功能类型并行。这是因为在这种情况下,我们的缓存已经承担了客户端世界的DataSource的责任。让我们看看在此上下文中的客户端解析器安装程序将是什么样的:

const defaultresolver = {查询:{user:{messages:{isopen :(父,args,{cache})=> {//引用缓存以获取数据返回cache.readquery({查询:message_is_open_query,变量:{message:parent.id,}});},},},},};

之后,我们还希望使用一些初始状态提供缓存,因此我们的第一个缓存读取将解决,因此我们也希望在初始化时将其馈送到我们的缓存中并将其馈送到我们的缓存。

//定义初始客户端状态const defaultstate = {user:{messages:{issopen:false}} const client client = new apolloclient({//其他apollo config,例如链接//和缓存定义解析器:defaultresolver,});//使用初始状态Client.WriteData({查询:Message_Is_Open_Query,Data:{DefaultState,}})将缓存括起来

In this example, we're just setting the default state for a given message

这intuition that Apollo had was to not only provide this directive and the option to resolve queries on the client side, but to offer up the client-side cache that mostapolroclient.无论如何都定义了实例(通常用于将其查询的响应存储在服务器上的查询)作为存储此客户端状态的位置。这有很多意义,我们有一个本地商店(我们的apolroclient.'s cache) and we have a way to interact with that store (using gql)... This is, in essence, what a solution like(which I'm sure needs no introduction at this point) or莫克斯为我们提供对吗?

嗯,是。它也完全良好!当我们探索这一点时,我们注意到一些最终导致我们的一些事情导致我们决定不依赖于Apollo进行国家管理。

我们遇到的问题

那么我们为什么决定实施这一点?嗯,决定背后的推理肯定是我们的情景,也许不会对他人同样重要,但确实包含一些我认为会考虑任何人沿着阿波罗路径的考虑会重视。

开发人员开销和学习曲线

虽然这是一个舞台管理解决方案,但它带来了一些新的开销。一个人现在必须为他们的客户端状态编写解析器,虽然是这种情况任何国家管理选项,它看起来并不像它看起来那么简单。

To really write these resolvers properly, you will want to have some experience / aptitude in working with the cache directly. As of version 3.0,阿波罗/客户有一些人对缓存如何处理归一化和非归一化数据的相当大幅度更新。了解缓存如何使用IDs and__typenames,决定是否合并或替换数据,并学习如何使用此范例的课程标准。Sidenote, Khalil @ Apollo just recently published一个令人难以置信的博客文章深入了解Apollo缓存并了解缓存归一化。这为我们提供了两个备受缓存数据的选项之一:确保每个查询都请求唯一识别我们所要求的数据的唯一识别字段(这并不易于请求我们想要的任何领域的目的?)或写作明确Typepolicies.要告诉我们的缓存如何标准化我们的数据。从撰写客户端库的人的角度来看,必须为众多不同的使用情况正常工作,我理解这样的解决方案的动机。但是,这不是客户端状态通过像Redux这样的解决方案的问题。

对比这种范例对抗类似的东西React的上下文APIin tandem with theUserducer Hook.甚至是Redux架构,似乎是Apollo解决方案更多的是从开发人员角度来理解和管理。但是,对于这个权衡,我们确实获得了思考和互动的能力我们所有的应用程序的数据in the same manner, which is undoubtedly an awesome benefit. But is it worth it?

一些新的东西,借来的东西?

嗯,我们已经在OKC的旧货币中使用了Redux,即使是回流,也在一些旧代码的示例中。在我们的应用中添加一个新的选项,它将真正导致混乱的混乱,这对船上的人无疑是复杂的,这是一个首先将头包裹在一起。就个人而言,我觉得开发者的经验和可维护性应该是巨大的定义任何架构或框架决定的因素。

“Redux已经死了”参数已经多次提出,以及与之相关的传统成本tons可以轻松地认为样板和包装组件不是非常可扩展的,因为一个人必须在到达任何地方之前对少数文件进行更改。尽管如此,它肯定已经成熟了多年。如果做得好,它绝对可以使用微风,它清楚地确实有一些留力(更不用说,与Redux钩子一起工作实际上非常好)。更不用说,剥夺我们现有的架构需要数月,并添加另一个范例来学习和遵循现有的国家管理范例,当DEVS编写代码时会导致更多的上下文切换,并且可能只是混淆并更多地卷起它们更多。

On top of that, there are also countless resources at one's disposal for using and understanding Redux (or any mature OSS, for that matter), one thing that I certainly had issues with personally when researching the Apollo client-side state management; there just aren't as many docs, videos, and articles on the Apollo approach, perhaps because it is more obscure or early in its life when compared to something like Redux. Also, a more mature solution could potentially offer more value in the terms of defining a stable API for the developer to work with, whereas a younger one could be more turbulent in that regard (that is all very situational, though, I will admit).

However, the common argument against Redux of having to "change so many files" and "it being overly complicated" doesn't really seem remedied by Apollo's solution. We'd still want to sensibly colocate the definition of resolvers and initial state, and after having done something like that myself, I felt like I was just writing the boilerplate for some Redux which felt pretty ironic to me. Working with the cache directly doesn't seem less complicated to me than working with the store, either.

阿波罗also has a Dev Tools offering as well, which I really liked using and found to be useful as well, but it too feels a tad immature when put up against something like Redux's parallel. Sometimes, it doesn't want to launch. It doesn't offer interesting features like Time Travel, and the one thing I was excited about using it for, which was the client-side introspection, would have required definingtypeDefs在这一点apolroclient.实例(这基本上是为我们的客户端状态创建架构的过程,这实际上只是担心和管理的另一个整个事情,但我承认,或许可以在这种情况下真正发光的打字或代码。。在我使用的其他库中,没有必要多次定义客户端状态的形状,如果有的话,它开始看起来像一些开始累积的阿波孔侧的样板。

其他选择

我们也一直在研究反应的孔蒂xt API, and presumably will want to consider some other options in the future as well. However, the consideration of what we choose having implications on our bundle size is super important to us as well. Could Context/Apollo remove the need for a state management dependency altogether? For some simpler applications, I think there examples out there where Context has been proven to be more than enough. Likewise, there are some examples of Apollo's solution being more than enough as well! There's also MobX, Facebook's new open source offeringRecoil, and even modeling your client-side state using a state machine withXSTATE.

我们在哪里网?

很难承认阿波罗所做的令人难以置信的工作。Apollo和GraphQL已经完成了清理我们的API和客户的一般网络层的奇迹。然而,追求其作为国家管理层的选择,对我们的代码库具有重大影响,目前我们只是没有觉得这一论点足够引人注目,但由于阿波罗的客户端管理和当前我们的客户设置- 邦德州。在我的意见中,在具有很多现有架构中实施具有很多现有架构的码级内容的返回投资需要相对较高。我只是不确定与阿波罗的投资回报足够高,在这种情况下足够高。

虽然探索阿波罗提供了一些新的洞察力,但是,我们希望能够最终能够更好地得出更好的结论,以及我们应该依靠解决问题,以及我们如何考虑方法,架构和我们决定滚动的任何图书馆,框架或工具的权衡。在此之前,我们将继续保持我们的眼睛如何掌握Apollo的客户状态解决方案的发展。