GraphQL服務(wù)器的結(jié)構(gòu)和實(shí)現(xiàn)(第一部分)
英文原址 該序列文章其實(shí)我覺得只有第三篇有些用處前兩篇鳞上、內(nèi)容相對比較尷尬(這第一篇只是順手翻譯過來)攀痊。如果你比較趕時間或者初學(xué)者其實(shí)本篇我覺得不太適合藐不,感興趣可以直接看第二(英文)和第三(中文)篇
在開始使用GraphQL時抡砂,首先要問的問題之一是如何構(gòu)建GraphQL服務(wù)器贞奋?facebook
已經(jīng)已經(jīng)簡單地發(fā)布了一個GraphQL規(guī)范,GraphQL服務(wù)器理論上可以使用任何您喜歡的編程語言實(shí)現(xiàn)咆霜。
在開始構(gòu)建服務(wù)器之前邓馒,GraphQL要求您設(shè)計一個 Schema
,該Schema
定義服務(wù)器的的API蛾坯。在這篇文章中光酣,我們希望了解Schema
的主要組件,闡明實(shí)際實(shí)現(xiàn)它的機(jī)制脉课,并了解如何使用GraphQL.js這樣的庫救军,graphql-tools
與graphene-js
在此過程中為您提供幫助。
本文僅涉及簡單的GraphQL功能 - 沒有定義服務(wù)器如何與客戶端通信的網(wǎng)絡(luò)層概念倘零。重點(diǎn)是“GraphQL執(zhí)行引擎”和查詢解析過程的內(nèi)部工作原理唱遭。要了解網(wǎng)絡(luò)層,請查看下一篇文章呈驶。
GraphQL Schema 定義服務(wù)器的API
Defining schemas:模式定義語言
GraphQL有自己的類型語言拷泽, GraphQL Schema:Schema Definition Language
(SDL)。在最簡單的形式中袖瞻,GraphQL SDL可用于定義如下所示的類型:
type User {
id: ID!
name: String
}
User
單獨(dú)的類型不會向客戶端應(yīng)用程序公開任何功能司致,它只是在應(yīng)用程序中定義用戶模型的結(jié)構(gòu)。為了增加功能的API虏辫,你需要字段添加到根類型的GraphQL架構(gòu):Query
蚌吸,Mutation
和Subscription
。這些類型定義了GraphQL API 的入口點(diǎn)砌庄。
例如羹唠,請考慮以下查詢:
query {
user(id: "abc") {
id
name
}
}
僅當(dāng)相應(yīng)的GraphQL架構(gòu)Query
使用以下user
字段定義根類型時,此查詢才有效:
type Query {
user(id: ID!): User
}
因此娄昆,Schema
的根類型決定了服務(wù)器可以接受的Query
佩微,Mutation
的形狀。
GraphQL架構(gòu)為客戶端 - 服務(wù)器通信提供了明確的契約萌焰。
該GraphQL Schema
對象是GraphQL服務(wù)器的核心
GraphQL.js是Facebook的GraphQL參考實(shí)現(xiàn)哺眯,為其他庫提供了基礎(chǔ),比如graphql-tools
和graphene-js
扒俯。使用這些庫中的任何一個時奶卓,您的開發(fā)過程都以一個GraphQL Schema
對象為中心,包括兩個主要組件:
-
Schema
定義 - 解析器函數(shù)形式實(shí)現(xiàn)
對于上面的示例撼玄,該GraphQLSchema
對象如下所示:
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString }
}
})
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: {
id: { type: GraphQLID }
}
}
}
})
})
如您所見夺姑,Schema
的SDL版本可以直接轉(zhuǎn)換為類型的JavaScript表示形式GraphQL Schema
。請注意掌猛,此Schema
沒有任何解析器 - 因此不允許您實(shí)際執(zhí)行任何Query
盏浙,Mutation
。更多關(guān)于下一節(jié)的內(nèi)容。
解析器實(shí)現(xiàn)API
GraphQL服務(wù)器中的結(jié)構(gòu)與行為
GraphQL具有明確的結(jié)構(gòu) 和行為分離废膘。GraphQL服務(wù)器的結(jié)構(gòu) - 正如我們剛剛討論的那樣 - 是它的Schema
竹海,是服務(wù)器功能的抽象描述。這個結(jié)構(gòu)通過一個確定服務(wù)器行為的具體實(shí)現(xiàn)來實(shí)現(xiàn)丐黄。實(shí)現(xiàn)的關(guān)鍵組件是所謂的解析器功能斋配。
GraphQL架構(gòu)中的每個字段都由解析程序支持。
在最基本的形式中灌闺,GraphQL服務(wù)器在其模式中的每個字段都有一個解析器函數(shù)许起。每個解析器都知道如何獲取其字段的數(shù)據(jù)。由于GraphQL查詢本質(zhì)上只是一個字段集合菩鲜,因此為了收集所請求的數(shù)據(jù)园细,實(shí)際需要做的所有GraphQL服務(wù)器都會調(diào)用查詢中指定的字段的所有解析器函數(shù)。(這也是為什么GraphQL經(jīng)常與RPC風(fēng)格的系統(tǒng)進(jìn)行比較的原因接校,因?yàn)樗举|(zhì)上是一種用于調(diào)用遠(yuǎn)程函數(shù)的語言猛频。)
解析器解析器功能
使用GraphQL.js時,GraphQL Schema
對象中某個類型的每個字段都可以resolve
附加一個函數(shù)蛛勉。讓我們從上面考慮我們的例子鹿寻,特別user
是Query
類型上的字段- 這里我們可以添加一個簡單的resolve
函數(shù)如下:
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: {
id: { type: GraphQLID }
},
resolve: (root, args, context, info) => {
const { id } = args // the `id` argument for this field is declared above
return fetchUserById(id) // hit the database
}
}
}
})
})
假設(shè)一個函數(shù)fetchUserById
實(shí)際可用并返回一個User
實(shí)例(帶有id
和name
字段的JS對象),該resolve
函數(shù)現(xiàn)在可以執(zhí)行該Schema
诽凌。
在我們深入研究之前毡熏,讓我們花一點(diǎn)時間來理解傳遞給解析器的四個參數(shù):
-
root
(有時也稱為parent
):還記得我們?nèi)绾握f一個GraphQL服務(wù)器需要做的就是解析一個查詢是調(diào)用查詢字段的解析器嗎?好吧侣诵,它正在做廣度優(yōu)先(逐級)并且root
每個解析器調(diào)用中的參數(shù)只是前一次調(diào)用的結(jié)果(如果沒有另外指定則初始值為null
)痢法。 -
args
:該參數(shù)攜帶用于查詢的參數(shù),在這種情況下id
的User
要提取杜顺。 -
context
:一個通過解析器鏈傳遞的對象财搁,每個解析器可以寫入和讀取(基本上是解析器通信和共享信息的方法)躬络。 -
info
:查詢或變異的AST表示尖奔。您可以在本系列的第III部分中閱讀有關(guān)詳細(xì)信息的更多信息:揭開GraphQL解析器中的信息參數(shù)的神秘面紗。
之前我們曾聲明GraphQL架構(gòu)中的每個字段都由解析器函數(shù)支持∏畹保現(xiàn)在我們只有一個解析器提茁,而我們的模式總共有三個字段:Query
類型上的根字段用戶,以及類型上的id
和name
字段User
馁菜。其余兩個領(lǐng)域仍然需要他們的解析器茴扁。正如您將看到的,這些解析器的實(shí)現(xiàn)是微不足道的:
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: {
type: GraphQLID,
resolve: (root, args, context, info) => {
return root.id
}
},
name: {
type: GraphQLString,
resolve: (root, args, context, info) => {
return root.name
}
}
}
})
查詢執(zhí)行
注:
根(域)
指Query火邓,Mutation和Subscription
考慮到我們上面的查詢丹弱,讓我們了解它是如何執(zhí)行的以及如何收集數(shù)據(jù)。在總查詢中包含三個字段:user
(在根字段)铲咨,id
和name
躲胳。這意味著當(dāng)查詢到達(dá)服務(wù)器時,服務(wù)器需要調(diào)用三個解析器函數(shù) - 每個字段一個纤勒。讓我們來看看執(zhí)行流程:
- 查詢到達(dá)服務(wù)器坯苹。
- 服務(wù)器為根域調(diào)用解析器
user
- 讓我們假設(shè)fetchUserById
返回此對象:{ "id": "abc", "name": "Sarah" }
- 服務(wù)器為類型
id
上的字段調(diào)用解析程序User
。root
此解析程序的輸入?yún)?shù)是前一次調(diào)用的返回值摇天,因此它可以簡單地返回root.id
粹湃。 - 類似于3,但最后返回
root.name
泉坐。(注意3和4可以并行發(fā)生为鳄。) - 解決過程終止 - 最后結(jié)果包含一個
data
字段以符合GraphQL規(guī)范:
{
"data": {
"user": {
"id": "abc",
"name": "Sarah"
}
}
}
現(xiàn)在,你真的需要寫解析器為user.id
和user.name
自己呢腕让?使用GraphQL.js時孤钦,如果實(shí)現(xiàn)與示例中一樣簡單,則不必實(shí)現(xiàn)解析程序纯丸。因此偏形,您可以省略它們的實(shí)現(xiàn),因?yàn)镚raphQL.js已經(jīng)根據(jù)字段名稱和根參數(shù)推斷出它需要返回的內(nèi)容觉鼻。
優(yōu)化請求:DataLoader模式
使用上述執(zhí)行方法俊扭,當(dāng)客戶端發(fā)送深層嵌套查詢時,很容易遇到性能問題坠陈。假設(shè)我們的API也有包含要求和允許此查詢的注釋的文章:
query {
user(id: "abc") {
name
article(title: "GraphQL is great") {
comments {
text
writtenBy {
name
}
}
}
}
}
請注意萨惑,我們?nèi)绾我筇囟?code>article的給予user
,以及其comments
與name
誰寫它們的用戶號第
我們假設(shè)這篇文章有五條評論仇矾,都是由同一個用戶編寫的咒钟。這意味著我們會擊中writtenBy
解析器五次,但每次都會返回相同的數(shù)據(jù)若未。該的DataLoader讓你在這些種情況朱嘴,以避免N + 1查詢問題優(yōu)化-總的想法是,解析器調(diào)用批處理粗合,因此數(shù)據(jù)庫(或其他數(shù)據(jù)源)只需要進(jìn)行一次打擊萍嬉。
要了解有關(guān)DataLoader的更多信息,您可以觀看Lee Byron 撰寫的精彩視頻:DataLoader - 源代碼演練(約35分鐘)
GraphQL.js vs. graphql-tools
現(xiàn)在讓我們來談?wù)効捎玫膸煜毒危鼈兛梢詭椭贘avaScript中實(shí)現(xiàn)GraphQL服務(wù)器 - 這主要是關(guān)于GraphQL.js和graphql-tools
壤追。之間的區(qū)別。
GraphQL.js為graphql-tools提供了基礎(chǔ)
要理解的第一個關(guān)鍵事項(xiàng)是GraphQL.js提供了基礎(chǔ)graphql-tools
供屉。它通過定義所需類型行冰,實(shí)現(xiàn)模式構(gòu)建以及查詢驗(yàn)證和解決方案來完成所有繁重任務(wù)溺蕉。graphql-tools
然后在GraphQL.js之上提供一個瘦的便利層。
讓我們快速瀏覽一下GraphQL.js提供的功能悼做。請注意疯特,其功能通常以GraphQLSchema
:
-
parse
并且buildASTSchema
:給定在GraphQL SDL中定義為字符串的GraphQL架構(gòu),這兩個函數(shù)將創(chuàng)建一個GraphQLSchema實(shí)例:const schema = buildASTSchema(parse(sdlString))
肛走。 -
validate
:給定GraphQLSchema
實(shí)例和查詢漓雅,validate
確保查詢遵循模式定義的API。 -
execute
:給定GraphQLSchema
實(shí)例和查詢朽色,execute
調(diào)用查詢字段的解析器并根據(jù)GraphQL規(guī)范創(chuàng)建響應(yīng)邻吞。當(dāng)然,這只有在解析器是GraphQLSchema
實(shí)例的一部分時才有效(否則它只是帶有菜單但沒有廚房的餐廳)葫男。 -
printSchema
:獲取一個GraphQLSchema
實(shí)例并在SDL中返回其定義(作為字符串)抱冷。
請注意,GraphQL.js中最重要的功能是graphql
獲取GraphQLSchema
實(shí)例和查詢 - 然后調(diào)用validate
和execute
:
graphql(schema, query).then(result => console.log(result))
要了解所有這些函數(shù)梢褐,請查看這個簡單的節(jié)點(diǎn)腳本徘层,該腳本在一個簡單的示例中使用它們。
該graphql
函數(shù)正在對一個模式執(zhí)行GraphQL查詢利职,該模式本身已經(jīng)包含結(jié)構(gòu)和行為趣效。因此,主要作用graphql
是協(xié)調(diào)解析器函數(shù)的調(diào)用猪贪,并根據(jù)提供的查詢的形狀打包響應(yīng)數(shù)據(jù)跷敬。在這方面,該功能實(shí)現(xiàn)的graphql
功能也稱為GraphQL引擎热押。
graphql-tools
:橋接接口和實(shí)現(xiàn)
使用GraphQL的一個好處是西傀,您可以采用模式優(yōu)先開發(fā)過程,這意味著您構(gòu)建的每個功能首先在GraphQL模式中顯示 - 然后通過相應(yīng)的解析器實(shí)現(xiàn)桶癣。這種方法有許多好處拥褂,例如它允許前端開發(fā)人員在由后端開發(fā)人員實(shí)際實(shí)現(xiàn)之前開始對抗模擬的API - 感謝SDL。
GraphQL.js的最大缺點(diǎn)是它不允許您在SDL中編寫模式牙寞,然后輕松生成a的可執(zhí)行版本
GraphQLSchema
饺鹃。
如上所述,您可以GraphQLSchema
使用parse
和從SDL 創(chuàng)建實(shí)例buildASTSchema
间雀,但這缺少了resolve
使執(zhí)行成為可能的必需功能悔详!GraphQLSchema
使用GraphQL.js 創(chuàng)建可執(zhí)行文件的唯一方法是手動將resolve
函數(shù)添加到模式的字段中。
graphql-tools
用一個重要的功能填補(bǔ)這個空白:addResolveFunctionsToSchema
惹挟。這非常有用茄螃,因?yàn)樗捎糜谔峁└玫幕赟DL的API來創(chuàng)建模式。而這恰恰graphql-tools
與以下內(nèi)容有關(guān)makeExecutableSchema
:
const { makeExecutableSchema } = require('graphql-tools')
const typeDefs = `
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String
}`
const resolvers = {
Query: {
user: (root, args, context, info) => {
return fetchUserById(args.id)
}
},
}
const schema = makeExecutableSchema({
typeDefs,
resolvers
})
因此连锯,使用graphql-tools
它的最大好處是它可以很好地連接聲明性模式和解析器归苍!
什么時候不用graphql-tools
用狱?
我們剛剛了解到,graphql-tools
它的核心在GraphQL.js之上提供了一個便利層拼弃,那么在它不是實(shí)現(xiàn)服務(wù)器的正確選擇時會出現(xiàn)這種情況嗎夏伊?
與大多數(shù)抽象一樣,graphql-tools
通過犧牲其他地方的靈活性肴敛,可以使某些工作流更容易。它提供了一個驚人的“入門” - 經(jīng)驗(yàn)吗购,并避免摩擦医男,快速建立一個GraphQLSchema
。如果你的后端有更多的自定義要求捻勉,比如動態(tài)構(gòu)建和修改你的模式镀梭,它的緊身胸衣可能有點(diǎn)太緊 - 在這種情況下你可以回到使用GraphQL.js。
快速說明 graphene-js
graphene-js
是一個新的GraphQL庫踱启,遵循Python對應(yīng)的想法报账。它還使用了GraphQL.js,但不允許SDL中的模式聲明埠偿。
graphene-js
深深地接受現(xiàn)代JavaScript語法透罢,提供直觀的API,其中查詢和突變可以作為JavaScript類實(shí)現(xiàn)冠蒋∮鹌裕看到更多的GraphQL實(shí)現(xiàn)以新鮮的想法豐富生態(tài)系統(tǒng),這是非常令人興奮的抖剿!
結(jié)論
在本文中朽寞,我們揭示了GraphQL執(zhí)行引擎的機(jī)制和內(nèi)部工作原理。從GraphQL模式開始斩郎,該模式定義服務(wù)器的API并確定將接受哪些查詢和突變脑融,以及響應(yīng)格式的外觀。然后缩宜,我們深入研究了解析器函數(shù)肘迎,并概述了GraphQL引擎在解析傳入查詢時啟用的執(zhí)行模型。最后概述了可用于幫助您實(shí)現(xiàn)GraphQL服務(wù)器的JavaScript庫锻煌。
如果您想要實(shí)際了解本文中討論的內(nèi)容膜宋,請查看此存儲庫。請注意炼幔,它有一個
graphql-js
和graphql-tools
分支來比較不同的方法秋茫。
通常,重要的是要注意GraphQL.js提供了構(gòu)建GraphQL服務(wù)器所需的所有功能 - graphql-tools
只需在頂部實(shí)現(xiàn)一個便利層乃秀,以滿足大多數(shù)用例并提供一個很棒的“入門”體驗(yàn)肛著。只有在構(gòu)建GraphQL架構(gòu)的更高級要求時圆兵,才可以關(guān)閉手套并使用簡單的GraphQL.js。
在接下來的文章中枢贿,我們將討論實(shí)施GraphQL服務(wù)器殉农,比如網(wǎng)絡(luò)層和不同的庫express-graphql, apollo-server
和graphql-yoga
。然后局荚,第3部分介紹了GraphQL解析器中info對象的結(jié)構(gòu)和作用超凳。