GraphQL 核心概念

A query language created by Facebook for decribing data requirements on complex application data models

系列文章:

  1. GraphQL 核心概念(本文)
  2. graphql-js 淺嘗

最近因為工作上新產品的需要,讓我有機會了解和嘗試 GraphQL。按照套路,在介紹一項新技術的時候總要回答 3 個問題:What, Why & How。

What is GraphQL?

正如副標題所說,GraphQL 是由 Facebook 創造的用於描述複雜數據模型的一種查詢語言。這裡查詢語言所指的並不是常規意義上的類似 sql 語句的查詢語言,而是一種用於前後端數據查詢方式的規範。

Why using GraphQL?

當今客戶端和服務端主要的交互方式有 2 種,分別是 REST 和 ad hoc 端點。GraphQL官網指出了它們的不足之處主要在於:當需求或數據發生變化時,它們都需要建立新的介面來適應變化,而不斷添加的介面,會造成伺服器代碼的不斷增長,即使通過增加介面版本,也並不能夠完全限制伺服器代碼的增長。(更多不足,前往官網查看)

既然,GraphQL 指出了它們的缺點,那麼它自然解決了這些問題。

如何解決的哪?那就得說說 GraphQL 的 3 大特性。

  • 首先,它是聲明式的。查詢的結果格式由請求方(即客戶端)決定而非響應方(即伺服器端)決定,也就是說,一個 GraphQL 查詢結果的返回是同客戶端請求時的結構一樣的,不多不少,不增不減。
  • 其次,它是可組合的。一個 GraphQL 的查詢結構是一個有層次的欄位集,它可以任意層次地進行嵌套或組合,也就是說它可以通過對欄位進行組合、嵌套來滿足需求。
  • 第三,它是強類型的。強類型保證,只有當一個 GraphQL 查詢滿足所設定的查詢類型,那麼查詢的結果才會被執行。

回到之前的問題,也就是說,當需求或數據發生變化時,客戶端可以根據需求來改變查詢的結構,只要查詢結構滿足之前的定義,伺服器端代碼甚至不需要做任何的修改;即使不滿足,也只需修改伺服器端的查詢結構,而不必額外添加新的介面來滿足需求。

Core Concepts

可能你會問,按套路這節不該是 HOW to use GraphQL,怎麼變成了 Core Concepts?

由於,GraphQL 是一種規範,於是,它的實現不限制某種特定語言,每種語言對 GraphQL 都可以有自己的實現,比如相對 JavaScript 就有 graphql-js。既然,實現都不相同,那麼,使用的方法也會不同,所以便不在這裡細述了。

這篇文章主要分享的是 GraphQL 的核心概念,主要分為:Type System, Query Syntax, Validation 和 Introspection 四部分。

Type System

類型系統是整個 GraphQL 的核心,它用來定義每個查詢對象和返回對象的類型,將所有定義的對象組合起來就形成了一整個 GraphQL Schema。

這個概念比較抽象,空說很難理解,還是拿例子來邊看邊說。個人博客相信大家都很熟悉,這裡就嘗試用一個簡單的博客系統的例子來說明,這會比官網星戰的例子簡單一點。

Lets go!

既然是一個博客,那麼,文章肯定少不了,我們首先來建立一個文章的類型。

type Post { id: String, name: String, createDate: String, title: String, subtitle: String, content: String}

這樣,一個簡單的文章類型就定義好了,它是一個自定義的類型,包含了一系列的欄位,巧合的是這些欄位的類型正好都是 String(字元串類型)。

String 沒有定義過,為什麼可以直接使用哪?因為,String 是 GraphQL 支持的 scalar type(標量類型),默認的標量類型還包括 Int,Float, Boolean 和 ID。

許多的博客網站都支持給每篇文章打標籤,那麼我們在來建立一個標籤的類型。

type Tag { id: String, name: String, label: String, createDate: String}

標籤類型和文章類型怎麼整合到一起哪?

GraphQL 不單單支持簡單類型,還支持一些其他類型,如 Object, Enum, List, NotNull 這些常見的類型,還有 Interface, Union, InputObject 這幾個特殊類型。

PS:一直沒搞明白 Interface 和 Union 的區別在哪,它們分別適用於什麼場景?谷歌了一下,還真有篇文章說它們的區別,不過恕我愚鈍,還是沒能領悟,還望大神點撥...

