Apollo客户端 - 搅拌缓存归一化

2021-04-08 03:17:07

Apollo客户端具有特别具有挑战性的责任:使互联的GraphQL数据易于在客户端使用。

在最丰富的客户端应用程序中,我们需要能够缓存数据并将其传递给组件。我们还需要知道何时重新获取数据与何时返回已经缓存的内容;这有助于避免制造不必要的网络请求。

即使您不使用GraphQL,这种缓存逻辑也很难实现。

为了有效地利用GraphQL的图形数据,并在何时从缓存中加入何时从高速缓存与网络请求进行读取,Apollo客户端作为我们缓存的数据图的那些小规范化段的顶部的抽象。客户端。

通过作为存储外观,Apollo客户端可以拦截查询的请求并自动拼凑地重复使用它们。

它还可以自动更新突变后的缓存,但这主要取决于突变是否更新了单个现有实体或创建,删除或修改多个实体。

缓存的操作类型无法自动更新缓存,以及处理这些方案的示例。

知道使用Apollo客户端,Redux,React Context或其他方法的状态管理的基础知识。

归一化是用于以减少数据冗余的方式组织数据的技术。

通常,当我们构建要存储的数据以某处(无论是数据库,客户端缓存还是JSON对象),我们希望减少保存的重复数据量。理想情况下,我们的目标是没有重复的数据。

关系数据库设置了一个很好的例子。通过使用关系(主键,外键)和约束,我们只能强制执行唯一的数据仅添加到数据库中。

关系数据库非常强大。如果我们正确设置了关系和约束,我们可以确保他们拒绝任何尝试添加重复数据或引用不再存在的对象。我认为这是一件好事,因为它可以保持您的数据清洁,一致,尽可能小。

关系数据库与apollo客户端有什么关系?除了这一件事之外,除了这一件事之外不多。它们如何提供对底层数据的访问方式是类似的。他们都使用了一个门面。

外立面模式公开了一个额外的顶级代码层,比较低级别的东西更容易处理。所以基本上,门面是一个API。

大多数人更喜欢使用这些高级API,而不是直接与数据(存储在文件中)进行交互。 Apollo Client和任何其他技术为您提供了一组工具,可以与缓存数据交互,是存储门面。 绘图的重要结论是,通过最大限度地减少与外立面或API的实际数据的直接访问,它提供了工具的能力,以使如罩筒下的数据标准化(和反应性)等能力。 在更赤裸的骨骼中,如Redux或React Context等方法,数据归一化是开发人员必须手动构建其状态管理架构的东西。 当我们执行操作时,Apollo客户端在将其保存到高速缓存之前将响应数据标准化。 从DOCS上的数据归一化,可以在三个步骤中解释该算法。 它用: 为每个对象分配逻辑唯一标识符,以便缓存以稳定的方式跟踪实体

让我们一步一步地走过真实的示例,并观察算法如何工作。

假设我们有一个todo应用程序。要获取数据图背后的所有托管,我们可以调用getAlltodos查询。

归一化算法的第一步是将数组的项目拆分为如此之类的单个对象。

下一个阶段是为每个项目分配唯一标识符。默认情况下,Apollo Client使用ID + __Typename创建一个。

值得注意的是,它也是一种非常实际的可能性,您可能使用GraphQL API返回没有ID字段的数据。如果我们有能力调整数据图的设计,以包括每种类型的ID字段,那么建议采用该方法。

如果我们无法改变它,那么我们可能被迫考虑其他方式可以可靠地为我们每个项目建立独特性。

关键字段API为我们提供了定制我们想要用作唯一标识符的能力。

例如,也许ID字段由不同的名称进行了不同。也许它被命名为todoid。这是一个快速修复。

