01.21 為什麼說 GraphQL 可以取代 REST API?

為什麼說 GraphQL 可以取代 REST API?

幾年前,我在 DocuSign 帶領了一個開發團隊,任務是重寫一個有數千萬個用戶在使用的 Web 應用程序。當時還沒有可以支持前端的 API,因為從一開始,Web 應用程序就是一個.NET 大單體。西雅圖的 API 團隊在將拆分單體,並逐步暴露出 RESTful API。這個 API 團隊由兩名工程師組成,發佈週期為一個月,而我們在舊金山的前端團隊每週都會發布新版本。

API 團隊的發佈週期太長,因為很多(幾乎所有)功能都必須進行手動測試,這是可以理解的。它畢竟是一個單體,而且沒有適當的自動化測試——如果他們修改了一個地方,不知道在應用程序的其他地方會出現什麼問題。

我記得有一次,我們的前端團隊面臨為某大會交付新版本的壓力,但我們忘記跟進一個重要的 API 變更,這個變更未被包含在即將發佈的 API 版本中。我們要麼一直等待,直到錯過截止日期,要麼有人願意放棄優先權,以便讓我們的變更包括在即將發佈的版本中。所幸的是,這個變更最後被包含在新版本中,我們也及時發佈了新的前端版本。我真的希望當時我們已經使用了 GraphQL,因為它可以消除對外部團隊及其發佈週期的重度依賴。

在這篇文章中,我將介紹 GraphQL 的優勢,以及為什麼它會變得如此受歡迎。

很多公司已經在內部從 RESTful 轉向了 GraphQL API:IBM、Twitter、Walmart Labs、紐約時報、Intuit、Coursera,等等。

其他一些公司不僅是在內部而且還將外部 API 也轉為 GraphQL:AWS、Yelp、GitHub、Facebook 和 Shopify,等等。GitHub 甚至打算停止使用 REST API,他們的 v4 版本只使用 GraphQL。

GraphQL 究竟是一個炒作流行語還是真正會帶來一場變革?有趣的是,我之前列出的大多數從 GraphQL 獲益的公司都有以下這些共同點。

  • 他們擁有包括移動端在內的多個客戶端;
  • 他們正在轉向或者已經採用了微服務架構;
  • 他們的遺留 REST API 數量暴增,變得十分複雜;
  • 他們希望消除客戶端團隊對 API 團隊的依賴;
  • 他們注重良好的 API 文檔和開發者體驗。

GitHub 工程團隊表明了他們的動機:

“GraphQL 彌合了發佈的內容與可以使用的內容之間的差距。我們真的很期待能夠同時發佈它們。GraphQL 代表了 API 開發的巨大飛躍。類型安全、內省、生成文檔和可預測的響應都為我們平臺的維護者和消費者帶來了好處。我們期待著由 GraphQL 提供支持的平臺進入新時代,也希望你們也這樣做!”

GraphQL 加速了開發速度,提升了開發者體驗,並提供了更好的工具。我並不是說這絕對是這樣的,但我會盡力說明 GraphQL 與 REST 之間的爭論點及其原因。

超級數據聚合器

我是 Indeed(世界排名第一的求職網站)的軟件工程負責人,所以讓我們先來看看 Indeed.com 的主頁和職位查詢結果頁面。它們分別發出了 10 和 11 個 XHR 請求。

為什麼說 GraphQL 可以取代 REST API?


需要注意的是,在 REST 中使用 POST 進行頁面瀏覽並不是很“正規”。

為什麼說 GraphQL 可以取代 REST API?


以下是其中的一些調用:

  • GET https://inbox.indeed.com/api/getConversationCount
  • GET https://www.indeed.com/rpc/jobdescs
  • GET https://www.indeed.com/rpc/vjslog
  • GET https://www.indeed.com/rpc/preccount
  • POST https://www.indeed.com/rpc/jobalert
  • POST https://www.indeed.com/rpc/count

在使用 GraphQL 時,上面的這些請求可以被包含在單個查詢和單個請求中。

複製代碼

