GraphQL 基礎(chǔ)實(shí)踐
編者按:本文作者奇舞團(tuán)前端開(kāi)發(fā)工程師何文力,同時(shí)也是 W3C CSS 工作組成員。
本次內(nèi)容是基于之前分享的文字版,若想看重點(diǎn)的話,可以看之前的 PPT (https://ppt.baomitu.com/d/4248c64a)。
什么是 GraphQL
GraphQL 是一款由 Facebook 主導(dǎo)開(kāi)發(fā)的數(shù)據(jù)查詢和操作語(yǔ)言, 寫(xiě)過(guò) SQL 查詢的同學(xué)可以把它想象成是 SQL 查詢語(yǔ)言,但 GraphQL 是給客戶端查詢數(shù)據(jù)用的。雖然這讓你聽(tīng)起來(lái)覺(jué)得像是一款數(shù)據(jù)庫(kù)軟件,但實(shí)際上 GraphQL 并不是數(shù)據(jù)庫(kù)軟件。你可以將 GraphQL 理解成一個(gè)中間件,是連接客戶端和數(shù)據(jù)庫(kù)之間的一座橋梁,客戶端給它一個(gè)描述,然后從數(shù)據(jù)庫(kù)中組合出符合這段描述的數(shù)據(jù)返回。這也意味著 GraphQL 并不關(guān)心數(shù)據(jù)存在什么數(shù)據(jù)庫(kù)上。
同時(shí) GraphQL 也是一套標(biāo)準(zhǔn),在這個(gè)標(biāo)準(zhǔn)下不同平臺(tái)不同語(yǔ)言有相應(yīng)的實(shí)現(xiàn)。GraphQL 中還設(shè)計(jì)了一套類型系統(tǒng),在這個(gè)類型系統(tǒng)的約束下,可以獲得與 TypeScript 相近的相對(duì)安全的開(kāi)發(fā)體驗(yàn)。
GraphQL 解決了什么問(wèn)題
我們先來(lái)回顧一下我們已經(jīng)非常熟悉的 RESTful API 設(shè)計(jì)。簡(jiǎn)單的說(shuō) RESTful API 主要是使用 URL 的方式表達(dá)和定位資源,用 HTTP 動(dòng)詞來(lái)描述對(duì)這個(gè)資源的操作。
我們以 IMDB 電影信息詳情頁(yè)為例子,看看我們得需要什么樣的 API 才能滿足 RESTful API 設(shè)計(jì)的要求。先來(lái)看看主頁(yè)面上都需要什么信息。
可以看到頁(yè)面上由電影基本信息,演員和評(píng)分/評(píng)論信息組成,按照設(shè)計(jì)要求,我們需要將這三種資源放在不同 API 之下。首先是電影基本信息,我們有 API /movie/:id,給定一個(gè)電影ID返回基本信息數(shù)據(jù)。
假裝 GET 一下獲得一個(gè) JSON 格式的數(shù)據(jù):
{
??name:?“Manchester by the Sea”,
??ratings:?“PG-13”,
??score:?8.2,
??release:?“2016”,
??actors:[“https://api/movie/1/actor/1/”],
??reviews:[“https://api/movie/1/reviews”]
}
這里面包含了我們所需的電影名、分級(jí)等信息,以及一種叫做 HyperMedia 的數(shù)據(jù),通常是一個(gè) URL,指明了能夠獲取這個(gè)資源的 API 端點(diǎn)地址。如果我們跟著 HyperMedia 指向的連接請(qǐng)求下去,我們就能得到我們頁(yè)面上所需的所有信息。
GET /api/movue/1/actor/1
{
??name:?“Ben Affleck”,
??dob:?“1971-01-26”,
??desc:?“blablabla”,
??movies:[“https://api/movie/1”]
}
GET /api/movie/1/reviews
[
??{
?????content:?“Its’s?as?good?as…”,
?????score:?9
??}
]
最后根據(jù)需要,我們要將所有包含需要信息的 API 端點(diǎn)都請(qǐng)求一遍,對(duì)于移動(dòng)端來(lái)說(shuō),發(fā)起一個(gè) HTTP 請(qǐng)求還是比較消耗資源的,特別是在一些網(wǎng)絡(luò)連接質(zhì)量不佳的情況下,一下發(fā)出多個(gè)請(qǐng)求反而會(huì)導(dǎo)致不好的體驗(yàn)。
而且在這樣的 API 設(shè)計(jì)之中,特定資源分布在特定的 API 端點(diǎn)之中,對(duì)于后端來(lái)說(shuō)寫(xiě)起來(lái)是挺方便的,但對(duì)于Web端或者客戶端來(lái)說(shuō)并不一定。例如在 Android 或 iOS 客戶端上,發(fā)版升級(jí)了一個(gè)很爆炸的功能,同一個(gè)API上可能為了支持這個(gè)功能而多吐一些數(shù)據(jù)。但是對(duì)于未升級(jí)的客戶端來(lái)說(shuō),這些新數(shù)據(jù)是沒(méi)有意義的,也造成了一定的資源浪費(fèi)。如果單單將所有資源整合到一個(gè) API 之中,還有可能會(huì)因?yàn)檎狭藷o(wú)關(guān)的數(shù)據(jù)而導(dǎo)致數(shù)據(jù)量的增加。
而 GraphQL 就是為了解決這些問(wèn)題而來(lái)的,向服務(wù)端發(fā)送一次描述信息,告知客戶端所需的所有數(shù)據(jù),數(shù)據(jù)的控制甚至可以精細(xì)到字段,達(dá)到一次請(qǐng)求獲取所有所需數(shù)據(jù)的目的。
GraphQL Hello World
GraphQL 請(qǐng)求體
我們先來(lái)看一下一個(gè) GraphQL 請(qǐng)求長(zhǎng)什么樣:
query myQry?($name:?String!)?{
??movie(name:?“Manchester”)?{
????name
????desc
????ratings
??}
}
這個(gè)請(qǐng)求結(jié)構(gòu)是不是和 JSON 有那么點(diǎn)相似?這是 Facebook 故意設(shè)計(jì)成這樣的,希望你讀完之后就能體會(huì)到 Facebook 的用心良苦了。
那么,上面的這個(gè)請(qǐng)求描述稱為一個(gè) GraphQL 請(qǐng)求體,請(qǐng)求體即用來(lái)描述你要從服務(wù)器上取什么數(shù)據(jù)用的。一般請(qǐng)求體由幾個(gè)部分組成,從里到外了解一下。
首先是字段,字段請(qǐng)求的是一個(gè)數(shù)據(jù)單元。同時(shí)在 GraphQL 中,標(biāo)量字段是粒度最細(xì)的一個(gè)數(shù)據(jù)單元了,同時(shí)作為返回 JSON 響應(yīng)數(shù)據(jù)中的最后一個(gè)字段。也就是說(shuō),如果是一個(gè) Object,還必須選擇至少其中的一個(gè)字段。
把我們所需要的字段合在一起,我們把它稱之為某某的選擇集。上面的 name、desc、ratings 合在一起則稱之為 movie 的選擇集,同理,movie 是 myQry 的選擇集。需要注意的是,在標(biāo)量上使用不能使用選擇集這種操作,因?yàn)樗呀?jīng)是最后一層了。
在 movie 的旁邊,name:?"Manchester",這個(gè)代表著傳入 movie 的參數(shù),參數(shù)名為 name 值為Manchester,利用這些參數(shù)向服務(wù)器表達(dá)你所需的數(shù)據(jù)需要符合什么條件。
最后我們來(lái)到請(qǐng)求體的最外層:
操作類型:指定本請(qǐng)求體要對(duì)數(shù)據(jù)做什么操作,類似與 REST 中的 GET POST。GraphQL 中基本操作類型有 query 表示查詢,mutation 表示對(duì)數(shù)據(jù)進(jìn)行操作,例如增刪改操作,subscription 訂閱操作。
操作名稱:操作名稱是個(gè)可選的參數(shù),操作名稱對(duì)整個(gè)請(qǐng)求并不產(chǎn)生影響,只是賦予請(qǐng)求體一個(gè)名字,可以作為調(diào)試的依據(jù)。
變量定義:在 GraphQL 中,聲明一個(gè)變量使用$符號(hào)開(kāi)頭,冒號(hào)后面緊跟著變量的傳入類型。如果要使用變量,直接引用即可,例如上面的 movie 就可以改寫(xiě)成 movie(name:?$name)。
如果上述三者都沒(méi)有提供,那么這個(gè)請(qǐng)求體默認(rèn)會(huì)被視為一個(gè) query 操作。
請(qǐng)求的結(jié)果
如果我們執(zhí)行上面的請(qǐng)求體,我們將會(huì)得到如下的數(shù)據(jù):
{
??"data":?{
????"movie":?{
??????"name":?"Manchester By the Sea",
??????"desc":?"A depressed uncle is asked to take care of his teenage nephew after the boy's father dies. ",
??????"ratings":?"R"
????}
??}
}
仔細(xì)對(duì)比結(jié)果和請(qǐng)求體的結(jié)構(gòu),你會(huì)發(fā)現(xiàn),與請(qǐng)求體的結(jié)構(gòu)是完全一致的。也就是說(shuō),請(qǐng)求體的結(jié)構(gòu)也確定了最終返回?cái)?shù)據(jù)的結(jié)構(gòu)。
GraphQL Server
在前面的 REST 舉例中,我們請(qǐng)求多個(gè)資源有多個(gè) API 端點(diǎn)。在 GraphQL 中,只有一個(gè) API 端點(diǎn),同樣也接受 GET 和 POST 動(dòng)詞,如要操作 mutation 則使用 POST 請(qǐng)求。
前面還提到 GraphQL 是一套標(biāo)準(zhǔn),怎么用呢,我們可以借助一些庫(kù)去解析。例如 Facebook 官方的 GraphQL.js。以及 Meteor 團(tuán)隊(duì)開(kāi)發(fā)的 Apollo,同時(shí)開(kāi)發(fā)了客戶端和服務(wù)端,同時(shí)也支持流行的 Vue 和 React 框架。調(diào)試方面,可以使用 Graphiql 進(jìn)行調(diào)試,得益于 GraphQL 的類型系統(tǒng)和 Schema,我們還可以在 Graphiql 調(diào)試中使用自動(dòng)完成功能。
Schema
前面我們提到,GraphQL 擁有一個(gè)類型系統(tǒng),那么每個(gè)字段的類型是怎么約定的呢?答案就在本小節(jié)中。在 GraphQL 中,類型的定義以及查詢本身都是通過(guò) Schema 去定義的。GraphQL 的 Schema 語(yǔ)言全稱叫 Schema Definition Language。Schema 本身并不代表你數(shù)據(jù)庫(kù)中真實(shí)的數(shù)據(jù)結(jié)構(gòu),它的定義決定了這整個(gè)端點(diǎn)能干些什么事情,確定了我們能向端點(diǎn)要什么,操作什么。再次回顧一下前面的請(qǐng)求體,請(qǐng)求體決定了返回?cái)?shù)據(jù)的結(jié)構(gòu),而 Schema 的定義決定了端點(diǎn)的能力。
接下來(lái)我們就通過(guò)一個(gè)一個(gè)的例子了解一下 Schema。
類型系統(tǒng)、標(biāo)量類型、非空類型、參數(shù)
先看右邊的 Schema:type 是 GraphQL Schema 中最基本的一個(gè)概念,表示一個(gè) GraphQL 對(duì)象類型,可以簡(jiǎn)單地將其理解為 JavaScript 中的一個(gè)對(duì)象,在 JavaScript 中一個(gè)對(duì)象可以包含各種 key,在 GraphQL 中,type 里面同樣可以包含各種字段(field),而且字段類型不僅僅可以是標(biāo)量類型,還可以是 Schema 中定義的其他 type。例如上面的 Schema 中, Query 下的 movie 字段的類型就可以是 Movie。
在 GraphQL 中,有如下幾種標(biāo)量類型:Int, Float, String, Boolean, ID ,分別表示整型、浮點(diǎn)型、字符串、布爾型以及一個(gè)ID類型。ID類型代表著一個(gè)獨(dú)一無(wú)二的標(biāo)識(shí),ID 類型最終會(huì)被轉(zhuǎn)化成String類型,但它必須是獨(dú)一無(wú)二的,例如 mongodb 中的 _id 字段就可以設(shè)置為ID類型。同時(shí)這些標(biāo)量類型可以理解為 JavaScript 中的原始類型,上面的標(biāo)量類型同樣可以對(duì)應(yīng) JavaScript 中的 Number, Number, String, Boolean, Symbol 。
在這里還要注意一點(diǎn),type Query, Query 類型是 Schema 中所有 query 查詢的入口,類似的還有 Mutation 和 Subscription,都作為對(duì)應(yīng)操作的入口點(diǎn)。
在type Query下的 movie 字段中,我們使用括號(hào)定義我們可以接受的參數(shù)名和參數(shù)的類型。在上面的 Schema 中,后面緊跟著的感嘆號(hào)聲明了此類型是個(gè)不可空類型(Non-Nullable),在參數(shù)中聲明表示該參數(shù)不能傳入為空。如果感嘆號(hào)跟在 field 的后面,則表示返回該 type 的數(shù)據(jù)時(shí),此字段一定不為空。
通過(guò)上面的類型定義,可以看到 GraphQL 中的類型系統(tǒng)起到了很重要的角色。在本例中,Schema 定義了 name 為 String類型,那么你就不能傳 Int類型進(jìn)去,此時(shí)會(huì)拋出類型不符的錯(cuò)誤。同樣的,如果傳出的 ratings 數(shù)據(jù)類型不為 String,也同樣會(huì)拋出類型不符的錯(cuò)誤。
列表(List)、枚舉類型(Enum)
如果我們的某個(gè)字段返回不止一個(gè)標(biāo)量類型的數(shù)據(jù),而是一組,則需要使用List類型聲明,在該標(biāo)量類型兩邊使用中括號(hào)[]包圍即可,與 JavaScript 中數(shù)組的寫(xiě)法相同,而且返回的數(shù)據(jù)也將會(huì)是數(shù)組類型。
需要注意的是[Movie]!與 [Movie!]兩種寫(xiě)法的含義是不同的:前者表示 movies字段始終返回不可為空但Movie元素可以為空。后者表示movies中返回的 Movie 元素不能為空,但 movies字段的返回是可以為空的。
你可能在請(qǐng)求體中注意到,genre 參數(shù)的值沒(méi)有被雙引號(hào)括起來(lái),也不是任何內(nèi)置類型。看到 Schema 定義,COMEDY是枚舉類型MovieTypes中的枚舉成員。枚舉類型用于聲明一組取值常量列表,如果聲明了某個(gè)參數(shù)為某個(gè)枚舉類型,那么該參數(shù)只能傳入該枚舉類型內(nèi)限定的常量名。
傳入復(fù)雜結(jié)構(gòu)的參數(shù)(Input)
前面的例子中,傳入的參數(shù)均為標(biāo)量類型,那么如果我們想傳入一個(gè)擁有復(fù)雜結(jié)構(gòu)的數(shù)據(jù)該怎么定義呢。答案是使用關(guān)鍵字input。其使用方法和type完全一致。
根據(jù)本例中的 Schema 定義,我們?cè)诓樵?search時(shí)data的參數(shù)必須為
{?term:?"Deepwater Horizon"?}
別名(Alias)
想象這么一個(gè)頁(yè)面,我要列出兩個(gè)電影的信息做對(duì)比,為了發(fā)揮 GraphQL 的優(yōu)勢(shì),我要同時(shí)查詢這兩部電影的信息,在請(qǐng)求體中請(qǐng)求 movie 數(shù)據(jù)。前面我們說(shuō)到,請(qǐng)求體決定了返回?cái)?shù)據(jù)的結(jié)構(gòu)。在數(shù)據(jù)返回前查出兩個(gè) key 為 movie 的數(shù)據(jù),合并之后由于 key 重復(fù)而只能拿到一條數(shù)據(jù)。那么在這種情況下我們需要使用別名功能。
別名即為返回字段使用另一個(gè)名字,使用方法也很簡(jiǎn)單,只需要在請(qǐng)求體的字段前面使用別名:的形式即可,返回的數(shù)據(jù)將會(huì)自動(dòng)替換為該名稱。
片段(Fragment)、片段解構(gòu)(Fragment Spread)
在上面的例子中,我們需要對(duì)比兩部電影的數(shù)據(jù)。如果換作是硬件對(duì)比網(wǎng)站,需要查詢的硬件數(shù)量往往不止兩個(gè)。此時(shí)編寫(xiě)冗余的選擇集顯得非常的費(fèi)勁、臃腫以及難維護(hù)。為了解決這個(gè)問(wèn)題,我們可以使用片段功能。GraphQL 允許定義一段公用的選擇集,叫片段。定義片段使用 fragment name on Type 的語(yǔ)法,其中 name為自定義的片段名稱,Type為片段來(lái)自的類型。
本例中的請(qǐng)求體的選擇集公共部分提取成片段之后為
fragment movieInfo on Movie?{
???name
???desc
}
在正式使用片段之前,還需要向各位介紹片段解構(gòu)功能。類似于 JavaScript 的結(jié)構(gòu)。GraphQL 的片段結(jié)構(gòu)符號(hào)將片段內(nèi)的字段“結(jié)構(gòu)”到選擇集中。
接口(Interface)
與其他大多數(shù)語(yǔ)言一樣,GraphQL 也提供了定義接口的功能。接口指的是 GraphQL 實(shí)體類型本身提供字段的集合,定義一組與外部溝通的方式。使用了 implements的類型必須包含接口中定義的字段。
interface?Basic?{
????name:?String!
????year:?Number!
}
type Song?implements?Basic?{
????name:?String!
????year:?Number!
????artist:?[String]!
}
type Video?implements?Basic?{
????name:?String!
????year:?Number!
????performers:?[String]!
}
Query?{
????search(term:?String!):?[Basic]!
}
在本例中,定義了一個(gè)Basic接口,Song以及Video類型都要實(shí)現(xiàn)該接口的字段。然后在search查詢中返回該接口。
searchMedia查詢返回一組Basic接口。由于該接口中的字段是所有實(shí)現(xiàn)了該接口的類型所共有的,在請(qǐng)求體上可以直接使用。而對(duì)于特定類型上的其他非共有字段,例如Video中的performers,直接選取是會(huì)有問(wèn)題的,因?yàn)?span style="margin-right: 2px;margin-left: 2px;border-width: 1px;border-style: solid;border-color: rgb(225, 225, 232);padding: 2px 4px;font-size: 12px;font-family: monospace;color: rgb(221, 17, 68);background-color: rgb(247, 247, 249);border-radius: 2px;word-break: break-all;overflow-wrap: break-word;">searchMedia在返回的數(shù)據(jù)中類型可能是所有實(shí)現(xiàn)了該接口的類型,而在 Song類型中就沒(méi)有performers字段。此時(shí)我們可以借助內(nèi)聯(lián)片段的幫助(下面介紹)。
聯(lián)合類型(Union)
聯(lián)合類型與接口概念差不多相同,不同之處在于聯(lián)合類型下的類型之間沒(méi)有定義公共的字段。在 Union 類型中必須使用內(nèi)聯(lián)片段的方式查詢,原因與上面的接口類型一致。
union SearchResult?=?Song?|?Video
Query?{
????search(term:?String!):?[SearchResult]!
}
內(nèi)聯(lián)片段(Inline Fragment)
對(duì)接口或聯(lián)合類型進(jìn)行查詢時(shí),由于返回類型的不同導(dǎo)致選取的字段可能不同,此時(shí)需要通過(guò)內(nèi)聯(lián)片段的方式?jīng)Q定在特定類型下使用特定的選擇集。內(nèi)聯(lián)選擇集的概念和用法與普通片段基本相同,不同的是內(nèi)聯(lián)片段直接聲明在選擇集內(nèi),并且不需要fragment聲明。
查詢接口的例子:
query?{
????searchMedia(term:?"AJR")?{
????????name
????????year
????????...on Song?{
????????????artist
????????}
????????...on Video?{
????????????performers
????????}
????}
}
首選我們需要該接口上的兩個(gè)公共字段,并且結(jié)果為Song類型時(shí)選取artist字段,結(jié)果為Video類型時(shí)選取performers字段。下面查詢聯(lián)合類型的例子也是一樣的道理。
查詢聯(lián)合類型的例子:
query?{
????searchStats(player:?"Aaron")?{
????????...on NFLScore?{
????????????YDS
????????????TD
????????}
????????...on MLBScore?{
????????????ERA
????????????IP
????????}
????}
}
GraphQL 內(nèi)置指令
GraphQL 中內(nèi)置了兩款邏輯指令,指令跟在字段名后使用。
@include
當(dāng)條件成立時(shí),查詢此字段
query?{
????search?{
????????actors @include(if:?$queryActor)?{
????????????name
????????}
????}
}
@skip
當(dāng)條件成立時(shí),不查詢此字段
query?{
????search?{
????????comments @skip(if:?$noComments)?{
????????????from
????????}
????}
}
Resolvers
前面我們已經(jīng)了解了請(qǐng)求體以及 Schema,那么我們的數(shù)據(jù)到底怎么來(lái)呢?答案是來(lái)自 Resolver 函數(shù)。
Resolver 的概念非常簡(jiǎn)單。Resolver 對(duì)應(yīng)著 Schema 上的字段,當(dāng)請(qǐng)求體查詢某個(gè)字段時(shí),對(duì)應(yīng)的 Resolver 函數(shù)會(huì)被執(zhí)行,由 Resolver 函數(shù)負(fù)責(zé)到數(shù)據(jù)庫(kù)中取得數(shù)據(jù)并返回,最終將請(qǐng)求體中指定的字段返回。
type Movie?{
????name
????genre
}
type Query?{
????movie:?Movie!
}
當(dāng)請(qǐng)求體查詢movie時(shí),同名的 Resolver 必須返回Movie類型的數(shù)據(jù)。當(dāng)然你還可以單獨(dú)為name字段使用獨(dú)立的 Resolver 進(jìn)行解析。后面的代碼例子中將會(huì)清楚地了解 Resolver。
使用 ThinkJS 搭建 GraphQL API
本例中我們將使用 ThinkJS 配合 MongoDB 進(jìn)行搭建 GraphQL API,ThinksJS 的簡(jiǎn)單易用性會(huì)讓你愛(ài)不釋手!
快速安裝
首先安裝 ThinkJS 腳手架 npm install -g think-cli
使用 CLI 快速創(chuàng)建項(xiàng)目 thinkjs new gqldemo
切換到工程目錄中 npm install && npm start
不到兩分鐘,ThinkJS?服務(wù)端就搭建完了,so?easy!
配置 MongoDB 數(shù)據(jù)庫(kù)
由于本人比較喜歡 mongoose,剛好 ThinkJS 官方提供了 think-mongoose 庫(kù)快速使用,安裝好之后我們需要在 src/config/extend.js中引入并加載該插件。
const?mongoose?=?require('think-mongoose');
module.exports?=?[mongoose(think.app)];
接下來(lái),在 adapter.js 中配置數(shù)據(jù)庫(kù)連接
export.model?=?{
????type:?'mongoose',
????mongoose:?{
????????connectionString:?'mongodb://你的數(shù)據(jù)庫(kù)/gql',
????????options:?{}
????}
};
現(xiàn)在,我們?cè)谡麄€(gè) ThinkJS 應(yīng)用中都擁有了 mongoose 實(shí)例,看看還差啥?數(shù)據(jù)模型!
借助 ThinkJS 強(qiáng)大的數(shù)據(jù) 模型功能,我們只需要以數(shù)據(jù)集合的名稱作為文件名建立文件并定義模型即可使用,相比 mongoose 原生的操作更為簡(jiǎn)單。
本例中我們實(shí)現(xiàn) actor 和 movie 兩組數(shù)據(jù),在 model 目錄下分別建立 actor.js 和 movie.js,并在里面定義模型。
actor.js
module.exports?=?class?extends?think.Mongoose?{
??get?schema()?{
????return?{
??????name:?String,
??????desc:?String,
??????dob:?String,
??????photo:?String,
??????addr:?String,
??????movies:?[
????????{
??????????type:?think.Mongoose.Schema.Types.ObjectId,
??????????ref:?'movie'
????????}
??????]
????};
??}
};
movie.js
module.exports?=?class?extends?think.Mongoose?{
??get?schema()?{
????return?{
??????name:?String,
??????desc:?String,
??????ratings:?String,
??????score:?Number,
??????release:?String,
??????cover:?String,
??????actors:?[
????????{
??????????type:?think.Mongoose.Schema.Types.ObjectId,
??????????ref:?'actor'
????????}
??????]
????};
??}
};
處理 GraphQL 請(qǐng)求的中間件
要處理 GraphQL 請(qǐng)求,我們就必須攔截特定請(qǐng)求進(jìn)行解析處理,在 ThinkJS 中,我們完全可以借助中間件的能力完成解析和數(shù)據(jù)返回。中間件的配置在 middleware.js中進(jìn)行。
ThinkJS 中配置中間件有三個(gè)關(guān)鍵參數(shù):
match:?用于匹配?URL,我們想讓我們的請(qǐng)求發(fā)送到?/graphql 中進(jìn)行處理,那么我們對(duì)這個(gè)路徑進(jìn)行 match 后進(jìn)行處理;
handle:中間件的處理函數(shù),當(dāng) match 到時(shí),此處理函數(shù)會(huì)被調(diào)用執(zhí)行,我們的解析任務(wù)也在這里進(jìn)行,并將解析結(jié)果返回;
options:options 時(shí)傳給中間件的參數(shù),我們可以在此將我們的 Schema 等內(nèi)容傳給解析器使用。
我們的中間件配置大概長(zhǎng)這樣:
{
????match:?'/graphql',
????handle:?()?=>?{},
????options:?{}
}
解析 GraphQL 的核心
Apollo Server
Apollo Server 是一款構(gòu)建在 Node.js 基礎(chǔ)上的 GraphQL 服務(wù)中間件,其強(qiáng)大的兼容性以及卓越的穩(wěn)定性是本文選取此中間件的首要因素。
盡管 Apollo Server 沒(méi)有 ThinkJS 版的中間件,但是萬(wàn)變不離其宗,我們可以通過(guò) Apollo Server Core 中的核心方法 runHttpQuery 進(jìn)行解析。
將它安裝到我們的項(xiàng)目中:npm install apollo-server-core graphql --save
編寫(xiě)中間件
runHttpQuery主要接受兩個(gè)參數(shù),第一個(gè)是 GraphQLServerOptions,這個(gè)我們可以不需要配置,留空數(shù)組即可;第二個(gè)是HttpQueryRequest對(duì)象,我們至少需要包含 methods,options以及query,
他們分別表示當(dāng)前請(qǐng)求的方法,GraphQL服務(wù)配置以及請(qǐng)求體。
而GraphQL服務(wù)配置中我們至少要給出 schema, schema 應(yīng)該是一個(gè) GraphQLSchema實(shí)例,對(duì)于我們前面例子中直接寫(xiě)的 Schema Language,是不能被識(shí)別的,此時(shí)我們需要借助 graphql-tools 中的 makeExecutableSchema 工具將我們的 Schema 和 Resolvers 進(jìn)行關(guān)聯(lián)成 GraphQLSchema實(shí)例。
將它安裝到我們的項(xiàng)目中:npm install graphql-tools --save
編寫(xiě) Schema 和 Resolver
在轉(zhuǎn)換成 GraphQLSchema 之前,首先要將我們的 Schema 和 Resolver 準(zhǔn)備好。
運(yùn)用前面所學(xué)的知識(shí),我們可以很快的編寫(xiě)出一個(gè)簡(jiǎn)單的 Schema 提供查詢演員信息和電影信息的接口。
type Movie?{
??name:?String!
??desc:?String!
??ratings:?String!
??score:?Float!
??cover:?String!
??actors:?[Actor]
}
type Actor?{
??name:?String!
??desc:?String!
??dob:?String!
??photo:?String!
??movies:?[Movie]
}
type Query?{
??movie(name:?String!):?[Movie]
??actor(name:?String!):?[Actor]
}
接下來(lái),分別編寫(xiě)解析 Query 下 movie和actor字段的 Resolver 函數(shù)。
const?MovieModel?=?think.mongoose('movie');
const?ActorModel?=?think.mongoose('actor');
module.exports?=?{
????Query:?{
????????movie(prev,?args,?context)?{
??????????return?MovieModel.find({?name:?args.name?})
????????????????.sort({?_id:?-1?})
????????????????.exec();
????????},
????????actor(prev,?args,?context)?{
??????????return?ActorModel.find({?name:?args.name?})
????????????????.sort({?_id:?-1})
????????????????.exec();
????????}
????}
}
為了能夠和 Schema 正確關(guān)聯(lián),Resolver 函數(shù)的結(jié)構(gòu)需要與 Schema 的結(jié)構(gòu)保持一致。
到達(dá)這一步,有沒(méi)有發(fā)現(xiàn)什么不對(duì)呢?
回憶前面的數(shù)據(jù)模型定義,里面的 movies 和 actors 字段是一組另一個(gè)集合中數(shù)據(jù)的引用,目的是方便建立電影和演員信息之間的關(guān)系以及維護(hù),在 Resolver 運(yùn)行之后,movies 和 actors 字段得到的是一組 id,不符合 Schema 的定義,此時(shí) GraphQL 會(huì)拋出錯(cuò)誤。
那么這個(gè)問(wèn)題怎么解決呢?前面講到 Resolver 的時(shí)候說(shuō)到,每個(gè)字段都可以對(duì)應(yīng)一個(gè) Resolver 函數(shù),我們分別對(duì) movies 和 actors 字段設(shè)置 Resolver 函數(shù),將上一個(gè) Resolver 解析出來(lái)的 id 查詢一遍得出結(jié)果,最終返回的數(shù)據(jù)就能符合 Schema 的定義了。
const?MovieModel?=?think.mongoose('movie');
const?ActorModel?=?think.mongoose('actor');
module.exports?=?{
????Query:?{
????????movie(prev,?args,?context)?{
??????????return?MovieModel.find({?name:?args.name?})
????????????????.sort({?_id:?-1?})
????????????????.exec();
????????},
????????actor(prev,?args,?context)?{
??????????return?ActorModel.find({?name:?args.name?})
????????????????.sort({?_id:?-1})
????????????????.exec();
????????}
????},
????Actor:?{
????????movies(prev,?args,?context)?{
????????????return?Promise.all(
????????????????prev.movies.map(_id?=>?MovieModel.findOne({?_id?}).exec())
????????????);
????????}
????},
????Movie:?{
????????actors(prev,?args,?context)?{
????????????return?Promise.all(
????????????????prev.actors.map(_id?=>?ActorModel.findOne({?_id?}).exec())
????????????);
????????}
????}
}
其中用到的 prev 參數(shù)就是上一個(gè) Resolver 解析出的數(shù)據(jù)。
組合成 GraphQLSchema 實(shí)例
有了 Schema 和 Resolver 之后,我們終于可以把它們變成一個(gè) GraphQLSchema 實(shí)例了。
調(diào)用 graphql-tools 中的 makeEcecutableSchema 進(jìn)行組合好,放在 options 里面稍后使用。
此時(shí)我們的中間長(zhǎng)這樣:
const?{?makeExecutableSchema?}?=?require('graphql-tools');
const?Resolvers?=?require('./resolvers');?// 我們剛寫(xiě)的 Resolver
const?Schema?=?require('./schema');?// 我們剛寫(xiě)的 Schema
module.exports?=?{
????match:?'/graphql',
????handle:?()?=>?{},
????options:?{
????????schema:?makeExecutableSchema({
????????????typeDefs:?Schema,
????????????resolvers:?Resolvers
????????})
????}
}
編寫(xiě) handler
有請(qǐng)apollo-server-core 里面的runHttpQuery出場(chǎng)!
const?{?runHttpQuery?}?=?require('apollo-server-core');
參照 apollo-server-koa,快速構(gòu)建出 ThinkJS 版的 apollo-server 中間件。
const?{?runHttpQuery?}?=?require('apollo-server-core');
module.exports?=?(options?=?{})?=>?{
??return?ctx?=>?{
????return?runHttpQuery([ctx],?{
??????method:?ctx.request.method,
??????options,
??????query:
????????ctx.request.method?===?'POST'
????????????ctx.post()
??????????:?ctx.param()
????}).then(
??????rsp?=>?{
????????ctx.set('Content-Type',?'application/json');
????????ctx.body?=?rsp;
??????},
??????err?=>?{
????????if?(err.name?!==?'HttpQueryError')?throw?err;
????????err.headers?&&
??????????Object.keys(err.headers).forEach(header?=>?{
????????????ctx.set(header,?err.headers[header]);
??????????});
????????ctx.status?=?err.statusCode;
????????ctx.body?=?err.message;
??????}
????);
??};
};
接下來(lái)引用到我們中間件的handle配置中,完美,大功告成,用 ThinkJS 搭建的 GraphQL 服務(wù)器就此告一段落,npm start 運(yùn)行起來(lái)之后,用 GraphiQL “播放”一下你的請(qǐng)求體(記得自己先往數(shù)據(jù)庫(kù)灌數(shù)據(jù))。
GraphQL 的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
所見(jiàn)即所得:所寫(xiě)請(qǐng)求體即為最終數(shù)據(jù)結(jié)構(gòu)
減少網(wǎng)絡(luò)請(qǐng)求:復(fù)雜數(shù)據(jù)的獲取也可以一次請(qǐng)求完成
Schema 即文檔:定義的 Schema 也規(guī)定了請(qǐng)求的規(guī)則
類型檢查:嚴(yán)格的類型檢查能夠消除一定的認(rèn)為失誤
缺點(diǎn)
增加了服務(wù)端實(shí)現(xiàn)的復(fù)雜度:一些業(yè)務(wù)可能無(wú)法遷移使用 GraphQL,雖然可以使用中間件的方式將原業(yè)務(wù)的請(qǐng)求進(jìn)行代理,這無(wú)疑也將增加復(fù)雜度和資源的消耗
完整源代碼可以在這里?(https://github.com/NimitzDEV/graphpql-in-thinkjs)找到,中間件可以在這里(https://github.com/NimitzDEV/think-graphql-middleware)找到