const cache = new({typepolicies:{todo:{// todo的唯一标识符实际上列出//作为" todoid" se let' s使用它。Keyfields:[&# 34; Todoid"],}},});

如果没有一种对待字段,怎么办?我们现在干什么?

考虑一下Todo对象如下所示,我们可能会做什么:

{__typename:" todo" ,文本:"第一个Todo" ,完成:虚假,日期:" 2020-07-08T15:05:32.248Z" ,用户:{电子邮件:" [email protected]" ,}}

潜在的唯一性也可以使用日期字段和嵌套电子邮件字段构建。

const cache = new({typepolicies:{todo:{//,如果其中一个keyfields是一个带有自己字段的对象,则可以使用嵌套的字符串数组包含这些嵌套的keyfield:keyfields:[&#34 ;日期","用户",["电子邮件"],}},});

唯一标识项目对于Apollo客户端很重要,因为这是它跟踪从多个查询返回的相同对象的方式。这就是对象字段可以在缓存中随时间合并在一起的方式。

一旦每个项目都有一个唯一的标识符,Apollo Client将在扁平的JavaScript对象中存储对象。这是Apollo客户端缓存中心的原始归一化JavaScript对象。它看起来像这样。

通过将每个归一化物品存放平整,它使它们通过其唯一ID(如散列表)来访问。如果您知道关于哈希表的一两个事情,您将知道检索非常快,我们知道我们正在寻找的项目的标识符。

由于我们获取了一系列物品,我们希望维护物品进入的原始订购。

为此,缓存实际上存储了GetAllTodos查询,我们传递给它的任何变量以及结果。

Apollo客户端缓存任何GraphQL操作,包括的变量以及结果。 Apollo客户端为疑问和突变执行此操作。

而不是将每个Todo复制在缓存的Todos查询中,而不是通过其唯一标识符维护对归一化Todo项的引用。这是在工作中的正常化。这就是我们如何保持缓存的大小尽可能小,并防止重复数据。

此内部数据旨在轻松json-serializable,因此您可以使用cache.extract()拍摄快照,将其保存在某个位置,然后用cache.restore(快照)还原。

传统上讲,缓存的全部点是减少需要拨打额外的网络电话,对吧?

默认情况下,当我们要求数据时,Apollo客户端尝试直接从缓存中源。如果数据存在,那么这就是所用的。

如果数据尚未缓存,或者如果我们要求更多的字段,那么我们会再次进行另一个请求并再次缓存响应。有一个名为fetch策略的功能。它决定了缓存在询问可能或可能不缓存的数据时如何行事。默认的获取策略称为缓存 - 首先,这就是它的工作原理。

......然后apollo客户端可以达到缓存并直接获取对象而不进行另一个请求。

有关获取策略的更多信息,请阅读“了解Apollo获取策略”并读取“获取策略上的文档”。

为了使Apollo客户端自动更新缓存,我们必须记住始终返回操作响应中的新数据。

对于查询响应,就是这一点。查询的整个目的是返回数据并缓存它。

但对于突变,如eDittodo改变一个实体,如果我们在突变响应中返回值,我们应该能够自动更新项目。

这是一个名为Edittodo的突变,它返回突变响应中的新Todo值。

突变eDittodo($ ID:int!,$文本:字符串!){edittodo(:$ ID,:$文本){成功todo {#< - 返回这里的ID文本已完成}错误{... on {message} ...在{message}}}}}

通过返回我们'在突变响应中编辑的新版本,Apollo客户端归一化算法执行以下操作:

使用默认__typename + ID字段或密钥字段配置确定其唯一标识符。

确定标识符已作为高速缓存中的归一项化项目存在,然后与该对象合并,更倾向于旧的字段值。它也有助于注意,您可以使用自定义合并功能来更改简单覆盖旧字段的默认行为。

导入从&#39反应;反应' ;从&#34导入{gql,uderquery}; @ apollo / client" ;从&#39导入todo; ../组件/ todo' const edit_todo = gql`变异edittodo($ id:int!,$ text:string!){edittodo(id:$ id,text:$ text){success todo {id text完成}错误{... on todonotfounderror {message } ...在TodovalidationError {message}}}}`出口const todocontainer =()=> {const todos = gettodos(); const [突变,{data,错误}] =解释(edit_todo)...返回todos。地图((todo,i)=>(< todo key = {i} action = {{edittodo :( id,text)=> mutate({变量:{id,text}})} /> ))))}

如果我们在第三个todo(todo:3)上运行了edittodo突变,从&#34更改文本;最好的Todo"来自"第三个Todo",突变响应数据看起来像这样。

没有任何进一步的干预,Apollo客户端应该自动将响应数据合并到缓存中,因为它识别出较早查询返回的Todo:3标识符。

由于Todos查询指向更新的ToDo:3,ui中的任何组件呈现托管列表(例如< todolist />组件),将获得重新呈现以显示新更改的文本值Todo:3。

缓存可以自动正常化,缓存和更新查询,更新单个现有实体的突变,以及返回整组更改项目的批量更新突变。

如前所述,如果我们返回新数据,缓存将其拆分为奇异对象,创建唯一标识符,并保存到缓存中的每个项目(除查询本身以及包括的任何变量)到缓存。

标准化并缓存在查询响应中返回的所有项目。如果项目已存在,则它将其融合,更倾向于新数据。

导入从&#39反应;反应' ;从&#34导入{gql,uderquery}; @ apollo / client" ;从&#39导入todo; ../组件/ todo'导出const_all_all_all_todos = gql`query getalltodos {todos {id text已完成}}`导出默认函数todolist(){const {loading,data,错误} = uderquery(get_all_todos); if(加载)返回< div>加载...< / div>如果(错误)返回< div>错误发生{JSON。 stryify(错误)}}< / div> if(!数据)返回< div和gt;没有todos! < / div> ;返回Todos。地图((todo,i)=>(< todo key = {i} todo = {todo} />))}

如果之前从未见过响应返回的实体,则缓存将标准化它并将其存储为缓存上的扁平对象。

导入从&#39反应;反应' ;从&#39导入{umerparams}; React-Router-Dom' ;从&#39导入{imedquery}; @ apollo / client' ;从&#39导入todo; ../组件/ todo' const get_todo_by_id = gql`查询gettodobyid($ id:int!){todo(ID:$ ID){...在TodOn {id text完成} ...在todonotfounderror {message}}} {让{id} = umermparams(); const {加载,数据,错误} = uderquery(get_todo_by_id,{变量:{id:number(id)})如果(加载)返回< div> loading ...< / div>如果(错误)返回< div> {error}< / div>返回数据?.todo .__ typename ===" todo" ? (< ul classname =" todo-list">>>>>>>>< / ul>):(< div> TONDO NOT DOD NOT NOT DODENDOURE; TONDO NOT DODENT; TONDOURENDORGENT。 ; / div>)}

这些类型的操作更新了一个有问题的实体。无论操作是什么,只要我们返回包含ID和更改字段的新对象,Apollo客户端就可以自动更新缓存中的项目并触发重新呈现给UI。

导入从&#39反应;反应' ;从&#34导入{gql,vqluition}; @ apollo / client" ;从&#39导入todo; ../组件/ todo' const edit_todo = gql`变异edittodo($ id:int!,$ text:string!){edittodo(id:$ id,text:$ text){success todo {id text完成}错误{... on todonotfounderror {message } ...在TodovalidationError {message}}}}`出口const todocontainer =()=> {const todos = gettodos(); const [突变,{data,错误}] =解释(edit_todo)...返回todos。地图((todo,i)=>(< todo key = {i} action = {{edittodo :( id,text)=> mutate({变量:{id,text}})} /> ))))}

导入从&#39反应;反应' ;从&#34导入{gql,vqluition}; @ apollo / client" ;从&#39导入todo; ../组件/ todo' const constult_todo = gql`变异completeTodo($ ID:int!){compledTodo(ID:$ ID){success todo {id text完成}错误{...在todonotfounderror {message} ...上on todoalreadycompletederror {message}} }`出口const todocontainer =()=> {const todos = gettodos(); const [变异,{data,错误}] = veremutation(complete_todo)...返回todos。映射((todo,i)=>(< todo key = {i}操作= {{compledetodo:(id)=>突变({变量:{id}})} />)))}

如果我们要对一组项目和突变响应执行批量更新,我们返回了更改的整套对象以及它们的新值,然后缓存可以自动更新。

如果没有,它第一次拆除项目,分配唯一标识符和缓存和#39; em。

从本质上讲,如果我们执行查询或突变,它就不会 - 如果我们在响应中返回一个项目数据集,则缓存将运行符合逻辑符合它的归一化逻辑。这导致合并或向缓存添加新项目。

从&#34导入{gql,vqluition}; @ apollo / client" ;导入*从&#39中的表达型号; / __生成__ / completealltodos'导出const_all_todos = gql` mutation compulealltodos {conferineAlelltodos {subdiceAlelltodos {superse todos {id text已完成}}` const [mutate] =过敏峰< CompuleAlltodostypes .completealltodos> (complete_all_todos)如果(加载)返回< div> loading ...< / div>如果(错误)返回< div>错误发生{JSON。 stryify(错误)}}< / div> if(!数据)返回< div和gt;没有todos! < / div> ;返回< layout> <按钮onclick = {()=>变异()}}}>填写所有TODOS< /按钮> {Todos。映射((todo,i)=>(< todo key = {i} todo = {todo} />))}< /布局> }

从本质上讲,如果我们执行查询或突变,它就不会 - 如果我们在响应中返回一个项目数据集,则缓存将运行符合逻辑符合它的归一化逻辑。这导致合并或向缓存添加新项目。

应用程序特定的副作用和更新操作,用于在缓存的集合中添加,删除或重新排序项目。

如果我们想要发生的副作用与返回数据无关

......然后我们需要编写一个更新函数来告知缓存是如何更新的。

特定于应用程序特定的副作用是您希望在可能不会从响应数据中使用任何内容的突变后发生在缓存中的内容。

也许在您调用了注销突变之后,您希望清除用户的整个缓存和#39; s信息,以便新用户可以启动会话。

相反,我们可能希望删除整个缓存。您可以在更新函数中使用client.clearstore()方法。

从&#34导入{gql,vqluition}; @ apollo / client"从&#34导入{client}; ./客户端" const logout = gql`突变注销{logout {success message}}`const navbar =()=> {const [logout] = Uleemutation(logout,{update(){client。clearstore()}});返回< div onclick = {()=> logout()}}> < / div> }

在Apollo客户端3中,我们使用反应变量和缓存策略来设置本地状态。它' s可能在执行操作后,我们需要更新一些本地状态。

可以直接在突变的更新功能中导入反应性变量(或与反应变量运行的交互逻辑的功能)。

通过反应变量阅读本地状态管理,以了解有关AC3中的本地国家管理的更多信息。

仅在返回已更改的整个对象集时才更新工作。如果我们可以' t返回更改的整套项目,我们如何使用上一节采取相同的例子。'我们如何更新缓存。

从&#34导入{gql,vqluition}; @ apollo / client" ;导入*从&#39中的表达型号; / __生成__ / completealltodos'导出const_all_todos = gql`突变compuleilealltodos {conficeAlelltodos {superfile todos {id#不返回所有数据}}` const [mutate] =过敏峰< CompuleAlltodostypes .completealltodos> (complete_all_todos,{update(cache,{data}){const tressttodos = data?.completealltodos .todos; const alltodos = cache .readquery< getalltodos>({查询:get_all_todos}; cache。pache。writequery({查询:get_all_todos ,数据:{todos:alltodos。地图((t)=>!hapdingtodos。查找((已完成)=>已完成的.id === t .id)}}}})如果(加载)返回&lt ; div>加载...< / div>如果(错误)返回< div>发生错误{JSON。Stringify(错误)}< / div和gt; if(!数据)返回< div >没有todos!< / div>;返回lt; layout>< on onclick = {()=> mutate()}}>填写所有todos< / button> {todos。地图(( todo,i)=>(< todo key = {i} todo = {todo} />))}}}}}}}}}}}}}

缓存不会知道它应该何时应该将新创建的实体添加到现有的数据查询中。在这些情况下,我们必须编写更新功能。

const [突变,{data,error}] = veremition< addtodotypes .addtodo,addtodotypes .addtodovariables> (add_todo,{update(cache,{data}){const newtodofromResponse = data?.addtodo .todo; const passiontodos = cache .readquery< getalltodos>({查询:get_all_todos,}); if(现有todos&& N

......