graphql 新API 開發方式
我們知道 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 帶來的最大改變和收益。