GraphQL是一种用于API的查询语言,它使客户能够准确地询问他们需要的数据并准确地接收这些数据,仅此而已。这样,单个查询就可以获取渲染组件所需的所有数据。
(相比之下,一个REST API必须触发多次往返以从不同端点的多个资源中获取数据,这可能会变得非常慢,尤其是在移动设备上。)
尽管GraphQL(意为 “Graph Query Language”)使用图数据模型来表示数据,但GraphQL服务器不一定需要使用图作为数据结构来解析查询,而是可以使用任何需要的数据结构。该图只是一个心理模型,而不是实际实现。
GraphQL项目在其网站graphql.org上声明了这一点:
Graph是对许多现实世界现象进行建模的强大工具,因为它们类似于我们的自然心理模型和对潜在过程的口头描述。使用GraphQL,您可以通过定义模式将业务领域建模为图形;在您的架构中,您定义不同类型的节点以及它们如何相互连接/关联。在客户端,这会创建一个类似于面向对象编程的模式:引用其他类型的类型。在服务器上,由于GraphQL只定义了接口,你可以自由地将它与任何后端(新的或旧的!)一起使用。
这是个好消息,因为处理图或树(它们是图的子集)并非易事,并且可能导致解决查询的指数或对数时间复杂度(即解决查询所需的时间可能会增加几个订单)查询的每个新输入的数量级)。
在本文中,我们将描述PoP在PHP GraphQL中的GraphQL服务器的架构设计,它使用组件作为数据结构而不是图。该服务器的名字来源于PoP,它是在PHP中构建组件的库,它是基于该库的。
本文分为5个部分,解释:
- 什么是组件
- PoP的工作原理
- PoP中如何定义组件
- 组件如何自然地适用于GraphQL
- 使用组件解决GraphQL查询的性能
1.什么是组件
每个网页的布局都可以使用组件来表示。组件只是一组代码(例如HTML、JavaScript和CSS)组合在一起以创建一个自治实体,该实体可以包装其他组件以创建更复杂的结构,并且自身也可以被其他组件包装。每个组件都有一个用途,可以是非常基本的东西,例如链接或按钮,也可以是非常复杂的东西,例如轮播或拖放图像上传器。
通过组件构建站点类似于玩乐高。例如,在下图中的网页中,简单的组件(链接、按钮、头像)被组合成更复杂的结构(小工具、部分、侧边栏、菜单)一直到顶部,直到我们获得网页:
页面是一个wrapping组件的组件,如方框所示
组件可以在客户端(例如JS库Vue和React,或CSS组件库Bootstrap和Material-UI)和服务器端以任何语言实现。
2. PoP的工作原理
PoP描述了一种基于服务器端组件模型的架构,并通过组件模型库在PHP中实现。
在以下部分中,术语“组件”和“模块”可互换使用。
组件层次结构
所有模块相互wrapping的关系,从最顶层的模块一直到最后一层,称为组件层次结构。这种关系可以通过服务器端的关联数组(key
=>property
)来表示,其中每个模块将其名称声明为关键属性,并将其内部模块声明为属性"modules"
。
PHP数组中的数据也可以直接在客户端使用,编码为JSON对象。
组件层次结构如下所示:
$componentHierarchy = [ 'module-level0' => [ "modules" => [ 'module-level1' => [ "modules" => [ 'module-level11' => [ "modules" => [...] ], 'module-level12' => [ "modules" => [ 'module-level121' => [ "modules" => [...] ] ] ] ] ], 'module-level2' => [ "modules" => [ 'module-level21' => [ "modules" => [...] ] ] ] ] ] ]
模块之间的关系以严格的自上而下的方式定义:一个模块wrap了其他模块并且知道它们是谁,但它不知道也不关心哪些模块wraps了他。
例如,在上面的组件层次结构中,模块'module-level1'
知道它wrap了模块'module-level11'
和'module-level12'
,并且,它也知道它wrap了'module-level121'
;但是模块'module-level11'
不关心谁在wrap他,因此不知道'module-level1'
.
有了基于组件的结构,我们添加了每个模块所需的实际信息,这些信息分为设置(例如配置值和其他属性)和数据(例如查询的数据库对象的ID和其他属性),并且相应地放在条目modulesettings
和moduledata
:
$componentHierarchyData = [ "modulesettings" => [ 'module-level0' => [ "configuration" => [...], ..., "modules" => [ 'module-level1' => [ "configuration" => [...], ..., "modules" => [ 'module-level11' => [ ...children... ], 'module-level12' => [ "configuration" => [...], ..., "modules" => [ 'module-level121' => [ ...children... ] ] ] ] ], 'module-level2' => [ "configuration" => [...], ..., "modules" => [ 'module-level21' => [ ...children... ] ] ] ] ] ], "moduledata" => [ 'module-level0' => [ "dbobjectids" => [...], ..., "modules" => [ 'module-level1' => [ "dbobjectids" => [...], ..., "modules" => [ 'module-level11' => [ ...children... ], 'module-level12' => [ "dbobjectids" => [...], ..., "modules" => [ 'module-level121' => [ ...children... ] ] ] ] ], 'module-level2' => [ "dbobjectids" => [...], ..., "modules" => [ 'module-level21' => [ ...children... ] ] ] ] ] ] ]
接下来,将数据库对象数据添加到组件层次结构中。此信息不是放在每个模块下,而是放在名为databases
的共享部分下,以避免在2个或更多不同模块从数据库中获取相同对象时重复信息。
此外,该库以关系的方式表示数据库对象数据,以避免当两个或多个不同的数据库对象与一个共同的对象相关时(例如两个具有相同作者的文章),信息重复。
换句话说,数据库对象数据是标准化的。该结构是一个字典,首先组织在每个对象类型下,然后是对象ID,我们可以从中获取对象属性:
$componentHierarchyData = [ ... "databases" => [ "dbobject_type" => [ "dbobject_id" => [ "property" => ..., ... ], ... ], ... ] ]
例如,下面的对象包含一个带有两个模块的组件层次结构"page"
=> "post-feed"
,其中模块"post-feed"
获取博客文章。请注意以下事项:
- 每个模块都知道哪些是其从属性
dbobjectids
(ID4
和9
博客文章)中查询的对象 - 每个模块从属性中知道其查询对象的对象类型
dbkeys
(每个文章的数据都在下面找到"posts"
,文章的作者数据,对应于在文章属性下给出的ID的作者,在下面"author"
找到"users"
): - 因为数据库对象数据是关系型的,所以属性
"author"
包含作者对象的ID,而不是直接打印作者数据
$componentHierarchyData = [ "moduledata" => [ 'page' => [ "modules" => [ 'post-feed' => [ "dbobjectids": [4, 9] ] ] ] ], "modulesettings" => [ 'page' => [ "modules" => [ 'post-feed' => [ "dbkeys" => [ 'id' => "posts", 'author' => "users" ] ] ] ] ], "databases" => [ 'posts' => [ 4 => [ 'title' => "Hello World!", 'author' => 7 ], 9 => [ 'title' => "Everything fine?", 'author' => 7 ] ], 'users' => [ 7 => [ 'name' => "Leo" ] ] ] ]
数据加载
当模块显示来自数据库对象的属性时,模块可能不知道或不关心它是什么对象;它所关心的只是定义加载对象的哪些属性是必需的。
例如,考虑下图:一个模块从数据库中加载一个对象(在本例中为单个文章),然后其后代模块将显示该对象的某些属性,例如"title"
和"content"
:
一些模块加载数据库对象,其他模块加载属性
因此,沿着组件层次结构,“数据加载”模块将负责加载查询的对象(在这种情况下是加载单个文章的模块),其后代模块将定义需要来自DB对象的哪些属性("title"
和"content"
, 在这种情况下)。
可以通过遍历组件层次结构来获取DB对象所需的所有属性:从数据加载模块开始,PoP一直迭代其所有后代模块,直到到达新的数据加载模块,或者直到树的末尾;在每一层,它获取所有需要的属性,然后将所有属性合并在一起并从数据库中查询它们,所有这些都只需要一次。
因为数据库对象数据是以关系方式检索的,那么我们也可以在数据库对象本身之间的关系中应用这种策略。
考虑下图: 从对象类型"post"
开始,向下移动组件层次结构,我们需要将数据库对象类型转换为"user"
和"comment"
,分别对应于文章的作者和每个文章的评论,然后,对于每个评论,它必须再次更改对象类型"user"
以对应评论的作者。从数据库对象转移到关系对象就是我所说的“切换域”。
切换到新域后,从组件层次结构的该级别向下,所有需要的属性都将受制于新域:属性"name"
取自代表文章作者的"user"
对象,"content"
取自代表文章每条评论的"comment"
对象,然后"name"
取自代表每条评论作者的"user"
对象:
将数据库对象从一个域更改为另一个域
遍历组件层次结构,PoP知道它何时切换域并适当地获取关系对象数据。
3. PoP中如何定义组件
模块属性(配置值、要获取的数据库数据等)和子模块是通过ModuleProcessor
对象逐模块定义的,PoP从处理所有相关模块的所有ModuleProcessor
创建组件层次结构。
类似于React应用程序(我们必须指出在哪个组件上渲染