query HomePage {
getConversationCount(...) {
...
}
jobdescs(...) {
...
}
vjslog(...) {
...
}
preccount(...) {

}
jobalert(...) {

}
count(...) {

}
}

響應結果可能是這樣的:

複製代碼

{
"data": {
"getConversationCount": [
{
...
}
],

"vjslog": [...],
"preccount": [...],
"jobalert": [...],
"count": {}
},
"errors": []
}

通常,單個調用比多個調用更方便、更有效,因為它需要更少的代碼和更少的網絡開銷。來自 PayPal 過程團隊的開發體驗還證實,很多 UI 工作實際上不是 UI 工作,而是其他任務,例如前端和後端之間的通信:

“我們發現,UI 開發人員實際用於構建 UI 的時間不到三分之一,剩下的時間用於確定在何處以及如何獲取數據、過濾 / 映射數據以及編排 API 調用,還有一些用於構建和部署。”

需要注意的是,有實時使多個請求也是有必要的,例如多個單獨的請求可以快速且異步獨立地獲取不同的數據,如果採用了微服務架構,它們會增加部署靈活性,而且它們的故障點是多個,而不是一個。

此外,如果頁面是由多個團隊開發的,GraphQL 提供了一個功能,可以將查詢分解稱為片段。稍後我們將詳細介紹這方面的內容。

從更大的角度來看,GraphQL API 的主要應用場景是 API 網關,在客戶端和服務之間提供了一個抽象層。

為什麼說 GraphQL 可以取代 REST API?


微服務架構很好,但也存在一些問題,GraphQL 可以用來解決這些問題。以下是來自 IBM 在微服務架構中使用 GraphQL 的經驗:

“總的來說,GraphQL 微服務的開發和部署都非常快。他們 5 月份開始開發,7 月份就進入了生產環境。因為他們不需要徵得許可,直接開幹。他強烈推薦這個方案,比開會討論好太多了。”

接下來,讓我們逐一討論 GraphQL 的每一個好處。

提高開發速度

首先,GraphQL 有助於減少發出的請求數。通過單個調用來獲取所需的數據比使用多個請求要容易得多。從工程師的角度來看,這加快了開發速度。後面我會解釋更多有關為什麼會提升開發速度的原因,但現在我想先說明另一個問題。

後端和客戶端團隊需要通過密切合作來定義 API、測試它們,並做出更改。前端、移動、物聯網(例如 Alexa)等客戶端團隊不斷迭代功能,並嘗試使用新的 UX 和設計。他們的數據需求經常發生變化,後端團隊必須跟上他們的節奏。如果客戶端和後端代碼由同一團隊負責,那麼問題就沒那麼嚴重了。Indeed 的大多數工程團隊都是由全棧工程師組成,但並非全部都是這樣。對於非全棧團隊,客戶端團隊經常因為依賴了後端團隊開發速度受到影響。

當我轉到 Job Seeker API 團隊時,移動團隊開始我們的開發進度。我們之間有很多關於參數、響應字段和測試的事情需要溝通。

在使用了 GraphQL 之後,客戶端工程師就可以完全控制前端,不需要依賴任何人,因為他們可以告訴後端他們需要什麼以及響應結構應該是怎樣的。他們使用了 GraphQL 查詢,它們會告訴後端 API 應該要提供哪些數據。

客戶端工程師不需要花時間讓後端 API 團隊添加或修改某些內容。GraphQL 具有自文檔的特點,所以可以節省一些用於查找文檔以便了解如何使用 API 的時間。我相信大多數人曾經在找出確切的請求參數方面浪費了很多時間。GraphQL 協議本身及其社區在文檔方面為我們提供了一些有用的工具。在某些情況下,可以從模式自動生成文檔。其他時候,只需使用 GraphiQL Web 界面就足以編寫一個查詢。

來自紐約時報的工程師表示,他們在轉到 GraphQL 和 Relay 之後,在做出變更時不需要改太多的東西:

“當我們想要更新所有產品的設計時,不再需要修改多個代碼庫。這就是我們想要的。我們認為 Relay 和 GraphQL 是幫助我們實現這個偉大目標的完美工具。”

