GraphQL是一種用於API的查詢語言,它使客戶能夠準確地詢問他們需要的資料並準確地接收這些資料,僅此而已。這樣,單個查詢就可以獲取渲染元件所需的所有資料。
(相比之下,一個REST API必須觸發多次往返以從不同端點的多個資源中獲取資料,這可能會變得非常慢,尤其是在移動裝置上。)
儘管GraphQL(意為 “Graph Query Language”)使用圖資料模型來表示資料,但GraphQL伺服器不一定需要使用圖作為資料結構來解析查詢,而是可以使用任何需要的資料結構。該圖只是一個心理模型,而不是實際實現。
GraphQL專案在其網站graphql.org上宣告瞭這一點:
Graph是對許多現實世界現象進行建模的強大工具,因為它們類似於我們的自然心理模型和對潛在過程的口頭描述。使用GraphQL,您可以通過定義模式將業務領域建模為圖形;在您的架構中,您定義不同型別的節點以及它們如何相互連線/關聯。在客戶端,這會建立一個類似於物件導向程式設計的模式:引用其他型別的型別。在伺服器上,由於GraphQL只定義了介面,你可以自由地將它與任何後端(新的或舊的!)一起使用。
這是個好訊息,因為處理圖或樹(它們是圖的子集)並非易事,並且可能導致解決查詢的指數或對數時間複雜度(即解決查詢所需的時間可能會增加幾個訂單)查詢的每個新輸入的數量級)。
在本文中,我們將描述PoP在PHP GraphQL中的GraphQL伺服器的架構設計,它使用元件作為資料結構而不是圖。該伺服器的名字來源於PoP,它是在PHP中構建元件的庫,它是基於該庫的。
本文分為5個部分,解釋:
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應用程式(我們必須指出在哪個元件上渲染<div id="root"></div>
),PoP中的元件模型必須有一個入口模組。從它開始,PoP將遍歷元件層次結構中的所有模組,從相應的ModuleProcessor
中獲取每個模組的屬性,並建立包含所有模組所有屬性的巢狀關聯陣列。
當一個元件定義一個後代元件時,它通過一個包含2個部分的陣列來引用它:
- PHP類
- 元件名稱
這是因為元件通常共享屬性。例如,元件POST_THUMBNAIL_LARGE
和POST_THUMBNAIL_SMALL
將共享大多數屬性,但縮圖的大小除外。然後,將所有相似的元件分組到同一個PHP類下,並使用switch
語句來識別請求的模組並返回相應的屬性,這是有意義的。
ModuleProcessor
要放置在不同頁面上的文章小工具元件如下所示:
class PostWidgetModuleProcessor extends AbstractModuleProcessor { const POST_WIDGET_HOMEPAGE = 'post-widget-homepage'; const POST_WIDGET_AUTHORPAGE = 'post-widget-authorpage'; function getSubmodulesToProcess() { return [ self::POST_WIDGET_HOMEPAGE, self::POST_WIDGET_AUTHORPAGE, ]; } function getSubmodules($module): array { $ret = []; switch ($module[1]) { case self::POST_WIDGET_HOMEPAGE: case self::POST_WIDGET_AUTHORPAGE: $ret[] = [ UserLayoutModuleProcessor::class, UserLayoutModuleProcessor::POST_THUMB ]; $ret[] = [ UserLayoutModuleProcessor::class, UserLayoutModuleProcessor::POST_TITLE ]; break; } switch ($module[1]) { case self::POST_WIDGET_HOMEPAGE: $ret[] = [ UserLayoutModuleProcessor::class, UserLayoutModuleProcessor::POST_DATE ]; break; } return $ret; } function getImmutableConfiguration($module, &$props) { $ret = []; switch ($module[1]) { case self::POST_WIDGET_HOMEPAGE: $ret['description'] = __('Latest posts', 'my-domain'); $ret['showmore'] = $this->getProp($module, $props, 'showmore'); $ret['class'] = $this->getProp($module, $props, 'class'); break; case self::POST_WIDGET_AUTHORPAGE: $ret['description'] = __('Latest posts by the author', 'my-domain'); $ret['showmore'] = false; $ret['class'] = 'text-center'; break; } return $ret; } function initModelProps($module, &$props) { switch ($module[1]) { case self::POST_WIDGET_HOMEPAGE: $this->setProp($module, $props, 'showmore', false); $this->appendProp($module, $props, 'class', 'text-center'); break; } parent::initModelProps($module, $props); } // ... }
建立可重用元件是通過建立抽象的ModuleProcessor
類來完成的,這些類定義了必須由一些例項化類實現的佔位符函式:
abstract class PostWidgetLayoutAbstractModuleProcessor extends AbstractModuleProcessor { function getSubmodules($module): array { $ret = [ $this->getContentModule($module), ]; if ($thumbnail_module = $this->getThumbnailModule($module)) { $ret[] = $thumbnail_module; } if ($aftercontent_modules = $this->getAfterContentModules($module)) { $ret = array_merge( $ret, $aftercontent_modules ); } return $ret; } abstract protected function getContentModule($module): array; protected function getThumbnailModule($module): ?array { // Default value (overridable) return [self::class, self::THUMBNAIL_LAYOUT]; } protected function getAfterContentModules($module): array { return []; } function getImmutableConfiguration($module, &$props): array { return [ 'description' => $this->getDescription(), ]; } protected function getDescription($module): string { return ''; } }
然後,自定義ModuleProcessor
類可以擴充套件抽象類,並定義自己的屬性:
class PostLayoutModuleProcessor extends AbstractPostLayoutModuleProcessor { const POST_CONTENT = 'post-content' const POST_EXCERPT = 'post-excerpt' const POST_THUMBNAIL_LARGE = 'post-thumbnail-large' const POST_THUMBNAIL_MEDIUM = 'post-thumbnail-medium' const POST_SHARE = 'post-share' function getSubmodulesToProcess() { return [ self::POST_CONTENT, self::POST_EXCERPT, self::POST_THUMBNAIL_LARGE, self::POST_THUMBNAIL_MEDIUM, self::POST_SHARE, ]; } } class PostWidgetLayoutModuleProcessor extends AbstractPostWidgetLayoutModuleProcessor { protected function getContentModule($module): ?array { switch ($module[1]) { case self::POST_WIDGET_HOMEPAGE_LARGE: return [ PostLayoutModuleProcessor::class, PostLayoutModuleProcessor::POST_CONTENT ]; case self::POST_WIDGET_HOMEPAGE_MEDIUM: case self::POST_WIDGET_HOMEPAGE_SMALL: return [ PostLayoutModuleProcessor::class, PostLayoutModuleProcessor::POST_EXCERPT ]; } return parent::getContentModule($module); } protected function getThumbnailModule($module): ?array { switch ($module[1]) { case self::POST_WIDGET_HOMEPAGE_LARGE: return [ PostLayoutModuleProcessor::class, PostLayoutModuleProcessor::POST_THUMBNAIL_LARGE ]; case self::POST_WIDGET_HOMEPAGE_MEDIUM: return [ PostLayoutModuleProcessor::class, PostLayoutModuleProcessor::POST_THUMBNAIL_MEDIUM ]; } return parent::getThumbnailModule($module); } protected function getAfterContentModules($module): array { $ret = []; switch ($module[1]) { case self::POST_WIDGET_HOMEPAGE_LARGE: $ret[] = [ PostLayoutModuleProcessor::class, PostLayoutModuleProcessor::POST_SHARE ]; break } return $ret; } protected function getDescription($module): string { return __('These are my blog posts', 'my-domain'); } }
4. 元件如何天生適合GraphQL
元件模型可以自然地對映樹形GraphQL查詢,使其成為實現GraphQL伺服器的理想架構。
PoP的GraphQL實現了將GraphQL查詢轉換為其相應元件層次結構所需的ModuleProcessor
類,並使用PoP資料載入引擎進行解析。
這就是該解決方案起作用的原因和方式。
將客戶端元件對映到GraphQL查詢
GraphQL查詢可以使用PoP的元件層次結構來表示,其中每個物件型別代表一個元件,從一個物件型別到另一個物件型別的每個關係欄位都代表一個包裝另一個元件的元件。
讓我們通過一個例子來看看是怎麼回事。假設我們要構建以下“熱門導演”小工具:
精選導演小工具
使用Vue或React(或任何其他基於元件的庫),我們將首先識別元件。在這種情況下,我們將有一個外部元件<FeaturedDirector>
(紅色),它wrap一個元件<Film>
(藍色),它本身wrap一個元件<Actor>
(綠色):
識別小工具中的元件
虛擬碼如下所示:
<!-- Component: <FeaturedDirector> --> <div> Country: {country} {foreach films as film} <Film film={film} /> {/foreach} </div> <!-- Component: <Film> --> <div> Title: {title} Pic: {thumbnail} {foreach actors as actor} <Actor actor={actor} /> {/foreach} </div> <!-- Component: <Actor> --> <div> Name: {name} Photo: {avatar} </div>
然後我們確定每個元件需要哪些資料。對於<FeaturedDirector>
,我們需要name
,avatar
和country
。對於<Film>
我們需要thumbnail
和title
。對於<Actor>
我們需要name
和avatar
:
識別每個元件的資料屬性
我們構建了GraphQL查詢來獲取所需的資料:
query { featuredDirector { name country avatar films { title thumbnail actors { name avatar } } } }
可以理解,元件層次結構的形狀和GraphQL查詢之間存在直接關係。事實上,一個GraphQL查詢甚至可以被認為是一個元件層次結構的表示。
使用伺服器端元件解析GraphQL查詢
由於GraphQL查詢具有相同的元件層次結構,PoP 將查詢轉換為其等效的元件層次結構,使用其為元件獲取資料的方法對其進行解析,最後重新建立查詢的形狀以在響應中傳送資料.
讓我們看看這是如何工作的。
為了處理資料,PoP將GraphQL型別轉換為元件:<FeaturedDirector>
=> Director
,<Film>
=> Film
,<Actor>
=> Actor
,並使用使用它們在查詢中出現的順序,PoP建立了一個具有相同元素的虛擬元件層次結構:根元件Director
,wrap元件Film
,wrap元件Actor
。
從現在開始,談論GraphQL型別或PoP元件不再重要。
為了載入它們的資料,PoP在“迭代”中處理它們,在自己的迭代中檢索每種型別的物件資料,如下所示:
處理迭代中的型別
PoP的資料載入引擎實現了以下偽演算法來載入資料:
準備:
- 有一個空佇列儲存必須從資料庫中獲取的物件的ID列表,按型別組織(每個條目將是
[type => list of IDs]
:) - 檢索特色導演物件的ID,並將其放在佇列中的
Director
型別下
迴圈直到佇列中沒有更多條目:
- 從佇列中獲取第一個條目:ID的型別和列表(例如:
Director
和[2]
),並將此條目從佇列中移除 - 對資料庫執行單個查詢以檢索具有這些ID的該型別的所有物件
- 如果該型別具有關係欄位(例如:
Director
型別具有films
型別的關係欄位Film
),則從當前迭代中檢索到的所有物件中收集這些欄位的所有ID(例如:來自Director
型別的所有物件的films
中的所有ID,並將佇列中對應型別下的這些ID(例如:Film
型別下的ID[3, 8]
)。
在迭代結束時,我們將載入所有型別的所有物件資料,如下所示:
處理迭代中的型別
請注意,在佇列中處理該型別之前,如何收集該型別的所有ID。例如,如果我們向型別Director
新增一個關係欄位preferredActors
,這些ID將新增到型別Actor
下的佇列中,並將與型別Film
中的欄位actors
的ID一起處理:
處理迭代中的型別
但是,如果一個型別已被處理,然後我們需要從該型別載入更多資料,那麼它就是該型別的新迭代。例如,將關係欄位preferredDirector
新增到Author
型別中,將使型別Director
再次新增到佇列中:
迭代重複的型別
還要注意,這裡我們可以使用快取機制:在型別Director
的第二次迭代中,不會再次檢索ID為2的物件,因為它在第一次迭代中已經檢索到,因此可以從快取中獲取。
現在我們已經獲取了所有物件資料,我們需要將其塑造成預期的響應,映象GraphQL查詢。目前,資料被組織為關聯式資料庫:
Director
型別表:
ID | NAME | COUNTRY | AVATAR | FILMS |
---|---|---|---|---|
2 | George Lucas | USA | george-lucas.jpg | [3, 8] |
Film
型別表:
ID | TITLE | THUMBNAIL | ACTORS |
---|---|---|---|
3 | The Phantom Menace | episode-1.jpg | [4, 6] |
8 | Attack of the Clones | episode-2.jpg | [6, 7] |
Actor
型別表:
ID | NAME | AVATAR |
---|---|---|
4 | Ewan McGregor | mcgregor.jpg |
6 | Nathalie Portman | portman.jpg |
7 | Hayden Christensen | christensen.jpg |
在這個階段,PoP將所有資料組織為表格,以及每種型別如何相互關聯(即Director
通過films
欄位引用Film
,Film
通過actors
引用Actor
))。然後,通過從根迭代元件層次結構、導航關係並從關係表中檢索相應的物件,PoP將從GraphQL查詢中生成樹形:
樹形響應
最後,將資料列印到輸出中會產生與GraphQ查詢形狀相同的響應:
{ data: { featuredDirector: { name: "George Lucas", country: "USA", avatar: "george-lucas.jpg", films: [ { title: "Star Wars: Episode I", thumbnail: "episode-1.jpg", actors: [ { name: "Ewan McGregor", avatar: "mcgregor.jpg", }, { name: "Natalie Portman", avatar: "portman.jpg", } ] }, { title: "Star Wars: Episode II", thumbnail: "episode-2.jpg", actors: [ { name: "Natalie Portman", avatar: "portman.jpg", }, { name: "Hayden Christensen", avatar: "christensen.jpg", } ] } ] } } }
5.使用元件解析GraphQL查詢的效能分析
讓我們分析資料載入演算法的大O表示法,以瞭解對資料庫執行的查詢數量如何隨著輸入數量的增長而增長,以確保該解決方案具有高效能。
PoP的資料載入引擎在與每種型別對應的迭代中載入資料。當它開始迭代時,它已經擁有所有要獲取的物件的所有 ID 的列表,因此它可以執行 1 個單一查詢來獲取相應物件的所有資料。然後,對資料庫的查詢數量將隨著查詢中涉及的型別數量線性增長。換句話說,時間複雜度是O(n)
,其中n
是查詢中的型別數量(但是,如果一個型別被多次迭代,那麼必須被多次新增到n
)。
這個解決方案的效能非常好,肯定超過了處理圖所期望的指數複雜度,或者處理樹所期望的對數複雜度。
結論
GraphQL伺服器不需要使用圖形來表示資料。在本文中,我們探索了PoP描述的架構,並通過PoP由GraphQL實現,它基於元件並根據型別在迭代中載入資料。
通過這種方法,伺服器可以解決具有線性時間複雜度的GraphQL查詢,這比使用圖或樹所期望的指數或對數時間複雜度要好。
評論留言