GraphQL 初探—面向未來 API 及其生態圈

美團點評點餐發表於2017-11-03

什麼是 GraphQL ?第一次看到這個名詞未免讓人聯想到資料庫查詢語言 SQL 。但本質上,這是兩個完全不同的東西, GraphQL 在官方文件裡的定義如下:

GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data.

即 GraphQL 既是一個 API 查詢語言,也指其服務端實現。但 GraphQL 不只是為了在 API 領域搞個類似資料庫的查詢語言,它的誕生更涉及到 API 設計的思路轉變。

REST 模式的難題

通常,一項新技術的產生都會伴隨著兩個背景,一個是該技術所在的領域出現了新趨勢、二是原有的技術難以應對新趨勢。而近幾年, API 領域有幾個趨勢愈發值得關注:

首先是日益增多的移動端應用,和移動端效能本身較低下的矛盾,要求資料載入過程更高效。

再者,要滿足客戶端和前端快速開發、快速新增特性的需求, API 必須能快速擴充。

第三則是各種不同的前端框架和平臺層出不窮,而後端 API 服務面對眾多的前端框架、乃至前端和客戶端共享 API 的情況,其能否按需提供資料,會影響介面複用度和開發效率。

而現如今在 API 領域被廣泛使用的 REST 模式,面對上述愈發複雜的客戶端和服務端互動,問題也漸漸浮現:

首先是介面靈活性差。由於設計介面粒度較粗或歷史遺留原因,介面中有時會存在當前資料互動不需要的欄位,導致取到無用且多餘的資料;而另一方面,有時前端需要一份資料,卻需要手動訪問多個介面才能完整獲取。

第二是介面操作流程繁瑣,回想下前端獲取資料的過程,通常我們要先構造 HTTP 請求,然後接收和解析服務端響應。有時還要對收到的或處理後的資料另作本地資料轉儲,最後才進行 UI 展示。

第三,隨著客戶端功能擴充,服務端不斷增加介面。這樣維護眾多介面,不僅服務端維護成本高,此外也不能按需提供資料、阻礙了客戶端的快速迭代和擴充。

還有 REST 模式實質上是基於 HTTP 協議的,這雖讓其易於被 Web 開發人員理解和上手,但也決定它不能靈活選擇網路協議來解決問題。

GraphQL 的解決方案

面對 REST 模式的上述不足, Facebook 提出了他們的解決方案 – GraphQL :

前面提到 GraphQL 既是一個 API 查詢語言,也指其服務端實現,所以 GraphQL 本身也由兩部分組成,Facebook 將它們分別開源

我們來逐條瞭解下 GraphQL 的特性:

宣告式的資料獲取

如下圖所示,宣告式的資料查詢帶來了介面的精確返回,伺服器會按資料查詢的格式返回同樣結構的 JSON 資料、真正照顧了客戶端的靈活性:

另外,這種資料獲取方式也帶來更簡潔的資料查詢流程。 GraphQL 認為,客戶端只需描述查詢結構發起查詢,再把服務端響應資料用於 UI 展示即可。中間構造請求和轉儲資料的操作可以交由 GraphQL 客戶端輔助完成。

一個服務僅暴露一個 GraphQL 層

上圖是一個 GraphQL 應用的基本架構,其中客戶端只和 GraphQL 層進行 API 互動,而 GraphQL 層再往後接入各種資料來源。這樣一來,只要是資料來源有的資料, GraphQL 層都可以讓客戶端按需獲取,不必專門再去定介面了。

傳輸層無關、資料庫技術無關

帶來了更靈活的技術棧選擇,比如我們可以選擇對移動裝置友好的協議,將網路傳輸資料量最小化,實現在網路協議層面優化應用。

GraphQL 接入概覽

既然 GraphQL 有諸多優點,那又該如何接入呢?大體上,有三種接入的方式:

直連資料庫的GraphQL服務

最為簡潔的服務配置,直接運算元據庫也能減少中間環節的效能損耗。

整合現有服務的GraphQL層

這種配置適合於舊服務的改造,尤其是在涉及第三方服務時、依然可以通過原有介面進行互動。

直連資料庫和整合服務的混合模式

前兩種方式的混合:

GraphQL 核心概念淺析