當一家公司已經擁有大量 GraphQL API,然後有人想出了一個新的產品創意,這也是我最喜歡 GraphQL 的應用場景。使用已有的 GraphQL API 實現原型比調用各種 REST 端點(將提供太少或太多的數據)或為新應用程序構建新的 REST API 要快得多。

開發速度的提升與開發者體驗的提升密切相關。

提升開發者體驗

GraphQL 提供了更好的開發者體驗(DX),開發者將花更少的時間思考如何獲取數據。在使用 Apollo 時,他們只需要在 UI 中聲明數據。數據和 UI 放在一起,閱讀代碼和編寫代碼都變得更方便。

通常,在開發 UI 時需要在 UI 模板、客戶端代碼和 UI 樣式之間跳轉。GraphQL 允許工程師在客戶端開發 UI,減少摩擦,因為工程師在添加或修改代碼時無需在文件之間切換。如果你熟悉 React,這裡有一個很好的比喻:GraphQL 之於數據,就像 React 之於 UI。

下面是一個簡單的示例,UI 中直接包含了屬性名稱launch.name和 launch.rocket.name 。

複製代碼

const GET_LAUNCHES = gql`
query launchList($after: String) {
launches(after: $after) {
launches {
id
name
isBooked
rocket {
id
name
}
}
}
}
`;

export default function Launches() {
return (
<query>
{({ data, loading, error }) => {
if (loading) return <loading>;
if (error) return

ERROR

;

return (

{data.launches.launches.map(launch => (
key={launch.id}
>{launch.name}

Rocket: {launch.rocket.name}

))}

);
}}
/<query>
);
};

使用這種方法,可以非常容易地修改或向 UI 或查詢(gql)添加新字段。React 組件的可移植性更強了,因為它們描述了所需的所有數據。

如前所述, GraphQL 提供了更好的文檔,而且還有一個叫作 GraphiQL 的 IDE:

為什麼說 GraphQL 可以取代 REST API?


前端工程師很喜歡 GraphiQL,下面引用 Indeed 的一位高級工程師說過的話:

“我認為開發體驗中最好的部分是能夠使用 GraphiQL。對我來說,與典型的 API 文檔相比,這是一種編寫查詢更有效的輔助方法”。

GraphQL 的另一個很棒的功能是片段,因為它允許我們在更高的組件層面重用查詢。

這些功能改善了開發者體驗,讓開發人員更快樂,更不容易出現 JavaScript 疲勞。

提升性能

工程師並不是唯一從 GraphQL 中受益的人。用戶也會從中受益,因為應用程序的性能獲得了提升(可以感知到的):

1. 減少了有效載荷(客戶端只需要必要的東西);

2. 多個請求合併為一個請求可減少網絡開銷;

3. 使用工具可以更輕鬆地實現客戶端緩存和後端批處理和後端緩存;

4. 預取;

5. 更快的 UI 更新。

PayPal 使用 GraphQL 重新設計了他們的結賬流程。下面是來自用戶的反饋:

“REST 的原則並沒有為 Web 和移動應用及其用戶的需求考慮,這個在結賬優化交易中體現得尤為明顯。用戶希望能夠儘快完成結賬,如果應用程序使用了很多原子 REST API,就需要在客戶端和服務器之間進行多次往返以獲取數據。我們的結賬每次往返網絡時間至少需要 700 毫秒,這還不包括服務器處理請求的時間。每次往返都會導致渲染變慢,用戶體驗不好,結算轉換率也會降低。”

性能改進中有一項是“多個請求組合成一個請求可以減少網絡開銷”。對於 HTTP/1 而言,這是非常正確的,因為它沒有 HTTP/2 那樣的多路複用機制。但儘管 HTTP/2 提供的多路複用機制有助於優化單獨的請求,但它對於圖遍歷(獲取相關或嵌套對象)並沒有實際幫助。讓我們來看一看 REST 和 GraphQL 是如何處理嵌套對象和其他複雜請求的。

標準化和簡化複雜的 API

通常,客戶端會發出複雜的請求來獲取有序、排好序、被過濾過的數據或子集(用於分頁),或者請求嵌套對象。GraphQL 支持嵌套數據和其他難以使用標準 REST API 資源(也叫端點或路由)實現的查詢。

