7月份咱們前端團隊推進落地了一個 toB 類型的系統,因爲服務端也由咱們前端工程師來承接,因此服務端技術選型上咱們有了話語權,API 這一起咱們選擇了 GraphQL 。本文將闡述我學習 GraphQL 這門技術的一些思考。javascript
學習一門新技術,首先要把問題域弄清楚。社區有大量 GraphQL 與傳統 API 解決方案(含 REST API)對比文章,總結下來,傳統 API 存在如下問題:html
針對以上問題,GraphQL 給出了較爲完善的解決方案。前端
接下來我經過一個實例講解 GraphQL 解決問題的思路,客戶端的述求:根據性別查詢團隊成員列表,返回 id
、 gender
、 name
、 nickName
,GrahpQL 的處理過程以下圖: java
請求參數在發送到服務端以前會先通過 GraphQL Client 轉換成客戶端 Schema,這段 Schema 實際上是一段 query
開頭的字符串,描述了客戶端的對數據的述求:調用哪一個方法,傳遞什麼樣的參數,返回哪些字段。服務端拿到這段 Schema 以後,經過事先定義好的服務端 Schema 接收請求參數並執行對應的 resolve
函數提供數據服務。整個過程能夠想象成咱們吃自助餐的過程,服務端 Schema 就比如自助餐線,擺上咱們能提供的全部食物;客戶端 Schema 就描述了咱們想要吃的食物,按需獲取就行了。node
講到這裏,好奇心強的同窗可能已經開始思考這個問題了:客戶端 Schema 本質上就是一段字符串,服務端如何識別並響應這段字符串?react
識別與響應客戶端 Schema 依賴於官方類庫 graphql-js ,服務端拿到客戶端 Schema 字符串後會作以下處理: webpack
graphql-js
定義了一系列的特徵標識符:export const TokenKind = Object.freeze({
BANG: '!',
DOLLAR: '$',
PAREN_L: '(',
PAREN_R: ')',
SPREAD: '...',
COLON: ':',
EQUALS: '=',
BRACKET_L: '[',
BRACKET_R: ']',
...
});
複製代碼
並定義了 AST 語法樹規範,規定語法樹支持如下節點:ios
/** * The set of allowed kind values for AST nodes. */
export const Kind = Object.freeze({
// Name
NAME: 'Name',
// Document
DOCUMENT: 'Document',
OPERATION_DEFINITION: 'OperationDefinition',
VARIABLE_DEFINITION: 'VariableDefinition',
VARIABLE: 'Variable',
// Values
INT: 'IntValue',
FLOAT: 'FloatValue',
STRING: 'StringValue',
BOOLEAN: 'BooleanValue',
...
});
複製代碼
有了特徵字符串與 AST 語法樹規範,GraphQL Server 對客戶端 Schema 進行逐字符掃描(charCodeAt),最終解析階段的產出物爲 document
,上文示例中的客戶端 Schema 解析完成以後的部分 document
:git
{
"kind":"Document",
"definitions":[
{
"kind":"OperationDefinition",
"operation":"query",
"name":{
"kind":"Name",
"value":"DisplayMember",
"loc":{
"start":13,
"end":26
}
},
"selectionSet":{
"kind":"SelectionSet",
"selections":[
{
"kind":"Field",
"alias":null,
"name":{
"kind":"Name",
"value":"fetchByGender",
"loc":{
"start":37,
"end":50
}
},
"arguments":[
{
"kind":"Argument",
"name":{
"kind":"Name",
"value":"gender",
"loc":{
"start":51,
"end":57
}
},
"value":{
"kind":"StringValue",
"value":"M",
"loc":{
"start":59,
"end":62
}
},
"loc":{
"start":51,
"end":62
}
}
],
...
複製代碼
若是客戶端 Schema 不符合服務端定義的 AST 規範,解析過程會直接拋出語法異常 Syntax Error
,拿上文的示例舉例,我將客戶端 Schema 中的 fetchByGender(gender: "M")
改成 fetchByGender(gender)
,只傳遞參數名,不傳遞參數值,則服務端會響應:github
{
"errors":[
{
"message":"Syntax Error GraphQL request (3:29) Expected :, found )
2: query DisplayMember {
3: fetchByGender(gender) {
^
4: list {
",
"locations":[
{
"line":3,
"column":29
}
]
}
]
}
複製代碼
結構化的報錯信息也是 GraphQL 的一大特色,定位問題很是方便。只要語法沒問題解析階段就能順利完成,而後進入校驗階段。
校驗階段用於驗證客戶端 Schema 是否按照服務端 Schema 定義好的方式獲取數據,好比:獲取數據的方法名是否有誤,必填項是否有值等等,校驗範圍一共有幾十種,我沒有辦法一一舉例。拿上文的示例舉例,我將客戶端 Schema 中的 fetchByGender
改成 fetchByGen
, fetchByGen
在服務端根本沒有定義,則服務端會響應:
{
"errors":[
{
"message":"Cannot query field "fetchByGen" on type "Query". Did you mean "fetchByGender"?",
"locations":[
{
"line":3,
"column":9
}
]
}
]
}
複製代碼
不只返回結構化的報錯信息,還很是人性化的告訴你正確的調用方式是什麼。校驗階段經過以後會進入執行階段
執行階段依賴的輸入爲:解析階段的產出物 document
,服務端 Schema;其中 document
準確描述了客戶端對數據的述求:請求哪一個方法,參數是什麼,須要哪些字段;服務端 Schema 描述了提供數據的方式;拿上文的示例舉例,服務端 Schema 須要這樣定義:
const graphqlApi = require('graphql');
const {
GraphQLObjectType,
GraphQLList,
GraphQLNonNull,
GraphQLSchema,
GraphQLString,
} = graphqlApi;
const dataSource = require('./dataSource');
const memType = new GraphQLObjectType({
name: 'Male',
description: 'A member gender is Male.',
fields: () => ({
id: {
type: new GraphQLNonNull(GraphQLString),
description: 'The id of member',
},
name: {
type: GraphQLString,
description: 'The name of the character.',
},
nickName: {
type: GraphQLString,
description: 'The nickName of the character.',
},
gender: {
type: GraphQLString,
description: 'The gender of the character.',
},
list: {
type: new GraphQLList(memType),
description: 'The mems list by gender.',
},
})
});
const queryType = new GraphQLObjectType({
name: 'Query',
fields: () => ({
fetchByGender: {
type: memType,
args: {
gender: {
description: 'gender of the human',
type: new GraphQLNonNull(GraphQLString),
},
},
resolve: (root, { gender }) => {
// 訪問數據庫或三方 API 查詢成員列表
return {
list: dataSource.getMembers(gender),
};
},
},
}),
});
module.exports = new GraphQLSchema({
query: queryType,
types: [memType],
});
複製代碼
執行服務端 Schema 中的 resolve
函數,獲得執行階段的輸出:
{
"data":{
"fetchByGender":{
"list":[
{
"id":"1",
"gender":"M",
"name":"童開宏",
"nickName":"慕冥"
}
]
}
}
}
複製代碼
固然要完成服務端 Schema 的定義,你須要學習 GraphQL 的 類型系統 ,你們翻閱 API 文檔便可。
原理弄清楚以後咱們須要對 GraphQL 這門技術的邊界有一個清醒的認識:
客戶端邊界:核心能力是將請求參數按照服務端定義好的 AST 語法樹規範拼裝成客戶端 Schema 字符串,實現方案你們可參考apollo
提供的 Webpack 插件 ,固然也有一些 GraphQL 客戶端連發送 Ajax 請求的活兒也幹了,無非是在底層調用其餘類庫好比 axios
發請求。
服務端邊界:核心能力是識別客戶端 Schema 字符串,並經過服務端 Schema 調用底層的數據服務按需返回用戶想要的數據,至於底層數據源來自哪裏(數據庫或者三方接口),以何種方式獲取數據(直連數據庫或者 ORM 方法調用),這些不屬於 GraphQL 關心的範疇。
因爲 GraphQL 經過客戶端 Schema 而不是經過 URL 描述數據述求,因此理論上服務端只須要對客戶端暴露一個地址便可,解決了接口數量衆多維護成本高的問題;同時,服務端提供的是全量字段,客戶端可按需獲取,面對接口擴展的需求,服務端沒有開發成本;最後,經過 GraphiQL 可視化調試界面展示服務端能提供的全部數據,開發過程再也不依賴接口文檔:
GraphQL 官方提供核心能力:
咱們還缺什麼?
服務端 官方只提供了 JavaScript 語言支持,社區愛好者很快在不一樣編程語言中實現了 GraphQL 的理念:JAVA 、 .NET 等等,更多語言支持,請查看 官網
客戶端 官方提供的 Relay 解決了 GraphQL 與 React 相結合的問題,Apollo Client 提供了與其餘前端框架融合的解決方案,好比 Vue、Angular 等等。
開發體驗
graphql-tools
:在上文示例代碼的服務端 Schema 中,咱們將類型的定義(typeDefs)與處理函數的定義(resolvers)放在同一個文件中,職責上不夠單一,藉助 graphql-tools 咱們能夠將兩者分不一樣的文件定義;
GraphQL 的優勢上文已經講過了,真的是從業務痛點出發,解決了傳統 API 存在的問題,可是 GraphQL 在解決問題的同時也帶了一些新的問題,這些問題在某種程度上阻礙了這門技術的普及:
query IAmEvil {
author(id: "abc") {
posts {
author {
posts {
author {
posts {
author {
# that could go on as deep as the client wants!
}
}
}
}
}
}
}
}
複製代碼
這樣的查詢語句會給數據庫帶來很大的性能開銷,服務端不得不作 限流 來規避這樣的問題,這也帶來了額外的開發成本。
任何技術都有利弊,你們要結合本身的場景權衡收益作出適合本身的技術選型。
文章可隨意轉載,但請保留此 原文連接。 很是歡迎有激情的你加入 ES2049 Studio,簡歷請發送至 caijun.hcj(at)alibaba-inc.com 。