GraphQL 的一大特點便是宣告式的 API Schema ,GraphQL 的 Schema 是一個宣告式的查詢規範(可認為是伺服器和客戶端間的一個查詢協議),它主要由兩部分組成:

  • 型別系統
  • 編寫語法:SDL(檢視定義語言)

GraphQL 的型別系統包含了各程式語言中通用的一些資料型別,具體可參考規範文件瞭解。

接下來簡單介紹下 GraphQL 的 SDL 語法:

定義 API Schema

自定義型別的定義主要是在服務端完成的,語法如下:

type 型別名 {
    欄位名: 型別
}
複製程式碼

此外, GraphQL 還有 Query, Mutation, Subscription 等特殊的根型別,用於定義 API Schema 。我們可以定義一個使用者:

type User {
    id: Int!
    name: String
}
複製程式碼

然後定義幾個用於資料操作的 API Schema :

type Query { // 基本查詢 Schema
    user(id: Int!): User // 傳入一個 id ,返回具體使用者
}

type Mutation { // 運算元據的 Schema
    createUser( // 傳入使用者名稱自動建立一個使用者
        name: String
    ): User
}

type Subscription { // 監聽資料變更的 Schema
    userChanged: User
}
複製程式碼

資料操作

有了這些定義好的 API Schema ,我們就可以此來發起資料操作了。 GraphQL 的資料操作也分為 Query, Mutation, Subscription 三種型別。簡單來講, Query 就是獲取資料的基本查詢;Mutation 支援對資料的增、刪、改等操作;而 Subscription 則用於監聽資料變動、並靠 Websocket 等協議推送變動的訊息給訂閱方。

基於前面的定義的使用者 Schema ,我們可以寫出如下的資料操作:

query {
  user(id:3) { // 查詢使用者 id 為3的使用者
    name
  }
}

mutation {
  createUser(name: "Tom") { // 新增一個名為 "Tom" 的使用者
    name
    id
  }
}

subscription {
  userChanged { // 監聽使用者資料變動
    name
    id
  }
}
複製程式碼

上面這些查詢,根欄位之後的所有內容稱為查詢的 payload 。服務端會按查詢格式,在 data 欄位返回 payload 中指定的資料,比如 createUser 這個操作就會返回如下的資料:

{
  "data": {
    "createUser": {
      "name": "Tom",
      "id": 9
    }
  }
}
複製程式碼

GraphQL 生態圈

通過 API Schema,我們既可指定 API 功能、同時也能定義客戶端如何請求資料。但前面介紹的只是個規範,而這個 GraphQL 的規範又是如何落地實現的呢?接下來會圍繞服務端、客戶端、除錯工具,介紹下 GraphQL 應用開發的 “生態圈”。

服務端實現

在服務端, GraphQL 伺服器可用任何可構建 Web 伺服器的語言實現。除 JavaScript 之外, Ruby , Python , Scala , Java , Clojure , Go 和 .NET 都有實現供參考。

服務端查詢執行的核心演算法也很簡單:就是查詢逐欄位遍歷,併為各欄位執行一個 resolver 以處理資料操作。下圖舉了一個例子:

最左邊為一個 GraphQL 查詢,該語句查詢了 id 為 'abc' 的作者所有文章的標題和內容。中間一副圖展示了每個查詢欄位對應的資料型別,然後在最右邊可看到每個欄位的解析過程:首先查詢 id 為 'abc' 的作者,再從該作者處獲取其所有文章;而由於文章是一個列表,最後我們還要遍歷這個列表以獲取各文章對應的標題、內容。

這個逐欄位解析的流程清晰易懂,但如果伺服器只是這麼實現的話,就會面臨效能問題。見下圖的例子,若使用者要查詢文章列表下各個作者的資訊,由於文章列表中可能有大量重複的作者,當處理到同一作者的文章時就要重複查詢該作者資訊,甚至當“查詢作者資訊”這操作本身就包含大量子操作的話、對伺服器效能的消耗就非常可觀:

對這種一個查詢觸發大量相同的資料操作的問題,一種解決思路是將資料操作改為批量處理。還是用上面的例子,下圖中我們把查詢作者資訊的操作改為存入一個佇列,待合適的時機再批量發起查詢,這時查詢的數量就只是佇列裡的一個最小子集,避免了重複操作。 Facebook 推出的 DataLoder 就是一個這樣的資料批量處理和快取的方案。