例如,我們假設有三種資源:用戶、訂閱和簡歷。工程師需要按順序進行兩次單獨的調用(這會降低性能)來獲取一個用戶簡歷,首先需要通過調用獲取用戶資源,拿到簡歷 ID,然後再使用簡歷 ID 來獲取簡歷數據。對於訂閱來說也是一樣的。

1.GET /users/123:響應中包含了簡歷 ID 和工作崗位通知訂閱的 ID 清單;

2.GET /resumes/ABC:響應中包含了簡歷文本——依賴第一個請求;

3.GET /subscriptions/XYZ:響應中包含了工作崗位通知的內容和地址——依賴第一個請求。

上面的示例很糟糕,原因有很多:客戶端可能會獲得太多數據,並且必須等待相關的請求完成了以後才能繼續。此外,客戶端需要實現如何獲取子資源(例如建立或訂閱)和過濾。

想象一下,一個客戶端可能只需要第一個訂閱的內容和地址以及簡歷中的當前職位,另一個客戶端可能需要所有訂閱和整個簡歷列表。所以,如果使用 REST API,對第一個客戶端來說有點不划算。

另一個例子:用戶表裡可能會有用戶的名字和姓氏、電子郵件、簡歷、地址、電話、社會保障號、密碼(當然是經過混淆的)和其他私人信息。並非每個客戶端都需要所有字段,有些應用程序可能只需要用戶電子郵件,所以向這些應用程序發送社會保障號等信息就不太安全。

當然,為每個客戶端創建不同的端點也是不可行的,例如 /api/v1/users 和 /api/v1/usersMobile。事實上,各種客戶端通常都有不同的數據需求:/api/v1/userPublic、/api/v1/userByName、/api/v1/usersForAdmin,如果這樣的話,端點會呈指數級增長。

GraphQL 允許客戶要求 API 發送他們想要的字段,這將使後端工作變得更加容易:/api/gql——所有客戶端只需要這個端點。

注意:對於 REST 和 GraphQL,後端都需要使用訪問控制級別。

或者可以使用舊 REST 來實現 GraphQL 的很多功能。但是這樣要付出什麼代價?後端可以支持複雜的 RESTful 請求,這樣客戶端就可以使用字段和嵌套對象進行調用:

複製代碼

GET /users/?fields=name,address&include=resumes,subscriptions

上面的請求將比使用多個 REST 請求更好,但它不是標準化的,不受客戶端庫支持,而且這樣的代碼也更難編寫和維護。對於相對複雜的 API,工程師需要在查詢中使用自己的查詢字符串參數約定,最終得到類似 GraphQL 的東西。既然 GraphQL 已經提供了標準和庫,為什麼還要基於 REST 設計自己的查詢約定呢?

將複雜的 REST 端點與以下的 GraphQL 嵌套查詢進行對比,嵌套查詢使用了更多的過濾條件,例如“只要給我前 X 個對象”和“按時間按升序排列”(可以添加無限制的過濾選項):

複製代碼

{
user (id: 123) {
id
firstName
lastName
address {
city
country
zip
}
resumes (first: 1, orderBy: time_ASC) {
text
title
blob
time
}
subscriptions(first: 10) {
what
where
time
}
}
}
}

在使用 GraphQL 時,我們可以在查詢中保留嵌套對象,對於每個對象,我們將精確地獲得我們需要的數據,不多也不少。

響應消息的數據格式反映了請求查詢的結構,如下所示:

複製代碼