再修改一下之前的文章類型,使一個文章可以包含多個標籤。

type Post { id: String, name: String, createDate: String, title: String, subtitle: String, content: String, tags: [Tag]}

通常在博客網站的標籤列表中會顯示該標籤下的一些文章,由於 GraphQL 是以產品為中心的,那麼在標籤類型下也可以有文章類型。於是,標籤類就變成了

type Tag { id: String, name: String, label: String, createDate: String, posts: [Post]}

可能你會疑惑,文章類型和標籤類型這樣相互嵌套會不會造成死循環?我可以負責任的告訴你:不會。你可以盡情地嵌套、組合類型結構來滿足你的需求。

最後,根據整個博客網站的需求,組合嵌套剛剛定義的文章類型和標籤類型,建立一個根類型作為查詢的 schema。

type Blog { post: Post, // 查詢一篇文章 posts: [Post], // 用於博客首頁,查詢一組文章 tag: Tag, // 查詢一個標籤 tags: [Tag], // 用於博客標籤頁,查詢所有標籤}

OK,我們的類型和 schema 都定義好了,就可以開始查詢了。怎麼查哪?那我們來看看 GraphQL 的查詢語法。

Query Syntax

GraphQL 的查詢語法同我們現在所使用的有一大不同是,傳輸的數據結構並不是 JSON 對象,而是一個字元串,這個字元串描述了客戶端希望服務端返回數據的具體結構。

知道了概念,那麼一個 GraphQL 的查詢到底長什麼樣哪?繼續我們的例子,假設,我們現在要查詢一篇文章,那麼,GraphQL 的查詢語句就可以是這樣。

query FetchPostQuery { post { id, name, createDate, title, subtitle, content, tags { name, label } }}

它相對應的返回就會是類似這樣的一個 JSON 數據。

{ "data": { "post": { "id": "3", "name": "graphql-core-concepts", "createDate": "2016-08-01", "title": "GraphQL 核心概念", "subtitle": "A query language created by Facebook for decribing data requirements on complex application data models", "content": "省略...", "tags": [{ "name": "graphql", "label": "GraphQL" }] } }}

從中我們可以看到,數據返回了整個文章的屬性以及部分的標籤屬性。其中,標籤屬性並沒有返回全部的欄位,而是只返回了 name 和 label 欄位的屬性,做到了返回數據的結構完成同請求數據的結構相同,沒有冗餘的數據。

查詢添加參數的需求也非常基本,在 GraphQL 的查詢語法中也相當簡單,就拿剛剛的例子,要查詢特定的文章就可以把它改成這樣。

query FetchPostQuery { post(name: graphql-core-concepts) { id, name, createDate, title, subtitle, content, tags { name, label } }}

返回的結果會是和之前的一樣。查詢關鍵字只有在多個查詢時才必須,在單個查詢時可以省略。同時,也可以對查詢的返回起別名,再來看看博客的首頁希望展示一個粗略的文章列表,那麼這樣的一個查詢語句可以是

{ postList: posts { id, name, createDate, title, subtitle, tags { name, label } }}

這裡,我們省略了查詢關鍵字,並將 posts 起了一個別名為 postList,返回的結果就會是

{ "data": { "postList": [{ "id": "3", "name": "graphql-core-concepts", "createDate": "2016-08-01", "title": "GraphQL 核心概念", "subtitle": "A query language created by Facebook for decribing data requirements on complex application data models", "tags": [{ "name": "graphql", "label": "GraphQL" }] }, { "id": "2", "name": "redux-advanced", "createDate": "2016-07-23", "title": "Redux 進階", "subtitle": "Advanced skill in Redux", "tags": [{ "name": "javascript", "label": "JavaScript" }, { "name": "redux", "label": "Redux" }, { "name": "state-management", "label": "State management" }, { "name": "angular-1.x", "label": "Angular 1.x" }, { "name": "ui-router", "label": "ui-router" }, { "name": "redux-ui-router", "label": "redux-ui-router" }] }, { "id": "1", "name": "getting-started-with-redux", "createDate": "2016-07-06", "title": "Redux 入門", "subtitle": "A tiny predictable state management lib for JavaScript apps", "tags": [{ "name": "javascript", "label": "JavaScript" }, { "name": "redux", "label": "Redux" }, { "name": "state-management", "label": "State management" }, { "name": "angular-1.x", "label": "Angular 1.x" }] }] }}

同樣,查詢所有標籤的語句就可以是這樣

{ tags { id, name, label, posts { name, title } }}

這樣,一個 GraphQL 的介面,滿足了一個簡單博客網站的所有需求,是不是很神奇?

Validation

由於 GraphQL 是一個強類型語言,所以它可以在執行查詢之前檢查每個查詢語句是否滿足事先設定的 schema,符合則合法,如果查詢語句不合法則不進行查詢。

以上所舉的都是合法的例子,官網上舉了一些例子,這裡就不貼了,我們就總結看看要注意的有哪幾點。

  1. fragment 不能引用自己從而形成一個循環
  2. 不能查詢類型中不存在的欄位
  3. 查詢的欄位如果不是 scalar type(標量類型)或 enum type(枚舉類型),則需要明確該欄位下所包含的欄位
  4. 同上一條相對,如果查詢欄位是 scalar type(標量類型),那麼它就不能再有子欄位

Introspection

Introspection 這個詞的意思是內省,自我檢查(第一次發現英語有語義如此豐富的詞,又暴露辭彙量少了-_-||)。

不扯遠了,在 GraphQL 中 Introspection 是一個非常有用的功能,它可以用來查詢當前 GraphQL 的 schema,從而得知伺服器端支持何種類型的查詢。

這是一個非常強大且有用的功能,可以想像一下,現在大型公司的開發基本上都是前後端分離的,客戶端並不知道伺服器端提供的 schema 結構,但通過 Introspection,客戶端就能獲得當前伺服器端所提供的 schema,這無論對開發,還是調試錯誤都很有幫助。

還是拿剛剛的博客系統來做例子,我們可以通過查詢 __schema 欄位來獲得當前所支持的查詢類型。

// query string{ __schema { types { name } }}// response data{ "data": { "__schema": { "types": [ { "name": "String" }, { "name": "BlogType" }, { "name": "PostType" }, { "name": "ID" }, { "name": "TagType" }, { "name": "__Schema" }, { "name": "__Type" }, { "name": "__TypeKind" }, { "name": "Boolean" }, { "name": "__Field" }, { "name": "__InputValue" }, { "name": "__EnumValue" }, { "name": "__Directive" }, { "name": "__DirectiveLocation" } ] } }}

從返回的數據中可以看到,我們自定義的 BlogType, PostType 和 TagType 類,剩下的都是 GraphQL 內部類型,其中又分為兩類:一類是 ID, String 和 Bealoon 所表示的標量類型,另一類以雙下劃線開頭的是用於自我檢查的類型。

知道了自定義類,假設,還想知道自定義類中包含哪些屬性以及屬性的類型,就可以這樣查詢

// query string{ __type(name: "PostType") { name fields { name, type { name, kind } } }}// response result{ "data": { "__type": { "name": "PostType", "fields": [ { "name": "id", "type": { "name": null, "kind": "NON_NULL" } }, { "name": "name", "type": { "name": null, "kind": "NON_NULL" } }, { "name": "createDate", "type": { "name": null, "kind": "NON_NULL" } }, { "name": "title", "type": { "name": null, "kind": "NON_NULL" } }, { "name": "subtitle", "type": { "name": "String", "kind": "SCALAR" } }, { "name": "content", "type": { "name": "String", "kind": "SCALAR" } }, { "name": "tags", "type": { "name": null, "kind": "LIST" } } ] } }}

最後

總結一下,GraphQL 是一種客戶端同服務端之間數據交互的概念,具有強大、靈活、易擴展等的特點。既然,它是一種概念,那麼,不同的語言就可以有各種不同的實現方式。

概念並不多,在於靈活運用。

PS:再次強調,本文主要講的是 GraphQL 的核心概念,Type System 中所定義的類,都是設計類,並不是具體實現代碼。實現請聽下回分解。

推薦閱讀:

[小心得]快速搭建一個Vue Live Markdown
前端日刊-2018.02.05
前端日刊-2017.12.25
《Oli-Zhao的前端一萬小時》之:做一次山大王,讓你的操作系統乖得像個小綿羊——命令行入門
論前端開發如何把AI帶進項目

TAG:前端開發 | 前端框架 |