上面討論了 GraphQL 服務端的基本實現思路,而針對 Node.js 的實現,我基於前文示例中的 API Schema 寫了一個簡單的 Demo ,讀者可瞭解下 GraphQL 的服務端具體是如何實現和使用的。

客戶端實現

常見的 GraphQL 客戶端庫有:

  • Relay:Facebook 官方的 GraphQL 客戶端,它大大優化了效能,但只能在 Web 上可用
  • Apollo:一個開源社群專案,旨在為所有開發平臺(Web, 安卓, iOS , React Native 等)構建強大而靈活的 GraphQL 客戶端

至於如何使用這兩個客戶端庫,可以參考官方文件,這裡不再贅述。而對於 Apollo 的入門, Full-stack React + GraphQL Tutorial 一文提供了深入淺出的示例,建議動手嘗試下,構建自己第一個 GraphQL 應用吧。

開發工具

GraphQL 有大量實用的開發工具,基本都是基於 introspection 查詢實現的。所謂 introspection 查詢,就是指客戶端向伺服器詢問 API Schema 資訊的查詢。比如,我們可以通過查詢 __schema 等元欄位來獲取完整的型別資訊:

query {
  __schema {
    types {
      name // 獲取根欄位名
      fields {
        name // 獲取欄位名
      }
    }
  }
}
複製程式碼

有了這樣一個查詢 Schema 資訊的功能,就使得 GraphQL 的文件瀏覽器,自動補全,程式碼生成等開發工具非常容易實現。而開發工具中,最有名的就是 GraphiQL 了,其本質上可認為是個 GraphQL 客戶端,但配有編輯、自動補全、文件瀏覽等功能,常用於服務端的除錯。

前面我們那個服務端 Demo 也以中介軟體形式引入了基於 GraphiQL 的除錯工具 GraphQL PlayGround 。執行 Demo 後,你可以訪問 localhost:3000/playground 試試上面列舉的所有查詢~

GraphQL 存在的問題

當然, GraphQL 也不是完美無缺的,現在 GraphQL 主要存在安全性和服務端快取能力兩方面的問題。

安全問題

GraphQL 宣告式的的資料查詢提供了靈活、易擴充的介面;但如果我們發起的一次查詢包含了過多的資料操作,那麼這一次查詢就會給資料伺服器的帶來巨大的壓力,提升了被 DDOS 的風險。

此外,每次發起的查詢語句,實質上也反映了查詢文件的結構,如果被攻擊者擷取了我們的請求、拼湊出完整的介面內容,這也不利於介面的安全。

面對查詢壓力,我們可以通過服務端限流、客戶端限流等措施來進行緩解,具體限流的措施可參見這篇文章

而對於 API 結構公開傳輸的問題,有人提出一個持久化查詢的方案。簡單來講,就是客戶端和服務端分別將約定好查詢內容轉換為查詢ID,轉而使用查詢ID進行查詢。這樣一來既解決了查詢語句公開傳輸的問題,而只傳 ID 還順便減少了傳輸的資料量、提升了傳輸速度。

服務端快取能力

GraphQL 能讓客戶端靈活地請求資料,這就樣一來客戶端請求內容就是不確定的,服務端難以根據同一個連線來維護查詢快取。

關於這個問題,前面提到 Facebook 有一個 DataLoader 的技術,可用於實現查詢的批量處理和快取,但其文件中描述的快取也只是針對單個請求進行、粒度還是較粗。

總結

GraphQL 作為一個新的 API 標準,通過宣告式的資料獲取方式,給客戶端提供了簡潔、靈活、高效的資料查詢。適應了移動網際網路時代客戶端技術的快速發展和需求的快速迭代,是當前 REST 模式的有力競爭者。

同時其活躍的社群和日漸成熟的生態圈也證明了這是一個很有生命力的技術,目前 GraphQL 已被許多的公司( Facebook , GitHub , Twitter 等等)用於生產環境中,相信其未來還有很大的發展前景。

但 GraphQL 自身存在的安全性等問題也不容忽視;此外引入 GraphQL 勢必存在學習成本,在 API 設計思想上的變化頁還會影響到相應的開發模式、開發流程。所以只有權衡好引入成本和收益,才能讓這項新技術用在刀刃上。

Ref

官方文件
[譯] 怎樣使用GraphQL(文中架構圖引用自該教程)


相關文章