{
"data": {
"user": {
"id": 123,
"firstName": "Azat",
"lastName": "Mardan",
"address": {

"city": "San Francisco",
"country": "US",
"zip": "94105"
},
"resumes" [
{
"text": "some text here...",
"title": "My Resume",
"blob": "<blob>",
"time": "2018-11-13T21:23:16.000Z"
},
],
"subscriptions": [ ]
},
"errors": []
}
/<blob>

相比複雜的 REST 端點,使用 GraphQL 的另一個好處是提高了安全性。這是因為 URL 經常會被記錄下來,而 RESTful GET 端點依賴於查詢字符串(是 URL 的一部分)。這可能會暴露敏感數據,所以 RESTful GET 請求的安全性低於 GraphQL 的 POST 請求。我打賭這就是為什麼 Indeed 主頁會使用 POST 發出“閱讀”頁面請求。

使用 GraphQL 可有更容易地實現分頁等關鍵功能,這要歸功於查詢以及 BaaS 提供商提供的標準,以及後端的實現和客戶端庫使用的標準。

改進的安全性、強類型和驗證

GraphQL 的 schema 與語言無關。對前面的示例進行擴展,我們可以在 schema 中定義 Address 類型:

複製代碼

type Address {
city: String!

country: String!
zip: Int
}

String 和 Int 是標量類型,! 表示字段不可為空。

schema 驗證是 GraphQL 規範的一部分,因此像這樣的查詢將返回錯誤,因為 name 和 phone 不是 Address 對象的字段:

複製代碼

{
user (id: 123) {
address {
name
phone
}
}
}

我們可以使用我們的類型構建複雜的 GraphQL schema。例如,用戶類型可能會使用我們的地址、簡歷和訂閱類型,如下所示:

複製代碼

type User {
id: ID!
firstName: String!
lastName: String!
address: Address!
resumes: [Resume]
subscriptions: [Subscription]
}

Indeed 的大量對象和類型都是使用 ProtoBuf 定義的。類型化數據並不是什麼新鮮事物,而且類型數據的好處也是眾所周知。與發明新的 JSON 類型標準相比,GraphQL 的優點在於已經存在可以從 ProtoBuf 自動換換到 GraphQL 的庫。即使其中一個庫(rejoiner)不能用,也可以開發自己的轉換器。

GraphQL 提供了比 JSON RESTful API 更強的安全性,主要有兩個原因:強類型 schema(例如數據驗證和無 SQL 注入)以及精確定義客戶端所需數據的能力(不會無意洩漏數據)。

靜態驗證是另一個優勢,可以幫助工程師節省時間,並在進行重構時提升工程師的信心。諸如eslint-plugin-graphql之類的工具可以讓工程師知道後端發生的變化,並讓後端工程師確保不會破壞客戶端代碼。

保持前端和後端之間的契約是非常重要的。在使用 REST API 時,我們要小心不要破壞了客戶端代碼,因為客戶端無法控制響應消息。相反,GraphQL 為客戶端提供了控制,GraphQL 可以頻繁更新,而不會因為引入了新類型造成重大變更。因為使用了 schema,所以 GraphQL 是一種無版本的 API。

GraphQL 的實現

在選擇實現 GraphQL API 的平臺時,Node 是一個候選項,因為最初 GraphQL 用於 Web 應用程序和前端,而 Node 是開發 Web 應用程序的首選,因為它是基於 JavaScript 的。使用 Node 可以非常容易地實現 GraphQL(假設提供了 schema)。事實上,使用 Express 或 Koa 來實現只需要幾行代碼:

複製代碼

const Koa = require('koa');
const Router = require('koa-router'); // [email protected]
const graphqlHTTP = require('koa-graphql');


const app = new Koa();
const router = new Router();

router.all('/graphql', graphqlHTTP({
schema: schema,
graphiql: true
}));

app.use(router.routes()).use(router.allowedMethods());

schema 是使用 npm 的 graphql 中的類型來定義的。Query 和 Mutation 是特殊的 schema 類型。

GraphQL API 的大部分實現都在於 schema 和解析器。解析器可以包含任意代碼,但最常見的是以下五個主要類別:

  • 調用 Thrift、gRPC 或其他 RPC 服務;
  • 調用 HTTP REST API(當優先事項不是重寫現有 REST API 時);
  • 直接調用數據存儲;
  • 調用其他 GraphQL schema 查詢或服務;
  • 調用外部 API。

這裡有一個示例。

Node 很棒,但在 Indeed,我們主要使用 Java。包括 Java 在內的很多語言都支持 GraphQL,例如https://github.com/graphql-go和https://github.com/graphql-python。

由於 Indeed 主要使用了 Java,因此這裡給出一個使用 graphql-java 的 Java GraphQL 示例,完整代碼位於這裡。它定義了 /graphql 端點:

複製代碼

import com.coxautodev.graphql.tools.SchemaParser;
import javax.servlet.annotation.WebServlet;
import graphql.servlet.SimpleGraphQLServlet;

@WebServlet(urlPatterns = "/graphql")
public class GraphQLEndpoint extends SimpleGraphQLServlet {

public GraphQLEndpoint() {
super(SchemaParser.newParser()
.file("schema.graphqls") //parse the schema file created earlier
.build()
.makeExecutableSchema());
}
}

GraphQL 的 schema 使用 POJO 來定義。GraphQL 端點類使用了 LinkRepository POJO。解析器包含了操作的(例如獲取鏈接)實際代碼:

複製代碼

@WebServlet(urlPatterns = "/graphql")
public class GraphQLEndpoint extends SimpleGraphQLServlet {

public GraphQLEndpoint() {
super(buildSchema());
}

private static GraphQLSchema buildSchema() {
LinkRepository linkRepository = new LinkRepository();
return SchemaParser.newParser()
.file("schema.graphqls")
.resolvers(new Query(linkRepository))
.build()
.makeExecutableSchema();
}
}

在很多情況下,GraphQL 的 schema 可以從其他類型的 schema 自動生成,例如 gRPC、Boxcar、ProtoBuf 或 ORM/ODM。

GraphQL 不一定需要客戶端。一個簡單的 GraphQL 請求就是一個常規的 POST HTTP 請求,其中包含了查詢內容。我們可以使用任意的 HTTP 代理庫(如 CURL、axios、fetch、superagent 等)來生成請求。例如,在終端中使用 curl 發送請求:

複製代碼

curl \\
-X POST \\
-H "Content-Type: application/json" \\
--data '{ "query": "{ posts { title } }" }' \\
https://1jzxrj179.lp.gql.zone/graphql

以下代碼可以在任意一個現代瀏覽器(為了避免 CORS,請訪問 launchpad.graphql.com)中運行。

複製代碼

fetch('https://1jzxrj179.lp.gql.zone/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: '{ posts { title } }' }),
})
.then(res => res.json())
.then(res => console.log(res.data));

