我們知道 GraphQL 使用 Schema 來描述數據,並通過制定和實現 GraphQL 規范 定義了支持 Schema 查詢的 DSQL (Domain Specific Query Language,領域特定查詢語言)。Schema 幫助將復雜的業務模型數據抽象拆分成細粒度的基礎數據結構,而 DSQL 的實現則賦予了前端開發者自由組織和定制請求數據的能力。如果以一張圖來表示的話,可以將 GraphQL 看做一條以 通用基礎業務數據模型 為基礎、將傳統后端服務和前端頁面緊密且自由地聯系在一起的紐帶。
為什么 GraphQL 的 Schema 能夠表示出服務器所支持的復雜業務模型數據,GraphQL 的 Query 又是怎樣賦予前端開發者對數據的定制能力,本文將通過分析和理解 GraphQL 的設計來和大家一起探討解答這些問題。
1.GraphQL 的設計
GraphQL 由以下組件構成:
- 類型系統(Type System)
- 查詢語言(Query Language)
- 執行語義(Execution Semantics)
- 靜態驗證(Static Validation)
- 類型檢查(Type Introspection)
作為將數據模型和具體接口實現解耦的 DSL,GraphQL 的基礎組件,也是它最重要的組件之一就是類型系統。
1.1 類型系統
可以將 GraphQL 的類型系統分為標量類型(Scalar Types,標量類型)和其他高級數據類型,標量類型即可以表示最細粒度數據結構的數據類型,可以和 JavaScript 的原始類型對應。GraphQL 規范目前規定支持的標量類型有:
Int
:整數,對應 JavaScript 的 NumberFloat
:浮點數,對應 JavaScript 的 NumberString
:字符串,對應 JavaScript 的 StringBoolean
:布爾值,對應 JavaScript 的 BooleanID
:ID 值,是一個序列化后值唯一的字符串,可以視作對應 ES 2015 新增的 Symbol
Scalar Types
的 JavaScript 參考實現代碼可以查看 這里 。
其他高級數據類型包括:
-
Object
:對象用於描述層級或者樹形數據結構。對於樹形數據結構來說,葉子字段的類型都是標量數據類型。幾乎所有 GraphQL 類型都是對象類型。Object 類型有一個 name 字段,以及一個很重要的 fields 字段。fields 字段可以描述出一個完整的數據結構。例如一個表示地址數據結構的 GraphQL 對象為:
const AddressType = new GraphQLObjectType({ name: 'Address', fields: { street: { type: GraphQLString }, number: { type: GraphQLInt }, formatted: { type: GraphQLString, resolve(obj) { return obj.number + ' ' + obj.street } } } });
-
Interface
:接口接口用於描述多個類型的通用字段,例如一個表示實體數據結構的 GraphQL 接口為:
const EntityType = new GraphQLInterfaceType({ name: 'Entity', fields: { name: { type: GraphQLString } } });
-
Union
:聯合聯合類型用於描述某個字段能夠支持的所有返回類型以及具體請求真正的返回類型,例如一個表示寵物(可以是貓或者狗)的 GraphQL 聯合類型為:
const PetType = new GraphQLUnionType({ name: 'Pet', types: [DogType, CatType], resolveType(value) { if (value instanceof Dog) { return DogType; } if (value instanceof Cat) { return CatType; } } });
-
Enum
:枚舉用於表示可枚舉數據結構的類型,例如表示 RGB 色值的 GraphQL 枚舉類型為:
const RGBType = new GraphQLEnumType({ name: 'RGB', values: { RED: { value: 0 }, GREEN: { value: 1 }, BLUE: { value: 2 }, } });
-
Input Object
:輸入對象是為了查詢(query)而定義的數據類型,不直接重用 Object 類型是因為 Object 的字段可能存在循環引用,或者字段引用了不能作為查詢輸入對象的接口和聯合類型。參考實現中
Input Object
的定義代碼為:export type GraphQLInputType = GraphQLScalarType | GraphQLEnumType | GraphQLInputObjectType | GraphQLList<GraphQLInputType> | GraphQLNonNull< GraphQLScalarType | GraphQLEnumType | GraphQLInputObjectType | GraphQLList<GraphQLInputType> >; export function isInputType(type: ?GraphQLType): boolean { const namedType = getNamedType(type); return ( namedType instanceof GraphQLScalarType || namedType instanceof GraphQLEnumType || namedType instanceof GraphQLInputObjectType ); }
可以看到,Object、Interface 和 Union 三種類型是不能作為輸入對象類型的。
-
List
:列表列表是其他類型的封裝,通常用於對象字段的描述。例如下面 PersonType 類型數據的 parents 和 children 字段:
const PersonType = new GraphQLObjectType({ name: 'Person', fields: () => ({ parents: { type: new GraphQLList(Person) }, children: { type: new GraphQLList(Person) }, }) });
-
Non-Null
:不能為 NullNon-Null 強制類型的值不能為 null,並且在請求出錯時一定會報錯。可以用於必須保證值不能為 null 的字段。例如數據庫的行的 id 字段不能為 null:
const RowType = new GraphQLObjectType({ name: 'Row', fields: () => ({ id: { type: new GraphQLNonNull(GraphQLString) } }) });
還有一種重要的數據類型,即 schema 類型,它描述了后端服務器能夠提供的數據支持。這里先暫時不介紹,因為它涉及 GraphQL 的其他組件,等全部介紹完我們再來看 GraphQL 中 schema 的 具體實現 。
1.2 查詢語言
類型系統對應我們開頭提到的 Schema,是對服務器端數據的描述,而查詢語言則解耦了前端開發者與后端接口的依賴。前端開發者利用查詢語言可以自由地組織和定制系統能夠提供的業務數據。
GraphQL 的一個查詢請求被稱為一份 query 文檔(query document),即 GraphQL 服務能夠解析驗證並執行的一串請求字符串。query 由操作(Operation)和片段(Fragments)組成。一個 query 可以包含多個操作和片段。只有包含操作的 query 才會被 GraphQL 服務執行。但是不包含操作,只有片段的 query 也會被 GraphQL 服務解析驗證,這樣一份片段就可以在多個 query 文檔內使用。
只包含一個操作的 query 可以不帶操作名稱或者使用簡寫形式(即 query 關鍵字加操作名)。query 包含多個操作時,所有操作都必須帶上名稱。
操作(Operations)
GraphQL 規范支持兩種操作:
- query:僅獲取數據(fetch)的只讀請求
- mutation:獲取數據后還有寫操作的請求
在官方提供的參考實現中我們會發現還支持一種操作 subscription ,這是為了處理訂閱更新這種比較復雜的實時數據更新場景而設計的操作,不過目前這種操作還處於試驗階段,不建議在生產環境中使用。
查詢請求的模型可以用下面的圖來表示:
選擇集合(Selection Sets)
選擇集合表示當前選中的數據內容,格式為:
{
Field // 字段名
FragmentSpread // 片段展開 InlineFragment // 內聯片段 }
關於選擇集合的使用,可以參考 graphql-js 的代碼 。參考實現代碼在 這里 。
字段(Field)
字段格式為:
alias:name(argument:value)
其中 alias
是字段的別名,即結果中顯示的字段名稱。
name
為字段名稱,對應 schema 中定義的 fields 字段名。
argument
為參數名稱,對應 schema 中定義的 fields 字段的參數名稱。
value
為參數值,值的類型對應標量類型的值。
例如這樣的請求: http://yunhe.taobao.com/?query={banner{backgroundURL:bg,biaoti:slogan}}
backgroundURL 就是 bg 字段的別名。
片段(Fragment)
片段是 GraphQL 的主要組合數據結構,通過片段可以重用重復的字段選擇,減少 query 中的重復內容。片段又分為 FragmentSpread 和 InlineFragment。例如沒有片段時需要這樣編寫 query:
query noFragments { user(id: 4) { friends(first: 10) { id name profilePic(size: 50) } mutualFriends(first: 10) { id name profilePic(size: 50) } } }
query 中存在下列重復的選擇集合:
{ id name profilePic(size: 50) }
可以用片段簡化為:
query withFragments { user(id: 4) { friends(first: 10) { ...friendFields } mutualFriends(first: 10) { ...friendFields } } } fragment friendFields on User { id name profilePic(size: 50) }
使用片段時需要加上 ...
操作符表示展開片段內容。
內聯片段示例如下:
query inlineFragmentTyping { profiles(handles: ["zuck", "cocacola"]) { handle ... on User { friends { count } } ... on Page { likers { count } } } }
指令(Directives)
指令要解決的是 query 執行時字段參數無法覆蓋的情況,例如引入或者忽略某個字段。指令為 GraphQL 執行添加了更多的信息。
指令實例如下:
query hasConditionalFragment($condition: Boolean) {
...maybeFragment @include(if: $condition) } fragment maybeFragment on Query { me { name } }
include 指令表示只有在 if 參數為 true 時才引入片段表示的字段。
skip 指令表示在 if 參數為 true 時忽略片段中的字段。
熟悉了 類型系統 和 查詢語言 我們就可以用 GraphQL 來實現應用層的數據請求了。其他三個 GraphQL 組件更偏向於 DSL 的實現和原理,因此本文不再做詳細介紹,感興趣的同學可以對照 規范 和 參考實現 自己研究。
2.總結
GraphQL 是在應用層對業務數據模型的抽象,是對數據請求定制的 DSQL,它解除了接口和數據之間的綁定,對業務數據結構做了抽象和整理,業務邏輯中的數據依賴於底層數據庫結構,並且可以由具體業務場景來定制,不同的業務場景只要基於同樣一套基礎業務數據模型就可以得到復用,在我看來,這才是 GraphQL 帶來的最大改變和收益。