五分鐘帶你搞懂GraphQL
當(dāng)下,前后端分離是互聯(lián)網(wǎng)應(yīng)用程序開發(fā)的主流做法,如何設(shè)計(jì)合理且高效的前后端交互 Web API 是前端和后臺開發(fā)人員日常開發(fā)工作的一大難點(diǎn)和痛點(diǎn)?;叵胛覀冊谌粘i_發(fā)過程中經(jīng)常會碰到的幾個(gè)場景:
- 后臺開發(fā)人員調(diào)整了返回值的類型和數(shù)量而沒有通知到前端;
- 后臺開發(fā)人員修改了某一個(gè)字段的名稱沒有通知到前端;
- 前端開發(fā)人員需要通過多個(gè)接口的拼接才能獲取頁面暫時(shí)所需要的所有字段;
- 前端開發(fā)人員無論想要多獲取還是少獲取目標(biāo)字段都需要與后臺開發(fā)人員進(jìn)行協(xié)商;
- …
相信你對上面這個(gè)場景非常熟悉,因?yàn)槲覀兠刻於伎赡茉诜磸?fù)經(jīng)歷著類似的場景。那么,如何解決這些問題呢?這就是今天我們要介紹的內(nèi)容,我們將引入 GraphQL 這套全新的技術(shù)體系,它為我們解決上述問題提供了方案。
GraphQL 的基本概念
相比 REST,GraphQL 可以說是一項(xiàng)比較新的技術(shù)體系,2012 年誕生在 Facebook 內(nèi)部,并于 2015 年正式開源。顧名思義,GraphQL 是一種基于圖(Graph)的查詢語言(Query Language,QL),從根本上改變了前后端交互 API 的定義和實(shí)現(xiàn)方式。
要想使用 GraphQL,我們首先需要關(guān)注它發(fā)送請求的方式。這里我們可以舉一個(gè)例子。假設(shè)系統(tǒng)中存在一個(gè)獲取用戶信息的場景,那么一個(gè)典型的請求示例如下所示:
{
user (id: "1") {
name
address
}
}
可以看到基于 GraphQL 的請求方式與使用傳統(tǒng)的 RESTful API 有很大的不同。除了在請求體中指定了目標(biāo) User 對象的參數(shù) id 值之外,我們還額外指定了“name”和“address”這兩個(gè)參數(shù),也就是告訴服務(wù)器端這次請求所希望獲取的數(shù)據(jù)字段。
顯然,這種請求方式完美地解決了前端無法預(yù)判響應(yīng)的數(shù)據(jù)格式問題,因?yàn)榍岸嗽谡埱蟮耐瑫r(shí)已經(jīng)知道從服務(wù)端返回的數(shù)據(jù)字段就是請求中指定的字段,因此前端就不需要再對響應(yīng)結(jié)果進(jìn)行專門的判斷和處理。
針對傳統(tǒng)交互方式中存在的多次請求問題,GraphQL 可以把多次請求合并成一次。例如,我們可以發(fā)送這樣一個(gè)請求。
{
users {
name
address
family{
count
}
}
}
在該請求中,我們一方面指定了想要獲取的 User 對象中的“name”和“address”字段,同時(shí)也指定了需要獲取用戶對應(yīng)的家庭字段“family”以及它的子字段“count”。這樣,通過一次請求,我們就可以同時(shí)獲取用戶信息和家庭信息,而不需要發(fā)送兩次請求。
講到這里,你可能已經(jīng)注意到,通過 GraphQL 發(fā)起請求實(shí)際上只需要指定一個(gè) HTTP 端點(diǎn)地址即可,因?yàn)槲覀兛梢曰谕粋€(gè)端點(diǎn)通過傳入不同的參數(shù)而獲取不同的結(jié)果,也就不需要專門設(shè)計(jì)一批 HTTP 端點(diǎn)來分別處理不同的請求了。
現(xiàn)在,我們已經(jīng)了解了 GraphQL 的功能特性。那么,如何實(shí)施 GraphQL 呢?
GraphQL 的實(shí)施方法
首先明確,我們并不推薦在任何場景下都使用 GraphQL。對于那些 API 定義與資源概念匹配度較高、也不需要實(shí)現(xiàn)類似用戶信息內(nèi)部嵌套家庭成員信息的復(fù)雜查詢場景,傳統(tǒng)的 RESTful API 仍然是首選,各個(gè) HTTP 端點(diǎn)之間相互獨(dú)立,職責(zé)非常明確。但對有些場景而言,GraphQL 則更有優(yōu)勢。包括:
- 業(yè)務(wù)復(fù)雜度高
- 需求變化快
- 弱文檔化管理
對于大多數(shù)系統(tǒng)而言,GraphQL 的推行需要前后端進(jìn)行緊密的配合,在這個(gè)配合過程中,前端的痛點(diǎn)往往是大于服務(wù)端的。所以,一般場景下前端對于引入 GraphQL 的訴求要大于服務(wù)端。另一方面,如果采用和 RESTful API 一樣的開發(fā)模式,那么實(shí)現(xiàn) GraphQL 的工作量主要是在服務(wù)端。服務(wù)端同學(xué)需要基于 GraphQL 規(guī)范重新設(shè)計(jì)并實(shí)現(xiàn) API,通常都建議單獨(dú)構(gòu)建一個(gè)數(shù)據(jù)層來對外暴露 GraphQL API。
圖 1 在后端服務(wù)中構(gòu)建數(shù)據(jù)層示意圖
在實(shí)施 GraphQL 的策略上,我們也可以總結(jié)幾條最佳實(shí)踐。首先,如果你已經(jīng)實(shí)現(xiàn)了一部分 RESTful 服務(wù),那么可以讓 GraphQL 與這部分 RESTful API 并存發(fā)展。尤其是對于那些單一的 RESTful 服務(wù),可以把 GraphQL 直接連接到已有的 RESTful 服務(wù)上。通過這種策略,RESTful 服務(wù)中已經(jīng)實(shí)現(xiàn)的業(yè)務(wù)邏輯層、數(shù)據(jù)訪問層組件都可以得到復(fù)用,我們要做的只是開放一個(gè)新的 GraphQL 訪問入口而已。而且,作為一項(xiàng)新技術(shù)的引入過程,GraphQL 和 RESTful 服務(wù)在一段時(shí)間內(nèi)并存發(fā)展也符合平滑過渡的客觀需求。
圖 2 RESTful 和 GraphQL 并存發(fā)展示意圖
如果你正在采用微服務(wù)架構(gòu),那么引入 GraphQL 的策略就是在所有后端微服務(wù)之前架設(shè)一層由 GraphQL 構(gòu)建的數(shù)據(jù)層,并對后端服務(wù)提供的數(shù)據(jù)進(jìn)行自由的組裝之后開放給前端。這種實(shí)施策略類似于微服務(wù)架構(gòu)中的 API 網(wǎng)關(guān),一方面對前端請求進(jìn)行適配和路由,一方面完成前后端之間的解耦。
GraphQL Java 框架
我們知道,GraphQL 是一種理念和規(guī)范,它并不直接提供開發(fā)工具和框架。而 GraphQL Java 是基于 Java 開發(fā)的一個(gè) GraphQL 實(shí)現(xiàn)庫,在實(shí)際的應(yīng)用開發(fā)中,開發(fā)人員可以創(chuàng)建自己的 Controller 層組件來與 GraphQL 完成整合。當(dāng)然,我們也可以對 GraphQL Java 進(jìn)行一定的封裝和擴(kuò)展。因此,我們需要首先掌握 GraphQL Java 中所包含的核心編程組件。GraphQL Java 中內(nèi)置了一組核心的技術(shù)組件。
圖 3 GraphQL Java 中的核心組件
首先,我們需要引入一個(gè)核心組件,即 Schema。所謂 Schema,簡單講就是一種前后端交互的協(xié)議和規(guī)范,或者可以把它類比成 RESTful API 中的接口定義文檔。在 Schema 中,開發(fā)人員需要指定兩部分內(nèi)容。一方面,我們需要明確定義前后端交互的數(shù)據(jù)結(jié)構(gòu),包括具體的字段名稱、類型、是否為空等屬性。另一方面,GraphQL 規(guī)定每一個(gè) Schema 中可以存在一個(gè)根 Query 和根 Mutation,分別用于執(zhí)行查詢和更新操作。我們來看一個(gè)典型的 Schema 定義。
schema {
query: Query,
mutation: Mutation
}
type Query {
users(filter: InputUser): [User!]
user(id: ID!): User
...
}
type Mutation {
addUser(user: InputUser!): User
....
}
type User{
id: ID!
name: String!
}
input InputUser {
name: String
}
在上述 Schema 中,我們看到了根 Query 和根 Mutation,也看到了 ID、String 等基本數(shù)據(jù)類型和 User 這個(gè)自定義復(fù)雜數(shù)據(jù)結(jié)構(gòu),以及用來表明是否為空的!和數(shù)組的[]。
第二個(gè)要介紹的組件是 DataFetcher。從命名上看,DataFetcher 組件的作用就是在執(zhí)行查詢時(shí)獲取字段對應(yīng)的數(shù)據(jù)。DataFetcher 是一個(gè)接口,只定義了一個(gè)方法,如下所示:
public interface DataFetcher<T> {
T get(DataFetchingEnvironment dataFetchingEnvironment) throws Exception;
}
開發(fā)人員可以從 DataFetchingEnvironment 中獲取傳入的參數(shù),并根據(jù)該參數(shù)來執(zhí)行具體的數(shù)據(jù)查詢操作。至于數(shù)據(jù)查詢操作的具體實(shí)現(xiàn)過程,DataFetcher 并不關(guān)心。
創(chuàng)建 DataFetcher 只是開始,我們還要將它們應(yīng)用在 GraphQL 服務(wù)器上,這就需要借助 RuntimeWiring 組件。通過 Runtime Wiring(運(yùn)行時(shí)組裝)機(jī)制,我們可以把 DataFetcher 整合在 GraphQL 的運(yùn)行環(huán)境中。創(chuàng)建 RuntimeWiring 的典型代碼如下所示:
private UsersDataFetcher usersDataFetcher;
private UserDataFetcher userDataFetcher;
private RuntimeWiring buildRuntimeWiring() {
return RuntimeWiring.newRuntimeWiring()
.type("Query", typeWiring -> typeWiring
.dataFetcher("users", usersDataFetcher)
.dataFetcher("user", userDataFetcher))
.build();
}
可以看到,這里通過 RuntimeWiring 的 type 方法將各個(gè) DataFetcher 與對應(yīng)的數(shù)據(jù)結(jié)構(gòu)關(guān)聯(lián)起來。
最后,基于 Schema 和 RuntimeWiring,我們就可以創(chuàng)建 GraphQL 對象,如下所示:
File schemas = schemeResource.getFile();
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(schemas);
RuntimeWiring wiring = buildRuntimeWiring();
GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(typeRegistry, wiring);
GraphQL graphQL = GraphQL.newGraphQL(schema).build();
基于這個(gè) GraphQL 對象,我們就可以使用它來完成具體的查詢操作,最終面向業(yè)務(wù)層的代碼如下所示:
String query = …;
ExecutionResult result = graphQL.execute(query);
可以看到,使用 GraphQL Java 進(jìn)行開發(fā)的難度并不大,流程也比較固化。這對我們開發(fā)人員來說無疑減輕了很多學(xué)習(xí)的壓力和負(fù)擔(dān)。
總結(jié)
在日常開發(fā)過程中,基于 HTTP 協(xié)議的 RESTful API 是我們目前主流的前后端開發(fā)模式,但并不一定是最合理的開發(fā)模式。今天我們所介紹的 GraphQL 具備的功能特性能夠在一定程度上解決傳統(tǒng)開發(fā)模式中所存在的一些問題。GraphQL 更加具有靈活性和擴(kuò)展性,并能顯著減少前后端交互所需要的溝通和開發(fā)成本。在日常開發(fā)過程中,建議你根據(jù)自身業(yè)務(wù)發(fā)展的需求和變化,在合適的場景中引入 GraphQL 相關(guān)技術(shù)體系。