雖然構建 GraphQL 請求很容易,但是還需要實現很多其他東西,比如緩存,因為緩存可以極大地改善用戶體驗。構建客戶端緩存不是那麼容易,所幸的是,Apollo 和 Relay Modern 等提供了開箱即用的客戶端緩存。

什麼時候不該使用 GraphQL?

當然,完美的解決方案是不存在的(儘管 GraphQL 接近完美),還有一些問題需要注意,例如:

1. 它有單點故障嗎?

2. 它可以擴展嗎?

3. 誰在使用 GraphQL?

最後,以下列出了我們自己的有關 GraphQL 可能不是一個好選擇的主要原因:

  • 當客戶端的需求很簡單時:如果你的 API 很簡單,例如 /users/resumes/123,那麼 GraphQL 就顯得有點重了;
  • 為了加快加載速度使用了異步資源加載;
  • 在開發新產品時使用新的 API,而不是基於已有的 API;
  • 不打算向公眾公開 API;
  • 不需要更改 UI 和其他客戶端;
  • 產品開發不活躍;
  • 使用了其他一些 JSON schema 或序列化格式。

總結

GraphQL 是一種協議和一種查詢語言。GraphQL API 可以直接訪問數據存儲,但在大多數情況下,GraphQL API 是一個數據聚合器和一個抽象層,一個可以提升開發速度、減少維護工作並讓開發人員更快樂的層。因此,GraphQL 比公共 API 更有意義。很多公司開始採用 GraphQL。IBM、PayPal 和 GitHub 聲稱在使用 GraphQL 方面取得了巨大的成功。如果 GraphQL 很有前途,我們現在是否可以停止構建過時且笨重的 REST API,並擁抱 GraphQL?

英文原文:https://webapplog.com/graphql/


分享到:


相關文章: